mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2025-12-16 12:08:36 +00:00
1333 lines
40 KiB
C++
1333 lines
40 KiB
C++
#include "server/game.h"
|
|
#include "server/ids.h"
|
|
#include "server/statsposter.h"
|
|
#include "server/room.h"
|
|
#include "server/lobby.h"
|
|
#include "base/tick.h"
|
|
#include "base/md5.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <algorithm>
|
|
|
|
namespace wi {
|
|
|
|
const long64 kctStartTimeout = 4500; // 45 second timeout to start a game
|
|
|
|
// This is how much time to give all clients to acknowledge a
|
|
// lag notification message.
|
|
const long64 kctLagAckKill = 3000;
|
|
|
|
dword Game::s_gameidCounter_;
|
|
|
|
Game::Game(Endpoint *endpoint, const GameParams& params, const LevelInfo& info,
|
|
Server& server, dword roomid, dword ff) : playerMgr_(server),
|
|
params_(params), info_(info), server_(server), roomid_(roomid),
|
|
ff_(ff), tLastLagNotify_(0), playing_(false), cUpdatesBlock_(-1),
|
|
msClock_(0), cUpdatesWaitSend_(0), cmsRate_(kcmsRateMax),
|
|
msCommandsSent_(0), pidLagging_(kpidNeutral), syncerror_(false),
|
|
advertiser_(endpoint), advertiserId_(endpoint->id()), id_(NewGameId()),
|
|
cSecsStart_(0), cSecsEnd_(0) {
|
|
|
|
// This only hangs around until this endpoint joins the game,
|
|
// so the game will go away if the endpoint goes away.
|
|
advertiser_->SignalOnDelete.connect(this, &Game::OnAdvertiserDelete);
|
|
|
|
strncpyz(creator_, endpoint->name(), sizeof(creator_));
|
|
tCreated_ = base::GetTickCount();
|
|
playerMgr_.Init(info_);
|
|
}
|
|
|
|
Game::~Game() {
|
|
LOG() << base::Log::Format("0x%p", this);
|
|
if (advertiser_ != NULL) {
|
|
advertiser_->SignalOnDelete.disconnect(this);
|
|
advertiser_ = NULL;
|
|
}
|
|
SignalOnDelete(this);
|
|
PostWinStats();
|
|
}
|
|
|
|
dword Game::NewGameId() {
|
|
s_gameidCounter_++;
|
|
if (s_gameidCounter_ == 0) {
|
|
s_gameidCounter_ = 1;
|
|
}
|
|
return s_gameidCounter_;
|
|
}
|
|
|
|
void Game::PostWinStats() {
|
|
if (ff_ & kfGameDontPostWinStats) {
|
|
return;
|
|
}
|
|
if (!playing_) {
|
|
return;
|
|
}
|
|
|
|
// Collect stats
|
|
|
|
GameStats stats;
|
|
memset(&stats, 0, sizeof(stats));
|
|
stats.server_id = server_.id();
|
|
stats.server_start = server_.start_time();
|
|
stats.gameid = id_;
|
|
stats.params = params_;
|
|
stats.info = info_;
|
|
stats.start_utc = cSecsStart_;
|
|
stats.end_utc = cSecsEnd_ != 0 ? cSecsEnd_ : base::GetSecondsUnixEpocUTC();
|
|
int count = 0;
|
|
Player *pplr = NULL;
|
|
while ((pplr = playerMgr_.GetNextPlayer(pplr)) != NULL) {
|
|
pplr->GetPlayerStats(&stats.player_stats[count]);
|
|
count++;
|
|
}
|
|
stats.player_count = count;
|
|
server_.poster().Submit(stats);
|
|
}
|
|
|
|
dword Game::CanAddPlayer(Endpoint *endpoint) {
|
|
if (disposed_) {
|
|
return knGameJoinResultGameNotFound;
|
|
}
|
|
if (playing_) {
|
|
return knGameJoinResultInProgress;
|
|
}
|
|
|
|
int cpplrUnfulfilled = 0;
|
|
Player *pplr = NULL;
|
|
while ((pplr = playerMgr_.GetNextHumanPlayer(pplr)) != NULL) {
|
|
if (pplr->flags() & kfPlrUnfulfilled) {
|
|
cpplrUnfulfilled++;
|
|
}
|
|
}
|
|
if (cpplrUnfulfilled == 0) {
|
|
return knGameJoinResultGameFull;
|
|
}
|
|
if (endpoint->anonymous() && anonblock()) {
|
|
return knGameJoinResultFail;
|
|
}
|
|
|
|
return knGameJoinResultSuccess;
|
|
}
|
|
|
|
void Game::AddPlayer(Endpoint *endpoint) {
|
|
// Add to list; establish callbacks
|
|
ConnectedList::iterator it = std::find(connected_.begin(),
|
|
connected_.end(), endpoint);
|
|
if (it == connected_.end()) {
|
|
connected_.push_back(endpoint);
|
|
if (endpoint == advertiser_) {
|
|
endpoint->SignalOnDelete.disconnect(this);
|
|
advertiser_ = NULL;
|
|
}
|
|
endpoint->SignalOnDelete.connect(this, &Game::OnEndpointDelete);
|
|
}
|
|
|
|
// Clients expect knmidScGameParams on connect
|
|
GameParamsNetMessage nm;
|
|
nm.nmid = knmidScGameParams;
|
|
nm.cb = sizeof(nm);
|
|
nm.rams = params_;
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nm));
|
|
}
|
|
|
|
void Game::RemovePlayer(Endpoint *endpoint, int reason, bool disconnect) {
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "endpoint disconnecting, reason:" << reason;
|
|
|
|
if (endpoint->id() == advertiserId_) {
|
|
// If this game hasn't started yet, dispose the game
|
|
if (!playing_) {
|
|
LOG() << base::Log::Format("0x%p ", this)
|
|
<< "advertiser leaving game, auto disposing!";
|
|
Dispose();
|
|
}
|
|
}
|
|
|
|
// Find this endpoint in the connected_ list.
|
|
ConnectedList::iterator it = std::find(connected_.begin(),
|
|
connected_.end(), endpoint);
|
|
if (it == connected_.end()) {
|
|
return;
|
|
}
|
|
|
|
// If this endpoint is assigned a player, unassign it, which is different
|
|
// if playing vs. not playing yet.
|
|
Player *pplr = playerMgr_.GetPlayer(endpoint);
|
|
if (pplr != NULL) {
|
|
if (!playing_) {
|
|
// Not yet playing the game. The player this endpoint was using
|
|
// is now available for anyone else.
|
|
pplr->SetFlags(pplr->flags() & ~(kfPlrReady | kfPlrHumanJoined));
|
|
pplr->SetFlags(pplr->flags() | kfPlrUnfulfilled);
|
|
pplr->SetEndpoint(NULL);
|
|
playerMgr_.BroadcastPlayersUpdate();
|
|
|
|
// Notify that players have changes
|
|
SignalOnPlayersChange(this);
|
|
} else {
|
|
// The game is playing; turn this player into a computer player.
|
|
// Tell other players this player is disconnecting.
|
|
pplr->SetFlags(pplr->flags() | kfPlrComputer);
|
|
pplr->SetEndpoint(NULL);
|
|
QueuePlayerDisconnect(pplr->pid(), reason);
|
|
|
|
// Notify that players have changes
|
|
SignalOnPlayersChange(this);
|
|
}
|
|
}
|
|
|
|
// Remove signaling and remove from list. If the list size is now zero,
|
|
// schedule this game for auto-deletion.
|
|
connected_.erase(it);
|
|
if (disconnect) {
|
|
endpoint->SignalOnDelete.disconnect(this);
|
|
}
|
|
if (connected_.size() == 0) {
|
|
LOG() << base::Log::Format("0x%p ", this)
|
|
<< "no endpoints connected - auto disposing!";
|
|
Dispose();
|
|
}
|
|
}
|
|
|
|
void Game::OnAdvertiserDelete(Endpoint *endpoint) {
|
|
LOG() << "name: " << endpoint->name();
|
|
|
|
// This will only get called if the advertiser goes away before
|
|
// joining the game. Don't disconnect from this signal while it is
|
|
// being called.
|
|
Dispose();
|
|
advertiser_ = NULL;
|
|
}
|
|
|
|
void Game::OnEndpointDelete(Endpoint *endpoint) {
|
|
// Player should be gone by now; if not it's abnormal disconnect
|
|
LOG() << "removing " << endpoint->name();
|
|
RemovePlayer(endpoint, knDisconnectReasonAbnormal, false);
|
|
}
|
|
|
|
void Game::OnNetMessage(Endpoint *endpoint, NetMessage *pnm) {
|
|
switch (pnm->nmid) {
|
|
case knmidCsClientCommands:
|
|
OnClientCommands(endpoint, (ClientCommandsNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsClientReady:
|
|
OnClientReady(endpoint, pnm);
|
|
break;
|
|
|
|
case knmidCsPlayerJoin:
|
|
OnPlayerJoin(endpoint, (PlayerJoinNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsUpdateResult:
|
|
OnUpdateResult(endpoint, (UpdateResultNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsConnect:
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "Received knmidCsConnect unexpectedly!";
|
|
break;
|
|
|
|
case knmidCsPlayerResign:
|
|
OnPlayerResign(endpoint, (PlayerResignNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsRequestBeginGame:
|
|
OnRequestBeginGame(endpoint, pnm);
|
|
break;
|
|
|
|
case knmidCsKillLaggingPlayer:
|
|
OnKillLaggingPlayer(endpoint, (KillLaggingPlayerNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsLagAcknowledge:
|
|
OnLagAcknowledge(endpoint);
|
|
break;
|
|
|
|
case knmidCsWinStats:
|
|
OnWinStats(endpoint, (WinStatsNetMessage *)pnm);
|
|
break;
|
|
|
|
case knmidCsChallengeWin:
|
|
OnChallengeWin(endpoint);
|
|
break;
|
|
|
|
default:
|
|
Assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Game::OnPlayerJoin(Endpoint *endpoint, PlayerJoinNetMessage *ppjnm) {
|
|
// If already playing, this message means nothing
|
|
if (playing_) {
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "received PlayerJoin while playing.";
|
|
NetMessage nmsg(knmidScCantAcceptMoreConnections);
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nmsg));
|
|
return;
|
|
}
|
|
|
|
// Are there any unfulfilled slots for human players?
|
|
Player *applrUnfulfilled[kcPlayersMax];
|
|
int cpplrUnfulfilled = 0;
|
|
Player *pplr = NULL;
|
|
while ((pplr = playerMgr_.GetNextHumanPlayer(pplr)) != NULL) {
|
|
if (pplr->flags() & kfPlrUnfulfilled) {
|
|
applrUnfulfilled[cpplrUnfulfilled++] = pplr;
|
|
}
|
|
}
|
|
|
|
// Don't accept this joiner if all the human slots are in use
|
|
if (cpplrUnfulfilled == 0) {
|
|
NetMessage nmsg(knmidScCantAcceptMoreConnections);
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nmsg));
|
|
return;
|
|
}
|
|
|
|
// Randomly assign this joiner to one of the unfulfilled player slots
|
|
pplr = applrUnfulfilled[rand() % cpplrUnfulfilled];
|
|
pplr->SetFlags(pplr->flags() & ~kfPlrUnfulfilled);
|
|
pplr->SetFlags(pplr->flags() | kfPlrHumanJoined);
|
|
//pplr->SetName(ppjnm->szPlayerName);
|
|
pplr->SetEndpoint(endpoint);
|
|
|
|
// Is this the first joining player? If so remember it is the creator.
|
|
if (playerMgr_.GetEndpointCount() == 1) {
|
|
pplr->SetFlags(pplr->flags() | kfPlrCreator);
|
|
}
|
|
|
|
// Notify all players of the new player + all old players
|
|
playerMgr_.BroadcastPlayersUpdate();
|
|
|
|
// Player names have changed
|
|
SignalOnPlayersChange(this);
|
|
}
|
|
|
|
void Game::OnClientReady(Endpoint *endpoint, NetMessage *pnm) {
|
|
// If already playing, this message means nothing
|
|
if (playing_) {
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "received ClientReady while playing.";
|
|
return;
|
|
}
|
|
|
|
// Update this player's state in the master list and let all players know
|
|
Player *pplr = playerMgr_.GetPlayer(endpoint);
|
|
if (pplr == NULL) {
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "has no player.";
|
|
return;
|
|
}
|
|
pplr->SetFlags(pplr->flags() | kfPlrReady);
|
|
playerMgr_.BroadcastPlayersUpdate();
|
|
}
|
|
|
|
void Game::OnRequestBeginGame(Endpoint *endpoint, NetMessage *pnm) {
|
|
LOG();
|
|
|
|
// Sent by the creator to begin a game
|
|
NetMessage nmFail(knmidScBeginGameFail);
|
|
if (playing_) {
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nmFail));
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "received RequestBeginGame while playing.";
|
|
return;
|
|
}
|
|
|
|
// Make sure there are enough players ready to begin the game
|
|
int cplrReady = 0;
|
|
Player *pplr = NULL;
|
|
while ((pplr = playerMgr_.GetNextHumanPlayer(pplr)) != NULL) {
|
|
word wf = pplr->flags();
|
|
if ((wf & kfPlrUnfulfilled) != 0) {
|
|
continue;
|
|
}
|
|
|
|
// Count # of players ready. If any player isn't ready,
|
|
// the game isn't ready to begin.
|
|
if (wf & kfPlrReady) {
|
|
cplrReady++;
|
|
} else {
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nmFail));
|
|
return;
|
|
}
|
|
}
|
|
if (cplrReady < info_.minplayers()) {
|
|
endpoint->xpump().Send(XMsgGameNetMessage::ToBuffer(&nmFail));
|
|
return;
|
|
}
|
|
|
|
// All ready. Convert unfulfilled Human players into Computer players.
|
|
// The client handles these by removing all pre-created units.
|
|
bool fPlayersChanged = false;
|
|
pplr = NULL;
|
|
while ((pplr = playerMgr_.GetNextHumanPlayer(pplr)) != NULL) {
|
|
word wf = pplr->flags();
|
|
if (wf & kfPlrUnfulfilled) {
|
|
pplr->SetFlags(wf | kfPlrComputer | kfPlrRemovedAtGameStart);
|
|
fPlayersChanged = true;
|
|
}
|
|
}
|
|
|
|
// Let everyone know the new player state
|
|
if (fPlayersChanged) {
|
|
playerMgr_.BroadcastPlayersUpdate();
|
|
}
|
|
|
|
// Broadcast the BeginGame notification
|
|
BeginGameNetMessage bgnm;
|
|
bgnm.ulRandomSeed = (dword)base::GetMillisecondCount();
|
|
playerMgr_.Broadcast(&bgnm);
|
|
|
|
// Remove all endpoints that aren't playing the game.
|
|
// These are endpoints that have connected to the game, but haven't
|
|
// joined. Normally there should be zero of these, but the protocol
|
|
// allows for it. This casuses synchronous changes to connected_, so
|
|
// re-enumerate if and endpoint is removed.
|
|
while (true) {
|
|
bool fDropped = false;
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
if (playerMgr_.GetPlayer(*it) == NULL) {
|
|
(*it)->DropGame(this, knDisconnectReasonKilled);
|
|
fDropped = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!fDropped) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The game is now playing
|
|
cSecsStart_ = base::GetSecondsUnixEpocUTC();
|
|
StartTiming();
|
|
|
|
// Let room subscribers know
|
|
SignalOnInProgress(this);
|
|
}
|
|
|
|
void Game::KickPlayers() {
|
|
// Forcefully disconnect players, and therefore take down this game
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
if (!(*it)->IsModerator()) {
|
|
(*it)->Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::StartTiming() {
|
|
// Calculate the timer rate. Rate limit it. This does not need to
|
|
// match the client update rate - the two run independently.
|
|
long cmsUpdate = params_.tGameSpeed * 10;
|
|
if (cmsUpdate < kcmsRateMax) {
|
|
cmsRate_ = kcmsRateMax;
|
|
} else {
|
|
cmsRate_ = cmsUpdate;
|
|
}
|
|
|
|
// The client sends UR for 0 to start things off
|
|
|
|
cUpdatesWaitSend_ = 0;
|
|
cUpdatesBlock_ = 0;
|
|
cUpdatesSync_ = 0;
|
|
msClock_ = cUpdatesBlock_ * cmsUpdate;
|
|
msCommandsSent_ = base::GetMillisecondCount();
|
|
|
|
// Calculate the lag amount: N seconds in terms of updates
|
|
|
|
cUpdatesAllowedLag_ = kcmsLagging / cmsUpdate;
|
|
pidLagging_ = kpidNeutral;
|
|
|
|
// Begin the game timer. Remember when the timer started, so that
|
|
// it is possible to detect lagging clients.
|
|
tStartTimeout_ = base::GetTickCount() + kctStartTimeout;
|
|
playing_ = true;
|
|
}
|
|
|
|
bool Game::HasHeartbeat() {
|
|
// Need heartbeat during the game
|
|
if (playing_) {
|
|
return true;
|
|
}
|
|
|
|
// After the game has been created but before endpoints have joined,
|
|
// the heartbeat will auto-destroy the game after awhile.
|
|
if (connected_.size() == 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Game::OnHeartbeat() {
|
|
// Auto-dispose after awhile if no endpoints have joined this game
|
|
if (!playing_) {
|
|
if (connected_.size() != 0) {
|
|
return;
|
|
}
|
|
if (base::GetTickCount() > tCreated_ + 100 * 30) {
|
|
Dispose();
|
|
}
|
|
return;
|
|
}
|
|
|
|
OnTimer();
|
|
}
|
|
|
|
void Game::OnTimer() {
|
|
LOG() << "cUpdatesBlock_: " << cUpdatesBlock_;
|
|
|
|
// If sync error, do nothing
|
|
|
|
if (syncerror_) {
|
|
return;
|
|
}
|
|
|
|
// Wait for initial UpdateResult from all players.
|
|
|
|
if (StartWait()) {
|
|
return;
|
|
}
|
|
|
|
// Wait for lagging players. cUpdatesAllowedLag_ gives grace period of
|
|
// "allowed lag".
|
|
if (LagWait(cUpdatesBlock_ - cUpdatesAllowedLag_)) {
|
|
return;
|
|
}
|
|
|
|
// Send the next Update to clients
|
|
// Send two commands to start the overlap if just starting out
|
|
|
|
if (cUpdatesWaitSend_ == 0) {
|
|
SendClientCommands();
|
|
SendClientCommands();
|
|
} else {
|
|
SendClientCommands();
|
|
}
|
|
}
|
|
|
|
bool Game::StartWait() {
|
|
// The first UpdateResult is sent spontaneously. If all players have
|
|
// sent it, no need to block.
|
|
|
|
if (playerMgr_.HaveAllPlayersReachedUpdate(0)) {
|
|
return false;
|
|
}
|
|
|
|
// Have players who haven't started the same as lagging is handled
|
|
// normally.
|
|
|
|
LagWait(0);
|
|
return true;
|
|
}
|
|
|
|
bool Game::LagWait(long cUpdatesOldest) {
|
|
// Find the laggy player, if there is one
|
|
|
|
Player *pplrLagging = playerMgr_.FindLaggingPlayer(cUpdatesOldest);
|
|
|
|
// Remember which player is lagging, for next time
|
|
|
|
Player *pplrLaggingLast = playerMgr_.GetPlayerFromPid(pidLagging_);
|
|
if (pplrLagging == NULL) {
|
|
pidLagging_ = kpidNeutral;
|
|
} else {
|
|
pidLagging_ = pplrLagging->pid();
|
|
}
|
|
|
|
// If there is no lagging player, and there was no lagging player
|
|
// last time, we're done.
|
|
|
|
if (pplrLagging == NULL && pplrLaggingLast == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// A message may need to be sent now. If there is an abrupt change, from
|
|
// lag to no lag, or vice versa, send a message now.
|
|
|
|
bool fSendNow = false;
|
|
if (pplrLagging != pplrLaggingLast) {
|
|
// There is an abrupt change from lag to no lag or vice versa.
|
|
// Send a message so the client can change UI
|
|
|
|
fSendNow = true;
|
|
|
|
// If going into a lag state, mark in the players when this
|
|
// started, so it's easy to tell if a player is not responding
|
|
// to the LagNotify message
|
|
|
|
playerMgr_.SetLastLagAcknowledge(base::GetTickCount());
|
|
} else {
|
|
// Same lagging player as last time. Send occasionally so client has
|
|
// latest lag info. Send only if interval has expired.
|
|
|
|
long64 tCurrent = base::GetTickCount();
|
|
if (llabs(tCurrent - tLastLagNotify_) >= 50) {
|
|
tLastLagNotify_ = tCurrent;
|
|
fSendNow = true;
|
|
}
|
|
}
|
|
|
|
// Send lag notification if necessary
|
|
|
|
if (fSendNow) {
|
|
LagNotifyNetMessage lnnm;
|
|
lnnm.pidLagging = pidLagging_;
|
|
if (pplrLagging == NULL) {
|
|
lnnm.cSeconds = 0;
|
|
} else {
|
|
lnnm.cSeconds = (word)pplrLagging->GetLagTimeout();
|
|
}
|
|
playerMgr_.Broadcast(&lnnm, lnnm.pidLagging);
|
|
|
|
// At least one player needs to acknowledge the lag notification, or
|
|
// else the server will go on a killing spree (to prevent zombie
|
|
// endpoints and games).
|
|
|
|
if (pplrLagging != NULL && pplrLagging->endpoint() != NULL) {
|
|
long64 tAcknowledge = playerMgr_.GetLastLagAcknowledge();
|
|
long64 tCurrent = base::GetTickCount();
|
|
if (tCurrent - tAcknowledge > kctLagAckKill) {
|
|
pplrLagging->endpoint()->DropGame(this,
|
|
knDisconnectReasonKilled);
|
|
}
|
|
}
|
|
}
|
|
|
|
return pplrLagging != NULL;
|
|
}
|
|
|
|
void Game::OnLagAcknowledge(Endpoint *endpoint) {
|
|
playerMgr_.SetLastLagAcknowledge(base::GetTickCount());
|
|
}
|
|
|
|
bool Game::ValidateWinStats(Endpoint *endpoint, WinStatsNetMessage *pnm) {
|
|
Player *pplrEndpoint = playerMgr_.GetPlayer(endpoint);
|
|
if (pplrEndpoint == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// Note: validate pnm->hash here, as needed to validate results sent
|
|
// from game. See game/Player.cpp.
|
|
|
|
// All appears fine
|
|
return true;
|
|
}
|
|
|
|
void Game::OnWinStats(Endpoint *endpoint, WinStatsNetMessage *pnm) {
|
|
LOG() << endpoint->name();
|
|
Player *pplrEndpoint = playerMgr_.GetPlayer(endpoint);
|
|
if (pplrEndpoint == NULL) {
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "Could not find player!";
|
|
return;
|
|
}
|
|
|
|
// When a client wins or loses, it sends stats for every player it knows
|
|
// about including computer players, which means every client is sending
|
|
// stats for every other client. The simple scenario is a two player game,
|
|
// where simultaneously one player wins, and one loses - the stats sent
|
|
// are consistent. If a player resigns in a 3 player game mid-way through,
|
|
// it will send stats but those stats will be stale by the time the game
|
|
// really ends. The server wants to record up to date stats, as much
|
|
// as possible, so clever updating is required.
|
|
if (!ValidateWinStats(endpoint, pnm)) {
|
|
#ifdef DEBUG_LOGGING
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "WinStats are invalid! " << PszFromNetMessage(pnm);
|
|
#endif
|
|
#ifdef RELEASE_LOGGING
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "WinStats are invalid! username: " << endpoint->name()
|
|
<< " ip address: "
|
|
<< endpoint->xpump().socket()->GetRemoteAddress().ToString();
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// Find the player these stats are for
|
|
Player *pplrStats = playerMgr_.GetPlayerFromPid(pnm->pid);
|
|
if (pplrStats == NULL) {
|
|
return;
|
|
}
|
|
if (!(pplrStats->flags() & kfPlrInUse)) {
|
|
return;
|
|
}
|
|
|
|
// Mark the game as ending when the first winner is announced
|
|
if (pnm->ws.ff & kfwsWinner) {
|
|
if (cSecsEnd_ == 0) {
|
|
cSecsEnd_ = base::GetSecondsUnixEpocUTC();
|
|
}
|
|
}
|
|
|
|
// If these stats are "owned" by the sender, treat them as
|
|
// authoritative, locking them in place. These come from
|
|
// human players.
|
|
if (pplrEndpoint == pplrStats) {
|
|
pplrStats->SaveWinStats(pnm->ws, true);
|
|
return;
|
|
}
|
|
|
|
// Otherwise it's what one player thinks the stats are of another.
|
|
// Just save this as the most recent one. Since human players lock
|
|
// their own stats, this updates computer players (unless a human
|
|
// player lost network connection).
|
|
pplrStats->SaveWinStats(pnm->ws, false);
|
|
|
|
// If this player is announcing win, allow other players to dispute it
|
|
if (pnm->ws.ff & kfwsWinner) {
|
|
CheckWinNetMessage cwnm;
|
|
cwnm.pid = pnm->pid;
|
|
playerMgr_.Broadcast(&cwnm);
|
|
}
|
|
}
|
|
|
|
void Game::OnChallengeWin(Endpoint *endpoint) {
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< endpoint->name() << " challenges the win!";
|
|
|
|
Player *pplr = playerMgr_.GetPlayer(endpoint);
|
|
if (pplr == NULL) {
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "Could not find player!";
|
|
return;
|
|
}
|
|
|
|
#if 0
|
|
// Not implemented.
|
|
// An endpoint is challenging who the winner is, which will invalidate
|
|
// the winner.
|
|
pplr->SetFlags(pplr->flags() | kfPlrChallengeWinner);
|
|
#endif
|
|
}
|
|
|
|
void Game::SendClientCommands() {
|
|
// Calculate when the client should next execute commands. If the commands
|
|
// are not there, the client will block. The server attempts to send the
|
|
// commands ahead of time, so they are there when the client needs them.
|
|
|
|
// Update cUpdatesBlock_
|
|
|
|
msClock_ += cmsRate_;
|
|
long cmsUpdate = params_.tGameSpeed * 10;
|
|
long cUpdatesBlockNew = (msClock_ + cmsUpdate - 1) / cmsUpdate;
|
|
if (cUpdatesBlockNew == cUpdatesBlock_) {
|
|
return;
|
|
}
|
|
cUpdatesWaitSend_ = cUpdatesBlock_;
|
|
cUpdatesBlock_ = cUpdatesBlockNew;
|
|
msCommandsSent_ = base::GetMillisecondCount();
|
|
|
|
LOG() << "Sending ClientCommands: cUpdatesBlock_: " << cUpdatesBlock_;
|
|
|
|
// Send UpdateNetMessage to client
|
|
int cmsg = cmds_.size();
|
|
int cbUnm = sizeof(UpdateNetMessage) + ((cmsg - 1) * sizeof(Message));
|
|
UpdateNetMessage *punm = (UpdateNetMessage *)new byte[cbUnm];
|
|
if (punm == NULL) {
|
|
LOG() << base::Log::Format("0x%p ", this)
|
|
<< "Allocation Error!";
|
|
Dispose();
|
|
return;
|
|
}
|
|
punm->nmid = knmidScUpdate;
|
|
punm->cb = cbUnm;
|
|
punm->cUpdatesBlock = cUpdatesBlock_;
|
|
punm->cUpdatesSync = cUpdatesSync_;
|
|
punm->cmsgCommands = cmsg;
|
|
Commands::const_iterator it = cmds_.begin();
|
|
wi::Message *pcmds = punm->amsgCommands;
|
|
for (; it != cmds_.end(); it++) {
|
|
*pcmds++ = *it;
|
|
}
|
|
cmds_.clear();
|
|
|
|
// Send updates to all players with connections.
|
|
|
|
Player *pplr = playerMgr_.GetNextPlayerWithEndpoint(NULL);
|
|
for (; pplr != NULL; pplr = playerMgr_.GetNextPlayerWithEndpoint(pplr)) {
|
|
pplr->endpoint()->xpump().Send(XMsgGameNetMessage::ToBuffer(punm));
|
|
}
|
|
delete punm;
|
|
}
|
|
|
|
void Game::OnKillLaggingPlayer(Endpoint *endpoint,
|
|
KillLaggingPlayerNetMessage *pnm) {
|
|
// If the player isn't lagging don't allow the kill
|
|
|
|
Player *pplr = playerMgr_.GetPlayerFromPid(pnm->pid);
|
|
if (pplr == NULL || pplr->lag() != knLagKill) {
|
|
return;
|
|
}
|
|
|
|
// Kill the player or reset the lag state for another timeout
|
|
|
|
if (pnm->fYes != 0) {
|
|
if (pplr->endpoint() != NULL) {
|
|
pplr->endpoint()->DropGame(this, knDisconnectReasonNotResponding);
|
|
}
|
|
} else {
|
|
// Set player lag state back to guilty. This way this player gets to
|
|
// wait through the kill timeout again.
|
|
|
|
pplr->SetLagState(knLagGuilty);
|
|
}
|
|
}
|
|
|
|
void Game::OnClientCommands(Endpoint *endpoint, ClientCommandsNetMessage *pnm) {
|
|
// If not yet playing, this message means nothing
|
|
if (!playing_) {
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "ClientCommands received while not in game";
|
|
return;
|
|
}
|
|
|
|
// Queue the messages from the client. They'll be sent out
|
|
// on the next game timer most likely.
|
|
for (int i = 0; i < pnm->cmsgCommands; i++) {
|
|
cmds_.push_back(pnm->amsgCommands[i]);
|
|
}
|
|
|
|
LOG() << base::Log::Format("0x%p ccmds:%d", endpoint, pnm->cmsgCommands);
|
|
|
|
// TODO: Validation
|
|
}
|
|
|
|
void Game::OnUpdateResult(Endpoint *endpoint, UpdateResultNetMessage *pnm) {
|
|
// If sync error, ignore
|
|
if (syncerror_) {
|
|
return;
|
|
}
|
|
|
|
// If not yet playing, this message means nothing
|
|
if (!playing_) {
|
|
RLOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "UpdateResult received while not in game";
|
|
return;
|
|
}
|
|
|
|
Player *pplr = playerMgr_.GetPlayer(endpoint);
|
|
if (pplr == NULL) {
|
|
LOG() << base::Log::Format("0x%p ", endpoint)
|
|
<< "Could not find player!";
|
|
return;
|
|
}
|
|
|
|
LOG() << "Received UpdateResult with cUpdatesBlock: " <<
|
|
pnm->ur.cUpdatesBlock << " cmsLatency: " << pnm->ur.cmsLatency;
|
|
|
|
LatencyRecord latr;
|
|
latr.cUpdatesBlock = pnm->ur.cUpdatesBlock;
|
|
latr.cmsLatency = pnm->ur.cmsLatency;
|
|
pplr->AddLatencyRecord(&latr);
|
|
|
|
// Track the UpdateResult, in order to compare state hashes. If there
|
|
// is an error, inform the clients and stop the game.
|
|
|
|
if (playerMgr_.CheckSyncError(pplr, pnm->ur, &cUpdatesSync_)) {
|
|
syncerror_ = true;
|
|
playerMgr_.BroadcastSyncError(pnm->ur);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Game::OnPlayerResign(Endpoint *endpoint, PlayerResignNetMessage *pnm) {
|
|
// When the player resigns, the player can choose to continue to watch
|
|
// the game, so the connection must stay and receives updates.
|
|
Player *pplr = playerMgr_.GetPlayer(endpoint);
|
|
if (pplr != NULL) {
|
|
// Change to computer player and tell clients about this, but
|
|
// keep the connection. This way this client will continue to
|
|
// receive game updates, which will allow "observing".
|
|
|
|
pplr->SetFlags(pplr->flags() | kfPlrComputer);
|
|
|
|
// If the player has sent win stats already, then they aren't
|
|
// resigning, they are leaving the game.
|
|
if (pplr->statslocked()) {
|
|
QueuePlayerDisconnect(pplr->pid(), knDisconnectReasonLeftGame);
|
|
} else {
|
|
QueuePlayerDisconnect(pplr->pid(), knDisconnectReasonResign);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::QueuePlayerDisconnect(Pid pid, int nReason) {
|
|
if (!playing_) {
|
|
// Players don't disconnect this way when not playing
|
|
return;
|
|
}
|
|
|
|
// Disconnects happen via a Message, so it is synched
|
|
// to the same update between clients via UpdateNetMessage.
|
|
wi::Message msg;
|
|
memset(&msg, 0, sizeof(msg));
|
|
msg.mid = kmidPlayerDisconnect;
|
|
msg.PlayerDisconnect.pid = pid;
|
|
msg.PlayerDisconnect.nReason = (word)nReason;
|
|
cmds_.push_back(msg);
|
|
}
|
|
|
|
int Game::GetNameCount() {
|
|
return playerMgr_.GetEndpointCount();
|
|
}
|
|
|
|
const PlayerName *Game::GetNames() {
|
|
return playerMgr_.GetEndpointNames();
|
|
}
|
|
|
|
std::vector<std::string> Game::GetIdsString(Endpoint *endpoint) {
|
|
std::vector<std::string> responses;
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
Player *pplr = playerMgr_.GetPlayer(*it);
|
|
if (pplr == NULL) {
|
|
continue;
|
|
}
|
|
if (endpoint->IsAdmin()) {
|
|
char ip[32];
|
|
(*it)->xpump().socket()->GetRemoteAddress().IPAsString(ip,
|
|
sizeof(ip));
|
|
responses.push_back(base::Format::ToString("%s: id %d ip %s os %s",
|
|
(*it)->name(), server_.GetChatterId(endpoint, *it), ip,
|
|
(*it)->platform()));
|
|
} else {
|
|
responses.push_back(base::Format::ToString("%s: id %d",
|
|
(*it)->name(), server_.GetChatterId(endpoint, *it)));
|
|
}
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
Player *Game::HumanPlayerFromColorChar(const char *psz) {
|
|
// 0: gray, 1: blue, 2: red, 3: yellow, 4: cyan
|
|
// Don't allow swapping gray because it is neutral.
|
|
|
|
if (strlen(psz) == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
Player *pplr = NULL;
|
|
const char *s_ch2side = "\bbryc";
|
|
Assert(strlen(s_ch2side) == kcSides);
|
|
for (int side = 0; s_ch2side[side] != 0; side++) {
|
|
if (s_ch2side[side] == psz[0]) {
|
|
pplr = playerMgr_.GetPlayer(side);
|
|
break;
|
|
}
|
|
}
|
|
if (pplr == NULL) {
|
|
return NULL;
|
|
}
|
|
if (pplr->endpoint() == NULL) {
|
|
return NULL;
|
|
}
|
|
if (pplr->flags() & (kfPlrComputer | kfPlrComputerOvermind |
|
|
kfPlrUnfulfilled | kfPlrObserver | kfPlrDisconnectBroadcasted)) {
|
|
return NULL;
|
|
}
|
|
if ((pplr->flags() & kfPlrInUse) == 0) {
|
|
return NULL;
|
|
}
|
|
return pplr;
|
|
}
|
|
|
|
const char *Game::ColorFromSide(int side) {
|
|
// 0: gray, 1: blue, 2: red, 3: yellow, 4: cyan
|
|
static const char *s_colors[] = {
|
|
"neutral", "blue", "red", "yellow", "cyan"
|
|
};
|
|
if (side < 0 || side >= ARRAYSIZE(s_colors)) {
|
|
return "unknown";
|
|
}
|
|
return s_colors[side];
|
|
}
|
|
|
|
const char *Game::ColorCharFromSide(int side) {
|
|
// 0: gray, 1: blue, 2: red, 3: yellow, 4: cyan
|
|
static const char *s_colors[] = {
|
|
"g", "b", "r", "y", "c"
|
|
};
|
|
if (side < 0 || side >= ARRAYSIZE(s_colors)) {
|
|
return "unknown";
|
|
}
|
|
return s_colors[side];
|
|
}
|
|
|
|
bool Game::IsSwapAllowed(Endpoint *endpoint) {
|
|
if (playing_) {
|
|
return false;
|
|
}
|
|
if (endpoint->id() != advertiserId_) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Game::SwapPlayersCommand(Endpoint *endpoint, const char *chat,
|
|
std::string *response) {
|
|
if (playing_) {
|
|
*response = "Can't change sides after the game has begun.";
|
|
return;
|
|
}
|
|
|
|
if (endpoint->id() != advertiserId_) {
|
|
*response = "Only the creator can issue this command.";
|
|
return;
|
|
}
|
|
|
|
bool error = false;
|
|
std::string arg_a;
|
|
if (!endpoint->GetArgument(chat, 1, &arg_a)) {
|
|
error = true;
|
|
}
|
|
std::string arg_b;
|
|
if (!endpoint->GetArgument(chat, 2, &arg_b)) {
|
|
error = true;
|
|
}
|
|
Player *playerA = HumanPlayerFromColorChar(arg_a.c_str());
|
|
Player *playerB = HumanPlayerFromColorChar(arg_b.c_str());
|
|
if (playerA == NULL || playerB == NULL) {
|
|
error = true;
|
|
}
|
|
if (playerA == playerB) {
|
|
error = true;
|
|
}
|
|
if (error) {
|
|
*response = "This command takes two arguments of either b, r, y, or c, referring to the first letter of the color of each player.";
|
|
return;
|
|
}
|
|
|
|
// Unset the ready state, so that players have to agree to the new
|
|
// arrangement, unless the player is the creator, who is already ready
|
|
// and literally doesn't have a ready button.
|
|
bool was_ready = false;
|
|
if ((playerA->flags() & (kfPlrCreator | kfPlrReady)) == kfPlrReady) {
|
|
was_ready = true;
|
|
playerA->SetFlags(playerA->flags() & ~kfPlrReady);
|
|
// HACK: this message is a hack-o-rama way to show the button again
|
|
NetMessage nmFail(knmidScBeginGameFail);
|
|
playerA->endpoint()->xpump().Send(
|
|
XMsgGameNetMessage::ToBuffer(&nmFail));
|
|
}
|
|
if ((playerB->flags() & (kfPlrCreator | kfPlrReady)) == kfPlrReady) {
|
|
was_ready = true;
|
|
playerB->SetFlags(playerB->flags() & ~kfPlrReady);
|
|
// HACK: this message is a hack-o-rama way to show the button again
|
|
NetMessage nmFail(knmidScBeginGameFail);
|
|
playerB->endpoint()->xpump().Send(
|
|
XMsgGameNetMessage::ToBuffer(&nmFail));
|
|
}
|
|
|
|
// Swap player state. This also broadcasts the changes.
|
|
playerMgr_.SwapPlayers(playerA, playerB);
|
|
|
|
// Use SendChat so that NULL can be sent for endpoint, to get gray text
|
|
if (!was_ready) {
|
|
SendChat(NULL, base::Format::ToString(
|
|
"Swapped colors. %s is %s, and %s is %s.",
|
|
playerA->name(), ColorFromSide(playerA->side()),
|
|
playerB->name(), ColorFromSide(playerB->side())), NULL);
|
|
} else {
|
|
// Make players press ready again, so the creator can't trick
|
|
// players into playing a certain color.
|
|
SendChat(NULL, base::Format::ToString(
|
|
"Swapped colors. %s is %s, and %s is %s. Players must press Ready again.",
|
|
playerA->name(), ColorFromSide(playerA->side()),
|
|
playerB->name(), ColorFromSide(playerB->side())), NULL);
|
|
}
|
|
}
|
|
|
|
bool Game::ProcessCommand(Endpoint *endpoint, const char *chat,
|
|
std::string *response, bool *broadcast) {
|
|
|
|
ModeratorCommand cmd = endpoint->GetModeratorCommand(chat);
|
|
if (cmd != kModeratorCommandNone) {
|
|
server_.logger().LogModCommand(endpoint, chat);
|
|
}
|
|
|
|
switch (endpoint->GetModeratorCommand(chat)) {
|
|
case kModeratorCommandRules:
|
|
*response = endpoint->server().GetChatRules();
|
|
*broadcast = true;
|
|
return true;
|
|
|
|
case kModeratorCommandSwap:
|
|
SwapPlayersCommand(endpoint, chat, response);
|
|
return true;
|
|
|
|
case kModeratorCommandNone:
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Game::SendChat(Endpoint *endpoint, const char *chat,
|
|
const char *unfiltered) {
|
|
bool broadcast = false;
|
|
std::string response;
|
|
if (endpoint != NULL && ProcessCommand(endpoint, chat, &response,
|
|
&broadcast)) {
|
|
if (!broadcast) {
|
|
if (response.size() != 0) {
|
|
endpoint->xpump().Send(XMsgGameReceiveChat::ToBuffer("",
|
|
response.c_str()));
|
|
server_.logger().LogSystemMsg(endpoint, response.c_str());
|
|
}
|
|
return;
|
|
}
|
|
chat = response.c_str();
|
|
}
|
|
|
|
std::string from = "";
|
|
if (endpoint != NULL) {
|
|
from = endpoint->GetChatName();
|
|
}
|
|
|
|
Room *room = server_.lobby().FindRoom(roomid_);
|
|
const char *pszRoomName = NULL;
|
|
bool private_room = false;
|
|
if (room != NULL) {
|
|
pszRoomName = room->name();
|
|
private_room = (room->password()[0] != 0);
|
|
}
|
|
|
|
if (unfiltered != NULL) {
|
|
server_.logger().LogGameChat(endpoint, roomid_, pszRoomName,
|
|
private_room, id(), info().title(), unfiltered);
|
|
} else {
|
|
server_.logger().LogGameChat(endpoint, roomid_, pszRoomName,
|
|
private_room, id(), info().title(), chat);
|
|
}
|
|
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
Player *pplr = playerMgr_.GetPlayer(*it);
|
|
if (pplr == NULL) {
|
|
continue;
|
|
}
|
|
if ((*it)->muted()) {
|
|
continue;
|
|
}
|
|
(*it)->xpump().Send(XMsgGameReceiveChat::ToBuffer(from.c_str(), chat));
|
|
if (unfiltered != NULL && (*it)->seechat()) {
|
|
(*it)->xpump().Send(XMsgGameReceiveChat::ToBuffer("", unfiltered));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::SendAdminChat(const char *name, const char *chat, bool mods_only) {
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
Player *pplr = playerMgr_.GetPlayer(*it);
|
|
if (pplr == NULL) {
|
|
continue;
|
|
}
|
|
if (mods_only && !(*it)->IsModerator()) {
|
|
continue;
|
|
}
|
|
(*it)->xpump().Send(XMsgGameReceiveChat::ToBuffer(name, chat));
|
|
}
|
|
}
|
|
|
|
bool Game::ToggleAnonBlock(Endpoint *endpoint) {
|
|
if (!IsAnonBlockAllowed(endpoint)) {
|
|
return false;
|
|
}
|
|
|
|
ff_ ^= kfGameAnonBlock;
|
|
if ((ff_ & kfGameAnonBlock) == 0) {
|
|
return true;
|
|
}
|
|
|
|
// Remove all endpoints that are anons
|
|
while (true) {
|
|
bool dropped = false;
|
|
ConnectedList::iterator it = connected_.begin();
|
|
for (; it != connected_.end(); it++) {
|
|
if ((*it)->anonymous()) {
|
|
(*it)->DropGame(this, knDisconnectReasonKilled);
|
|
dropped = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!dropped) {
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Game::IsAnonBlockAllowed(Endpoint *endpoint) {
|
|
if (endpoint->anonymous()) {
|
|
return false;
|
|
}
|
|
if (playing_) {
|
|
return false;
|
|
}
|
|
if (endpoint->id() != advertiserId_) {
|
|
return false;
|
|
}
|
|
|
|
// For now don't allow toggling. Only set once to prevent abuse
|
|
if ((ff_ & kfGameAnonBlock) != 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Game::SetAllies(SideMask side, SideMask sidmAllies) {
|
|
Player *player = HumanPlayerFromColorChar(ColorCharFromSide(side));
|
|
if (player != NULL)
|
|
player->SetAllies(sidmAllies);
|
|
}
|
|
|
|
Player *Game::HumanPlayerFromEndpoint(Endpoint *endpoint) {
|
|
Player *pplr = NULL;
|
|
for (int i = 0; i < kcPlayersMax; i++) {
|
|
pplr = playerMgr_.GetPlayer(i);
|
|
|
|
if (pplr == NULL)
|
|
continue;
|
|
|
|
if (pplr->endpoint() == endpoint) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pplr == NULL) {
|
|
return NULL;
|
|
}
|
|
if (pplr->endpoint() == NULL) {
|
|
return NULL;
|
|
}
|
|
if (pplr->flags() & (kfPlrComputer | kfPlrComputerOvermind |
|
|
kfPlrUnfulfilled | kfPlrObserver | kfPlrDisconnectBroadcasted)) {
|
|
return NULL;
|
|
}
|
|
if ((pplr->flags() & kfPlrInUse) == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
return pplr;
|
|
}
|
|
|
|
void Game::SendTeamChat(Endpoint *endpoint, const char *chat, const char *unfiltered) {
|
|
if (endpoint == NULL) {
|
|
return;
|
|
}
|
|
if (!CanSendTeamChat(endpoint, true)) {
|
|
return;
|
|
}
|
|
|
|
// This won't include observing players
|
|
Player *pendplr = HumanPlayerFromEndpoint(endpoint);
|
|
if (pendplr == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Player name
|
|
const char *name = base::Format::ToString("%s to team", endpoint->GetChatName().c_str());
|
|
|
|
// Players
|
|
Player *playerBlue, *playerRed, *playerYellow, *playerCyan;
|
|
std::vector<Player *> players;
|
|
players.push_back(playerBlue);
|
|
players.push_back(playerRed);
|
|
players.push_back(playerYellow);
|
|
players.push_back(playerCyan);
|
|
|
|
// The endpoint sending the chat sees it regardless of the value in pendplr->allies()
|
|
endpoint->xpump().Send(XMsgGameReceiveChat::ToBuffer(name, chat));
|
|
|
|
// If this endpoint is a mod, send unfiltered chat too
|
|
if (unfiltered != NULL && endpoint->seechat()) {
|
|
endpoint->xpump().Send(XMsgGameReceiveChat::ToBuffer("", unfiltered));
|
|
}
|
|
|
|
SideMask sides[kcPlayersMax] = {ksideNeutral, ksidmSide1, ksidmSide2, ksidmSide3, ksidmSide4};
|
|
|
|
// Loop through the four human sides
|
|
for (int i = 1; i < kcPlayersMax; i++) {
|
|
|
|
// Is this an ally?
|
|
if (pendplr->allies() & sides[i]) {
|
|
|
|
// Skip if this ally is the sending player
|
|
if (pendplr->side() == i)
|
|
continue;
|
|
|
|
// This won't include observing players
|
|
players[i] = HumanPlayerFromColorChar(ColorCharFromSide(i));
|
|
if (players[i] == NULL)
|
|
continue;
|
|
|
|
// Is this ally muted?
|
|
if (players[i]->endpoint()->muted())
|
|
continue;
|
|
|
|
// Send the message to the ally
|
|
players[i]->endpoint()->xpump().Send(
|
|
XMsgGameReceiveChat::ToBuffer(name, chat));
|
|
|
|
// Send the unfiltered version if the ally is a mod
|
|
if (unfiltered != NULL && players[i]->endpoint()->seechat()) {
|
|
players[i]->endpoint()->xpump().Send(
|
|
XMsgGameReceiveChat::ToBuffer("", unfiltered));
|
|
}
|
|
|
|
// Log the chat
|
|
Room *room = server_.lobby().FindRoom(roomid_);
|
|
const char *pszRoomName = NULL;
|
|
bool private_room = false;
|
|
if (room != NULL) {
|
|
pszRoomName = room->name();
|
|
private_room = (room->password()[0] != 0);
|
|
}
|
|
if (unfiltered != NULL) {
|
|
server_.logger().LogGameChat(endpoint, roomid_, pszRoomName,
|
|
private_room, id(), info().title(), unfiltered);
|
|
} else {
|
|
server_.logger().LogGameChat(endpoint, roomid_, pszRoomName,
|
|
private_room, id(), info().title(), chat);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Game::CanSendTeamChat(Endpoint *endpoint, bool broadcast) {
|
|
if (endpoint == NULL)
|
|
return false;
|
|
|
|
// Game not started yet
|
|
if (!playing()) {
|
|
if (broadcast) {
|
|
endpoint->xpump().Send(XMsgGameReceiveChat::ToBuffer("",
|
|
"You cannot use team chat until the game starts."));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Player *pplr = HumanPlayerFromEndpoint(endpoint);
|
|
if (pplr == NULL) {
|
|
|
|
// If pplr is NULL, assume it's because the player is observing
|
|
if (broadcast) {
|
|
endpoint->xpump().Send(XMsgGameReceiveChat::ToBuffer("",
|
|
"You cannot use team chat while in observe mode."));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace wi
|