1
0
forked from 0ad/0ad

Split TurnManager classes into individual files per class. Patch by echotangoecho, fixes #4095.

Remove the "Net" prefix from the non-networked classes.
Use variadic macros and mark the client turnmanager as NONCOPYABLE.

Differential Revision: D16
Reviewed By: leper
This was SVN commit r19165.
This commit is contained in:
elexis 2017-01-24 02:04:50 +00:00
parent 43c7551202
commit 4f01db4831
19 changed files with 1111 additions and 921 deletions

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -37,8 +37,8 @@
#include "lobby/scripting/JSInterface_Lobby.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
#include "network/NetTurnManager.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Errors.h"
@ -70,6 +70,7 @@
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/helpers/Selection.h"
#include "simulation2/system/TurnManager.h"
#include "soundmanager/SoundManager.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "tools/atlas/GameInterface/GameLoop.h"

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -19,9 +19,9 @@
#include "NetClient.h"
#include "NetClientTurnManager.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetTurnManager.h"
#include "lib/byte_order.h"
#include "lib/sysdep/sysdep.h"

View File

@ -0,0 +1,139 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "NetClientTurnManager.h"
#include "NetClient.h"
#include "gui/GUIManager.h"
#include "ps/CLogger.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "simulation2/Simulation2.h"
#if 0
#define NETCLIENTTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETCLIENTTURN_LOG(...)
#endif
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay)
: CTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
{
}
void CNetClientTurnManager::PostCommand(JS::HandleValue data)
{
NETCLIENTTURN_LOG("PostCommand()\n");
// Transmit command to server
CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data);
m_NetClient.SendMessage(&msg);
// Add to our local queue
//AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY);
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
CEndCommandBatchMessage msg;
msg.m_Turn = turn;
// The turn-length field of the CEndCommandBatchMessage is currently only relevant
// when sending it from the server to the clients.
// It could be used to verify that the client simulated the correct turn length.
msg.m_TurnLength = 0;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
}
NETCLIENTTURN_LOG("NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str());
m_Replay.Hash(hash, quick);
// Don't send the hash if OOS
if (m_HasSyncError)
return;
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::OnDestroyConnection()
{
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
}
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
{
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str());
// Only complain the first time
if (m_HasSyncError)
return;
m_HasSyncError = true;
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, !TurnNeedsFullHash(turn)));
OsPath path = psLogDir() / "oos_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
hash = Hexify(hash);
std::stringstream msg;
msg << "Out of sync on turn " << turn;
for (size_t i = 0; i < playerNames.size(); ++i)
msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring(playerNames[i].m_Name);
msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") << " the hosts game state.";
msg << "\n\n" << "Dumping current state to " << CStr(path.string8()).EscapeToPrintableASCII();
LOGERROR("%s", msg.str());
if (g_GUI)
g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
}

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_NETCLIENTTURNMANAGER
#define INCLUDED_NETCLIENTTURNMANAGER
#include "simulation2/system/TurnManager.h"
#include "NetMessage.h"
class CNetClient;
/**
* Implementation of CTurnManager for network clients.
*/
class CNetClientTurnManager : public CTurnManager
{
NONCOPYABLE(CNetClientTurnManager);
public:
CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay);
void OnSimulationMessage(CSimulationMessage* msg) override;
void PostCommand(JS::HandleValue data) override;
/**
* Notify the server that all commands are sent to prepare the connection for termination.
*/
void OnDestroyConnection();
void OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
private:
void NotifyFinishedOwnCommands(u32 turn) override;
void NotifyFinishedUpdate(u32 turn) override;
CNetClient& m_NetClient;
};
#endif // INCLUDED_NETCLIENTTURNMANAGER

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -22,8 +22,8 @@
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetServerTurnManager.h"
#include "NetStats.h"
#include "NetTurnManager.h"
#include "lib/external_libraries/enet.h"
#include "ps/CLogger.h"
@ -32,6 +32,7 @@
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include <miniupnpc/miniwget.h>

View File

@ -0,0 +1,173 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "NetMessage.h"
#include "NetServerTurnManager.h"
#include "NetServer.h"
#include "simulation2/system/TurnManager.h"
#if 0
#define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETSERVERTURN_LOG(...)
#endif
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
: m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false)
{
// The first turn we will actually execute is number 2,
// so store dummy values into the saved lengths list
m_SavedTurnLengths.push_back(0);
m_SavedTurnLengths.push_back(0);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
{
NETSERVERTURN_LOG("NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn);
// Must be a client we've already heard of
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsReady[client] + 1);
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
// See if all clients (including self) are ready for a new turn
for (const std::pair<int, u32>& clientReady : m_ClientsReady)
{
NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
++m_ReadyTurn;
NETSERVERTURN_LOG("CheckClientsReady: ready for turn %d\n", m_ReadyTurn);
// Tell all clients that the next turn is ready
CEndCommandBatchMessage msg;
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg);
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, const CStrW& playername, u32 turn, const CStr& hash)
{
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsSimulated[client] + 1);
m_ClientsSimulated[client] = turn;
// Check for OOS only if in sync
if (m_HasSyncError)
return;
m_ClientPlayernames[client] = playername;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits<u32>::max();
for (const std::pair<int, u32>& clientSimulated : m_ClientsSimulated)
if (clientSimulated.second < newest)
newest = clientSimulated.second;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair<u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = clientStateHash.second.begin()->second;
// Find all players that are OOS on that turn
std::vector<CStrW> OOSPlayerNames;
for (const std::pair<int, std::string>& hashPair : clientStateHash.second)
{
NETSERVERTURN_LOG("sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str());
if (hashPair.second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = clientStateHash.first;
msg.m_HashExpected = expected;
for (const CStrW& playername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
h.m_Name = playername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg);
break;
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client);
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
CheckClientsReady();
}
void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}

View File

@ -0,0 +1,99 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_NETSERVERTURNMANAGER
#define INCLUDED_NETSERVERTURNMANAGER
#include <map>
#include "ps/CStr.h"
class CNetServerWorker;
/**
* The server-side counterpart to CNetClientTurnManager.
* Records the turn state of each client, and sends turn advancement messages
* when all clients are ready.
*
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerTurnManager
{
NONCOPYABLE(CNetServerTurnManager);
public:
CNetServerTurnManager(CNetServerWorker& server);
void NotifyFinishedClientCommands(int client, u32 turn);
void NotifyFinishedClientUpdate(int client, const CStrW& playername, u32 turn, const CStr& hash);
/**
* Inform the turn manager of a new client who will be sending commands.
*/
void InitialiseClient(int client, u32 turn);
/**
* Inform the turn manager that a previously-initialised client has left the game
* and will no longer be sending commands.
*/
void UninitialiseClient(int client);
void SetTurnLength(u32 msecs);
/**
* Returns the latest turn for which all clients are ready;
* they will have already been told to execute this turn.
*/
u32 GetReadyTurn() { return m_ReadyTurn; }
/**
* Returns the turn length that was used for the given turn.
* Requires turn <= GetReadyTurn().
*/
u32 GetSavedTurnLength(u32 turn);
private:
void CheckClientsReady();
/// The latest turn for which we have received all commands from all clients
u32 m_ReadyTurn;
// Client ID -> ready turn number (the latest turn for which all commands have been received from that client)
std::map<int, u32> m_ClientsReady;
// Client ID -> last known simulated turn number (for which we have the state hash)
// (the client has reached the start of this turn, not done the update for it yet)
std::map<int, u32> m_ClientsSimulated;
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
std::map<u32, std::map<int, std::string>> m_ClientStateHashes;
// Map of client ID -> playername
std::map<u32, CStrW> m_ClientPlayernames;
// Current turn length
u32 m_TurnLength;
// Turn lengths for all previously executed turns
std::vector<u32> m_SavedTurnLengths;
CNetServerWorker& m_NetServer;
bool m_HasSyncError;
};
#endif // INCLUDED_NETSERVERTURNMANAGER

View File

@ -1,719 +0,0 @@
/* Copyright (C) 2016 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "NetTurnManager.h"
#include "NetMessage.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/SavedGame.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include <fstream>
const u32 DEFAULT_TURN_LENGTH_MP = 500;
const u32 DEFAULT_TURN_LENGTH_SP = 200;
static const int COMMAND_DELAY = 2;
#if 0
#define NETTURN_LOG(args) debug_printf args
#else
#define NETTURN_LOG(args)
#endif
CNetTurnManager::CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay) :
m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength), m_DeltaSimTime(0),
m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay),
m_TimeWarpNumTurns(0), m_FinalTurn(std::numeric_limits<u32>::max())
{
// When we are on turn n, we schedule new commands for n+2.
// We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
// We know we have not yet finished scheduling commands for n+2.
// Hence other clients can be on turn n-1, n, n+1, and no other.
// So they can be sending us commands scheduled for n+1, n+2, n+3.
// So we need a 3-element buffer:
m_QueuedCommands.resize(COMMAND_DELAY + 1);
}
void CNetTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaSimTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CNetTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
}
bool CNetTurnManager::WillUpdate(float simFrameLength)
{
// Keep this in sync with the return value of Update()
if (m_CurrentTurn > m_FinalTurn)
return false;
if (m_DeltaSimTime + simFrameLength < 0)
return false;
if (m_ReadyTurn <= m_CurrentTurn)
return false;
return true;
}
bool CNetTurnManager::Update(float simFrameLength, size_t maxTurns)
{
if (m_CurrentTurn > m_FinalTurn)
return false;
m_DeltaSimTime += simFrameLength;
// If the game becomes laggy, m_DeltaSimTime increases progressively.
// The engine will fast forward accordingly to catch up.
// To keep the game playable, stop fast forwarding after 2 turn lengths.
m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);
// If we haven't reached the next turn yet, do nothing
if (m_DeltaSimTime < 0)
return false;
NETTURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
{
// Oops, we wanted to start the next turn but it's not ready yet -
// there must be too much network lag.
// TODO: complain to the user.
// TODO: send feedback to the server to increase the turn length.
// Reset the next-turn timer to 0 so we try again next update but
// so we don't rush to catch up in subsequent turns.
// TODO: we should do clever rate adjustment instead of just pausing like this.
m_DeltaSimTime = 0;
return false;
}
maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn
for (size_t i = 0; i < maxTurns; ++i)
{
// Check that we've reached the i'th next turn
if (m_DeltaSimTime < 0)
break;
// Check that the i'th next turn is still ready
if (m_ReadyTurn <= m_CurrentTurn)
break;
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
// Increase now, so Update can send new commands for a subsequent turn
++m_CurrentTurn;
// Clean up any destroyed entities since the last turn (e.g. placement previews
// or rally point flags generated by the GUI). (Must do this before the time warp
// serialization.)
m_Simulation2.FlushDestroyedEntities();
// Save the current state for rewinding, if enabled
if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
{
PROFILE3("time warp serialization");
std::stringstream stream;
m_Simulation2.SerializeState(stream);
m_TimeWarpStates.push_back(stream.str());
}
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG((L"Running %d cmds\n", commands.size()));
m_Simulation2.Update(m_TurnLength, commands);
NotifyFinishedUpdate(m_CurrentTurn);
// Set the time for the next turn update
m_DeltaSimTime -= m_TurnLength / 1000.f;
}
return true;
}
bool CNetTurnManager::UpdateFastForward()
{
m_DeltaSimTime = 0;
NETTURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
++m_CurrentTurn;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG((L"Running %d cmds\n", commands.size()));
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CNetTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)
{
NETTURN_LOG((L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str()));
// Only complain the first time
if (m_HasSyncError)
return;
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
OsPath path = psLogDir() / "oos_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
hash = Hexify(hash);
const std::string& expectedHashHex = Hexify(expectedHash);
DisplayOOSError(turn, hash, expectedHashHex, false, &playerNames, &path);
}
void CNetTurnManager::DisplayOOSError(u32 turn, const CStr& hash, const CStr& expectedHash, bool isReplay, std::vector<CSyncErrorMessage::S_m_PlayerNames>* playerNames = NULL, OsPath* path = NULL)
{
m_HasSyncError = true;
std::stringstream msg;
msg << "Out of sync on turn " << turn;
if (playerNames)
for (size_t i = 0; i < playerNames->size(); ++i)
msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring((*playerNames)[i].m_Name);
if (isReplay)
msg << "\n\n" << "The current game state is different from the original game state.";
else
msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") << " the hosts game state.";
if (path)
msg << "\n\n" << "Dumping current state to " << CStr(path->string8()).EscapeToPrintableASCII();
LOGERROR("%s", msg.str());
if (g_GUI)
g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
}
void CNetTurnManager::Interpolate(float simFrameLength, float realFrameLength)
{
// TODO: using m_TurnLength might be a bit dodgy when length changes - maybe
// we need to save the previous turn length?
float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);
// Stop animations while still updating the selection highlight
if (m_CurrentTurn > m_FinalTurn)
simFrameLength = 0;
m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);
}
void CNetTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)
{
NETTURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn));
if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))
{
debug_warn(L"Received command for invalid turn");
return;
}
m_Simulation2.GetScriptInterface().FreezeObject(data, true);
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, cx, data);
}
void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
{
NETTURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength));
ENSURE(turn == m_ReadyTurn + 1);
m_ReadyTurn = turn;
m_TurnLength = turnLength;
}
bool CNetTurnManager::TurnNeedsFullHash(u32 turn)
{
// Check immediately for errors caused by e.g. inconsistent game versions
// (The hash is computed after the first sim update, so we start at turn == 1)
if (turn == 1)
return true;
// Otherwise check the full state every ~10 seconds in multiplayer games
// (TODO: should probably remove this when we're reasonably sure the game
// isn't too buggy, since the full hash is still pretty slow)
if (turn % 20 == 0)
return true;
return false;
}
void CNetTurnManager::EnableTimeWarpRecording(size_t numTurns)
{
m_TimeWarpStates.clear();
m_TimeWarpNumTurns = numTurns;
}
void CNetTurnManager::RewindTimeWarp()
{
if (m_TimeWarpStates.empty())
return;
std::stringstream stream(m_TimeWarpStates.back());
m_Simulation2.DeserializeState(stream);
m_TimeWarpStates.pop_back();
// Reset the turn manager state, so we won't execute stray commands and
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
ResetState(0, 1);
}
void CNetTurnManager::QuickSave()
{
TIMER(L"QuickSave");
std::stringstream stream;
if (!m_Simulation2.SerializeState(stream))
{
LOGERROR("Failed to quicksave game");
return;
}
m_QuickSaveState = stream.str();
if (g_GUI)
m_QuickSaveMetadata = g_GUI->GetSavedGameData();
else
m_QuickSaveMetadata = std::string();
LOGMESSAGERENDER("Quicksaved game");
}
void CNetTurnManager::QuickLoad()
{
TIMER(L"QuickLoad");
if (m_QuickSaveState.empty())
{
LOGERROR("Cannot quickload game - no game was quicksaved");
return;
}
std::stringstream stream(m_QuickSaveState);
if (!m_Simulation2.DeserializeState(stream))
{
LOGERROR("Failed to quickload game");
return;
}
if (g_GUI && !m_QuickSaveMetadata.empty())
g_GUI->RestoreSavedGameData(m_QuickSaveMetadata);
LOGMESSAGERENDER("Quickloaded game");
// See RewindTimeWarp
ResetState(0, 1);
}
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) :
CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
{
}
void CNetClientTurnManager::PostCommand(JS::HandleValue data)
{
NETTURN_LOG((L"PostCommand()\n"));
// Transmit command to server
CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data);
m_NetClient.SendMessage(&msg);
// Add to our local queue
//AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY);
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
NETTURN_LOG((L"NotifyFinishedOwnCommands(%d)\n", turn));
CEndCommandBatchMessage msg;
msg.m_Turn = turn;
// The turn-length field of the CEndCommandBatchMessage is currently only relevant
// when sending it from the server to the clients.
// It could be used to verify that the client simulated the correct turn length.
msg.m_TurnLength = 0;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
}
NETTURN_LOG((L"NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str()));
m_Replay.Hash(hash, quick);
// Don't send the hash if OOS
if (m_HasSyncError)
return;
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::OnDestroyConnection()
{
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
}
CNetLocalTurnManager::CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) :
CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay)
{
}
void CNetLocalTurnManager::PostCommand(JS::HandleValue data)
{
// Add directly to the next turn, ignoring COMMAND_DELAY,
// because we don't need to compensate for network latency
AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1);
}
void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
FinishedAllCommands(turn, m_TurnLength);
}
void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn))
{
#if 0 // this hurts performance and is only useful for verifying log replays
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash));
}
m_Replay.Hash(hash);
#endif
}
void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
{
debug_warn(L"This should never be called");
}
CNetReplayTurnManager::CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay) :
CNetLocalTurnManager(simulation, replay)
{
}
void CNetReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command)
{
// Using the pair we make sure that commands per turn will be processed in the correct order
m_ReplayCommands[turn].emplace_back(player, command);
}
void CNetReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick)
{
m_ReplayHash[turn] = std::make_pair(hash, quick);
}
void CNetReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength)
{
m_ReplayTurnLengths[turn] = turnLength;
// Initialize turn length
if (turn == 0)
m_TurnLength = m_ReplayTurnLengths[0];
}
void CNetReplayTurnManager::StoreFinalReplayTurn(u32 turn)
{
m_FinalTurn = turn;
}
void CNetReplayTurnManager::NotifyFinishedUpdate(u32 turn)
{
if (turn == 1 && m_FinalTurn == 0)
g_GUI->SendEventToAll("ReplayFinished");
if (turn > m_FinalTurn)
return;
DoTurn(turn);
// Compare hash if it exists in the replay and if we didn't have an OOS already
if (m_HasSyncError || m_ReplayHash.find(turn) == m_ReplayHash.end())
return;
std::string expectedHash = m_ReplayHash[turn].first;
bool quickHash = m_ReplayHash[turn].second;
// Compute hash
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash));
hash = Hexify(hash);
if (hash != expectedHash)
DisplayOOSError(turn, hash, expectedHash, true);
}
void CNetReplayTurnManager::DoTurn(u32 turn)
{
debug_printf("Executing turn %u of %u\n", turn, m_FinalTurn);
m_TurnLength = m_ReplayTurnLengths[turn];
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// Simulate commands for that turn
for (const std::pair<player_id_t, std::string>& p : m_ReplayCommands[turn])
{
JS::RootedValue command(cx);
m_Simulation2.GetScriptInterface().ParseJSON(p.second, &command);
AddCommand(m_ClientId, p.first, command, m_CurrentTurn + 1);
}
if (turn == m_FinalTurn)
g_GUI->SendEventToAll("ReplayFinished");
}
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) :
m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false)
{
// The first turn we will actually execute is number 2,
// so store dummy values into the saved lengths list
m_SavedTurnLengths.push_back(0);
m_SavedTurnLengths.push_back(0);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
{
NETTURN_LOG((L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn));
// Must be a client we've already heard of
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsReady[client] + 1);
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
// See if all clients (including self) are ready for a new turn
for (const std::pair<int, u32>& clientReady : m_ClientsReady)
{
NETTURN_LOG((L" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn));
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
++m_ReadyTurn;
NETTURN_LOG((L"CheckClientsReady: ready for turn %d\n", m_ReadyTurn));
// Tell all clients that the next turn is ready
CEndCommandBatchMessage msg;
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg);
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, const CStrW& playername, u32 turn, const CStr& hash)
{
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsSimulated[client] + 1);
m_ClientsSimulated[client] = turn;
// Check for OOS only if in sync
if (m_HasSyncError)
return;
m_ClientPlayernames[client] = playername;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits<u32>::max();
for (const std::pair<int, u32>& clientSimulated : m_ClientsSimulated)
if (clientSimulated.second < newest)
newest = clientSimulated.second;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair<u32, std::map<int, std::string>>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = clientStateHash.second.begin()->second;
// Find all players that are OOS on that turn
std::vector<CStrW> OOSPlayerNames;
for (const std::pair<int, std::string>& hashPair : clientStateHash.second)
{
NETTURN_LOG((L"sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str()));
if (hashPair.second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = clientStateHash.first;
msg.m_HashExpected = expected;
for (const CStrW& playername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
h.m_Name = playername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg);
break;
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETTURN_LOG((L"InitialiseClient(client=%d, turn=%d)\n", client, turn));
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETTURN_LOG((L"UninitialiseClient(client=%d)\n", client));
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
CheckClientsReady();
}
void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -23,7 +23,6 @@
#include "lib/tex/tex.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetTurnManager.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CLogger.h"
@ -33,6 +32,7 @@
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
class TestNetComms : public CxxTest::TestSuite
{
@ -310,7 +310,7 @@ public:
wait(clients, 100);
// (This SetTurnLength thing doesn't actually detect errors unless you change
// CNetTurnManager::TurnNeedsFullHash to always return true)
// CTurnManager::TurnNeedsFullHash to always return true)
{
JS::RootedValue cmd(cx);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -29,7 +29,6 @@
#include "lib/timer.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetTurnManager.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
@ -47,6 +46,7 @@
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/system/ReplayTurnManager.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
@ -87,7 +87,7 @@ CGame::CGame(bool disableGraphics, bool replayLog):
if (m_GameView)
m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager());
m_TurnManager = new CNetLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client
m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client
m_Simulation2->LoadDefaultScripts();
}
@ -110,7 +110,7 @@ CGame::~CGame()
delete m_ReplayStream;
}
void CGame::SetTurnManager(CNetTurnManager* turnManager)
void CGame::SetTurnManager(CTurnManager* turnManager)
{
if (m_TurnManager)
delete m_TurnManager;
@ -127,7 +127,7 @@ int CGame::LoadVisualReplayData()
ENSURE(!m_ReplayPath.empty());
ENSURE(m_ReplayStream);
CNetReplayTurnManager* replayTurnMgr = static_cast<CNetReplayTurnManager*>(GetTurnManager());
CReplayTurnManager* replayTurnMgr = static_cast<CReplayTurnManager*>(GetTurnManager());
u32 currentTurn = 0;
std::string type;
@ -175,7 +175,7 @@ bool CGame::StartVisualReplay(const std::string& replayPath)
m_IsVisualReplay = true;
ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface();
SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger()));
SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger()));
m_ReplayPath = replayPath;
m_ReplayStream = new std::ifstream(m_ReplayPath.c_str());

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -27,7 +27,7 @@
class CWorld;
class CSimulation2;
class CGameView;
class CNetTurnManager;
class CTurnManager;
class IReplayLogger;
struct CColor;
@ -77,7 +77,7 @@ class CGame
*/
player_id_t m_ViewedPlayerID;
CNetTurnManager* m_TurnManager;
CTurnManager* m_TurnManager;
public:
CGame(bool disableGraphics = false, bool replayLog = true);
@ -185,9 +185,9 @@ public:
* Replace the current turn manager.
* This class will take ownership of the pointer.
*/
void SetTurnManager(CNetTurnManager* turnManager);
void SetTurnManager(CTurnManager* turnManager);
CNetTurnManager* GetTurnManager() const
CTurnManager* GetTurnManager() const
{ return m_TurnManager; }
IReplayLogger& GetReplayLogger() const

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -18,6 +18,7 @@
#ifndef PS_UTIL_H
#define PS_UTIL_H
#include "lib/os_path.h"
#include "lib/file/vfs/vfs_path.h"
struct Tex;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -23,7 +23,7 @@
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "network/NetTurnManager.h"
#include "simulation2/system/TurnManager.h"
class CCmpCommandQueue : public ICmpCommandQueue
{

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "LocalTurnManager.h"
CLocalTurnManager::CLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay)
: CTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay)
{
}
void CLocalTurnManager::PostCommand(JS::HandleValue data)
{
// Add directly to the next turn, ignoring COMMAND_DELAY,
// because we don't need to compensate for network latency
AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1);
}
void CLocalTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
FinishedAllCommands(turn, m_TurnLength);
}
void CLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn))
{
#if 0 // this hurts performance and is only useful for verifying log replays
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash));
}
m_Replay.Hash(hash);
#endif
}
void CLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
{
debug_warn(L"This should never be called");
}

View File

@ -0,0 +1,41 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_LOCALTURNMANAGER
#define INCLUDED_LOCALTURNMANAGER
#include "TurnManager.h"
/**
* Implementation of CTurnManager for offline games.
*/
class CLocalTurnManager : public CTurnManager
{
public:
CLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay);
void OnSimulationMessage(CSimulationMessage* msg) override;
void PostCommand(JS::HandleValue data) override;
protected:
void NotifyFinishedOwnCommands(u32 turn) override;
virtual void NotifyFinishedUpdate(u32 turn) override;
};
#endif // INCLUDED_LOCALTURNMANAGER

View File

@ -0,0 +1,115 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ReplayTurnManager.h"
#include "gui/GUIManager.h"
#include "ps/Util.h"
#include "simulation2/Simulation2.h"
CReplayTurnManager::CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay)
: CLocalTurnManager(simulation, replay)
{
}
void CReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command)
{
// Using the pair we make sure that commands per turn will be processed in the correct order
m_ReplayCommands[turn].emplace_back(player, command);
}
void CReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick)
{
m_ReplayHash[turn] = std::make_pair(hash, quick);
}
void CReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength)
{
m_ReplayTurnLengths[turn] = turnLength;
// Initialize turn length
if (turn == 0)
m_TurnLength = m_ReplayTurnLengths[0];
}
void CReplayTurnManager::StoreFinalReplayTurn(u32 turn)
{
m_FinalTurn = turn;
}
void CReplayTurnManager::NotifyFinishedUpdate(u32 turn)
{
if (turn == 1 && m_FinalTurn == 0)
g_GUI->SendEventToAll("ReplayFinished");
if (turn > m_FinalTurn)
return;
DoTurn(turn);
// Compare hash if it exists in the replay and if we didn't have an OOS already
std::map<u32, std::pair<std::string, bool>>::iterator turnHashIt = m_ReplayHash.find(turn);
if (m_HasSyncError || turnHashIt == m_ReplayHash.end())
return;
std::string expectedHash = turnHashIt->second.first;
bool quickHash = turnHashIt->second.second;
// Compute hash
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash));
hash = Hexify(hash);
if (hash != expectedHash)
OnSyncError(turn);
}
void CReplayTurnManager::DoTurn(u32 turn)
{
debug_printf("Executing turn %u of %u\n", turn, m_FinalTurn);
m_TurnLength = m_ReplayTurnLengths[turn];
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// Simulate commands for that turn
for (const std::pair<player_id_t, std::string>& p : m_ReplayCommands[turn])
{
JS::RootedValue command(cx);
m_Simulation2.GetScriptInterface().ParseJSON(p.second, &command);
AddCommand(m_ClientId, p.first, command, m_CurrentTurn + 1);
}
if (turn == m_FinalTurn)
g_GUI->SendEventToAll("ReplayFinished");
}
void CReplayTurnManager::OnSyncError(u32 turn)
{
m_HasSyncError = true;
std::stringstream msg;
msg << "Out of sync on turn " << turn << "\n\n" << "The current game state is different from the original game state.";
LOGERROR("%s", msg.str());
if (g_GUI)
g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_REPLAYTURNMANAGER
#define INCLUDED_REPLAYTURNMANAGER
#include "LocalTurnManager.h"
/**
* Implementation of CLocalTurnManager for replay games.
*/
class CReplayTurnManager : public CLocalTurnManager
{
public:
CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay);
void StoreReplayCommand(u32 turn, int player, const std::string& command);
void StoreReplayTurnLength(u32 turn, u32 turnLength);
void StoreReplayHash(u32 turn, const std::string& hash, bool quick);
void StoreFinalReplayTurn(u32 turn);
private:
void NotifyFinishedUpdate(u32 turn) override;
void DoTurn(u32 turn);
void OnSyncError(u32 turn);
// Contains the commands of every player on each turn
std::map<u32, std::vector<std::pair<player_id_t, std::string>>> m_ReplayCommands;
// Contains the length of every turn
std::map<u32, u32> m_ReplayTurnLengths;
// Contains all replay hash values and weather or not the quick hash method was used
std::map<u32, std::pair<std::string, bool>> m_ReplayHash;
};
#endif // INCLUDED_REPLAYTURNMANAGER

View File

@ -0,0 +1,336 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "TurnManager.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
const u32 DEFAULT_TURN_LENGTH_MP = 500;
const u32 DEFAULT_TURN_LENGTH_SP = 200;
const int COMMAND_DELAY = 2;
#if 0
#define NETTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETTURN_LOG(...)
#endif
CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay)
: m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength),
m_PlayerId(-1), m_ClientId(clientId), m_DeltaSimTime(0), m_HasSyncError(false), m_Replay(replay),
m_FinalTurn(std::numeric_limits<u32>::max()), m_TimeWarpNumTurns(0)
{
// When we are on turn n, we schedule new commands for n+2.
// We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
// We know we have not yet finished scheduling commands for n+2.
// Hence other clients can be on turn n-1, n, n+1, and no other.
// So they can be sending us commands scheduled for n+1, n+2, n+3.
// So we need a 3-element buffer:
m_QueuedCommands.resize(COMMAND_DELAY + 1);
}
void CTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaSimTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
}
bool CTurnManager::WillUpdate(float simFrameLength) const
{
// Keep this in sync with the return value of Update()
if (m_CurrentTurn > m_FinalTurn)
return false;
if (m_DeltaSimTime + simFrameLength < 0)
return false;
if (m_ReadyTurn <= m_CurrentTurn)
return false;
return true;
}
bool CTurnManager::Update(float simFrameLength, size_t maxTurns)
{
if (m_CurrentTurn > m_FinalTurn)
return false;
m_DeltaSimTime += simFrameLength;
// If the game becomes laggy, m_DeltaSimTime increases progressively.
// The engine will fast forward accordingly to catch up.
// To keep the game playable, stop fast forwarding after 2 turn lengths.
m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);
// If we haven't reached the next turn yet, do nothing
if (m_DeltaSimTime < 0)
return false;
NETTURN_LOG("Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
{
// Oops, we wanted to start the next turn but it's not ready yet -
// there must be too much network lag.
// TODO: complain to the user.
// TODO: send feedback to the server to increase the turn length.
// Reset the next-turn timer to 0 so we try again next update but
// so we don't rush to catch up in subsequent turns.
// TODO: we should do clever rate adjustment instead of just pausing like this.
m_DeltaSimTime = 0;
return false;
}
maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn
for (size_t i = 0; i < maxTurns; ++i)
{
// Check that we've reached the i'th next turn
if (m_DeltaSimTime < 0)
break;
// Check that the i'th next turn is still ready
if (m_ReadyTurn <= m_CurrentTurn)
break;
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
// Increase now, so Update can send new commands for a subsequent turn
++m_CurrentTurn;
// Clean up any destroyed entities since the last turn (e.g. placement previews
// or rally point flags generated by the GUI). (Must do this before the time warp
// serialization.)
m_Simulation2.FlushDestroyedEntities();
// Save the current state for rewinding, if enabled
if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
{
PROFILE3("time warp serialization");
std::stringstream stream;
m_Simulation2.SerializeState(stream);
m_TimeWarpStates.push_back(stream.str());
}
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
NotifyFinishedUpdate(m_CurrentTurn);
// Set the time for the next turn update
m_DeltaSimTime -= m_TurnLength / 1000.f;
}
return true;
}
bool CTurnManager::UpdateFastForward()
{
m_DeltaSimTime = 0;
NETTURN_LOG("UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
++m_CurrentTurn;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CTurnManager::Interpolate(float simFrameLength, float realFrameLength)
{
// TODO: using m_TurnLength might be a bit dodgy when length changes - maybe
// we need to save the previous turn length?
float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);
// Stop animations while still updating the selection highlight
if (m_CurrentTurn > m_FinalTurn)
simFrameLength = 0;
m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);
}
void CTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)
{
NETTURN_LOG("AddCommand(client=%d player=%d turn=%d)\n", client, player, turn);
if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))
{
debug_warn(L"Received command for invalid turn");
return;
}
m_Simulation2.GetScriptInterface().FreezeObject(data, true);
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, cx, data);
}
void CTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
{
NETTURN_LOG("FinishedAllCommands(%d, %d)\n", turn, turnLength);
ENSURE(turn == m_ReadyTurn + 1);
m_ReadyTurn = turn;
m_TurnLength = turnLength;
}
bool CTurnManager::TurnNeedsFullHash(u32 turn) const
{
// Check immediately for errors caused by e.g. inconsistent game versions
// (The hash is computed after the first sim update, so we start at turn == 1)
if (turn == 1)
return true;
// Otherwise check the full state every ~10 seconds in multiplayer games
// (TODO: should probably remove this when we're reasonably sure the game
// isn't too buggy, since the full hash is still pretty slow)
if (turn % 20 == 0)
return true;
return false;
}
void CTurnManager::EnableTimeWarpRecording(size_t numTurns)
{
m_TimeWarpStates.clear();
m_TimeWarpNumTurns = numTurns;
}
void CTurnManager::RewindTimeWarp()
{
if (m_TimeWarpStates.empty())
return;
std::stringstream stream(m_TimeWarpStates.back());
m_Simulation2.DeserializeState(stream);
m_TimeWarpStates.pop_back();
// Reset the turn manager state, so we won't execute stray commands and
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
ResetState(0, 1);
}
void CTurnManager::QuickSave()
{
TIMER(L"QuickSave");
std::stringstream stream;
if (!m_Simulation2.SerializeState(stream))
{
LOGERROR("Failed to quicksave game");
return;
}
m_QuickSaveState = stream.str();
if (g_GUI)
m_QuickSaveMetadata = g_GUI->GetSavedGameData();
else
m_QuickSaveMetadata = std::string();
LOGMESSAGERENDER("Quicksaved game");
}
void CTurnManager::QuickLoad()
{
TIMER(L"QuickLoad");
if (m_QuickSaveState.empty())
{
LOGERROR("Cannot quickload game - no game was quicksaved");
return;
}
std::stringstream stream(m_QuickSaveState);
if (!m_Simulation2.DeserializeState(stream))
{
LOGERROR("Failed to quickload game");
return;
}
if (g_GUI && !m_QuickSaveMetadata.empty())
g_GUI->RestoreSavedGameData(m_QuickSaveMetadata);
LOGMESSAGERENDER("Quickloaded game");
// See RewindTimeWarp
ResetState(0, 1);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -15,34 +15,33 @@
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_NETTURNMANAGER
#define INCLUDED_NETTURNMANAGER
#ifndef INCLUDED_TURNMANAGER
#define INCLUDED_TURNMANAGER
#include "simulation2/helpers/SimulationCommand.h"
#include "lib/os_path.h"
#include "NetMessage.h"
#include <list>
#include <map>
#include <vector>
extern const u32 DEFAULT_TURN_LENGTH_MP;
extern const u32 DEFAULT_TURN_LENGTH_SP;
extern const u32 DEFAULT_TURN_LENGTH_MP;
extern const int COMMAND_DELAY;
class CNetServerWorker;
class CNetClient;
class CSimulationMessage;
class CSimulation2;
class IReplayLogger;
/*
* This file deals with the logic of the network turn system. The basic idea is as in
/**
* This file defines the base class of the turn managers for clients, local games and replays.
* The basic idea of our turn managing system across a network is as in this article:
* http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.php?print=1
*
* Each player performs the simulation for turn N.
* User input is translated into commands scheduled for execution in turn N+2 which are
* distributed to all other clients.
* After a while, a player wants to perform the simulation for turn N+1,
* After a while, a client wants to perform the simulation for turn N+1,
* which first requires that it has all the other clients' commands for turn N+1.
* In that case, it does the simulation and tells all the other clients (via the server)
* it has finished sending commands for turn N+2, and it starts sending commands for turn N+3.
@ -53,18 +52,18 @@ class IReplayLogger;
*/
/**
* Common network turn system (used by clients and offline games).
* Common turn system (used by clients and offline games).
*/
class CNetTurnManager
class CTurnManager
{
NONCOPYABLE(CNetTurnManager);
NONCOPYABLE(CTurnManager);
public:
/**
* Construct for a given network session ID.
*/
CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay);
CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay);
virtual ~CNetTurnManager() { }
virtual ~CTurnManager() { }
void ResetState(u32 newCurrentTurn, u32 newReadyTurn);
@ -94,7 +93,7 @@ public:
* Returns whether Update(simFrameLength, ...) will process at least one new turn.
* @param simFrameLength Length of the previous frame, in simulation seconds
*/
bool WillUpdate(float simFrameLength);
bool WillUpdate(float simFrameLength) const;
/**
* Advance the graphics by a certain time.
@ -108,16 +107,6 @@ public:
*/
virtual void OnSimulationMessage(CSimulationMessage* msg) = 0;
/**
* Called when there has been an out-of-sync error.
*/
virtual void OnSyncError(u32 turn, const CStr& expectedHash, std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);
/**
* Shows a message box when an out of sync error has been detected in the session or visual replay.
*/
virtual void DisplayOOSError(u32 turn, const CStr& hash, const CStr& expectedHash, bool isReplay, std::vector<CSyncErrorMessage::S_m_PlayerNames>* playerNames, OsPath* path);
/**
* Called by simulation code, to add a new command to be distributed to all clients and executed soon.
*/
@ -166,7 +155,7 @@ protected:
* Returns whether we should compute a complete state hash for the given turn,
* instead of a quick less-complete hash.
*/
bool TurnNeedsFullHash(u32 turn);
bool TurnNeedsFullHash(u32 turn) const;
CSimulation2& m_Simulation2;
@ -203,154 +192,4 @@ private:
std::string m_QuickSaveMetadata;
};
/**
* Implementation of CNetTurnManager for network clients.
*/
class CNetClientTurnManager : public CNetTurnManager
{
public:
CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay);
virtual void OnSimulationMessage(CSimulationMessage* msg);
virtual void PostCommand(JS::HandleValue data);
/**
* Notifiy the server that all commands are sent to prepare the connection for termination.
*/
void OnDestroyConnection();
protected:
virtual void NotifyFinishedOwnCommands(u32 turn);
virtual void NotifyFinishedUpdate(u32 turn);
CNetClient& m_NetClient;
};
/**
* Implementation of CNetTurnManager for offline games.
*/
class CNetLocalTurnManager : public CNetTurnManager
{
public:
CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay);
virtual void OnSimulationMessage(CSimulationMessage* msg);
virtual void PostCommand(JS::HandleValue data);
protected:
virtual void NotifyFinishedOwnCommands(u32 turn);
virtual void NotifyFinishedUpdate(u32 turn);
};
/**
* Implementation of CNetTurnManager for replay games.
*/
class CNetReplayTurnManager : public CNetLocalTurnManager
{
public:
CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay);
void StoreReplayCommand(u32 turn, int player, const std::string& command);
void StoreReplayTurnLength(u32 turn, u32 turnLength);
void StoreReplayHash(u32 turn, const std::string& hash, bool quick);
void StoreFinalReplayTurn(u32 turn);
protected:
virtual void NotifyFinishedUpdate(u32 turn);
void DoTurn(u32 turn);
// Contains the commands of every player on each turn
std::map<u32, std::vector<std::pair<player_id_t, std::string>>> m_ReplayCommands;
// Contains the length of every turn
std::map<u32, u32> m_ReplayTurnLengths;
// Contains all replay hash values and weather or not the quick hash method was used
std::map<u32, std::pair<std::string, bool>> m_ReplayHash;
};
/**
* The server-side counterpart to CNetClientTurnManager.
* Records the turn state of each client, and sends turn advancement messages
* when all clients are ready.
*
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerTurnManager
{
NONCOPYABLE(CNetServerTurnManager);
public:
CNetServerTurnManager(CNetServerWorker& server);
void NotifyFinishedClientCommands(int client, u32 turn);
void NotifyFinishedClientUpdate(int client, const CStrW& playername, u32 turn, const CStr& hash);
/**
* Inform the turn manager of a new client who will be sending commands.
*/
void InitialiseClient(int client, u32 turn);
/**
* Inform the turn manager that a previously-initialised client has left the game
* and will no longer be sending commands.
*/
void UninitialiseClient(int client);
void SetTurnLength(u32 msecs);
/**
* Returns the latest turn for which all clients are ready;
* they will have already been told to execute this turn.
*/
u32 GetReadyTurn() { return m_ReadyTurn; }
/**
* Returns the turn length that was used for the given turn.
* Requires turn <= GetReadyTurn().
*/
u32 GetSavedTurnLength(u32 turn);
protected:
void CheckClientsReady();
/// The latest turn for which we have received all commands from all clients
u32 m_ReadyTurn;
// Client ID -> ready turn number (the latest turn for which all commands have been received from that client)
std::map<int, u32> m_ClientsReady;
// Client ID -> last known simulated turn number (for which we have the state hash)
// (the client has reached the start of this turn, not done the update for it yet)
std::map<int, u32> m_ClientsSimulated;
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
std::map<u32, std::map<int, std::string>> m_ClientStateHashes;
// Map of client ID -> playername
std::map<u32, CStrW> m_ClientPlayernames;
// Current turn length
u32 m_TurnLength;
// Turn lengths for all previously executed turns
std::vector<u32> m_SavedTurnLengths;
CNetServerWorker& m_NetServer;
bool m_HasSyncError;
};
#endif // INCLUDED_NETTURNMANAGER
#endif // INCLUDED_TURNMANAGER