diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp index 2448e40c8b..6b14ceb221 100644 --- a/source/gui/scripting/ScriptFunctions.cpp +++ b/source/gui/scripting/ScriptFunctions.cpp @@ -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" diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index aa23532ad0..b33dac382e 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -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" diff --git a/source/network/NetClientTurnManager.cpp b/source/network/NetClientTurnManager.cpp new file mode 100644 index 0000000000..7da290cc0e --- /dev/null +++ b/source/network/NetClientTurnManager.cpp @@ -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 . + */ + +#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& 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())); +} diff --git a/source/network/NetClientTurnManager.h b/source/network/NetClientTurnManager.h new file mode 100644 index 0000000000..c2f3d6ddfc --- /dev/null +++ b/source/network/NetClientTurnManager.h @@ -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 . + */ + +#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& playerNames); + +private: + void NotifyFinishedOwnCommands(u32 turn) override; + + void NotifyFinishedUpdate(u32 turn) override; + + CNetClient& m_NetClient; +}; + +#endif // INCLUDED_NETCLIENTTURNMANAGER diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 38680d8efd..941b31ef01 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -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 diff --git a/source/network/NetServerTurnManager.cpp b/source/network/NetServerTurnManager.cpp new file mode 100644 index 0000000000..1fc4114b51 --- /dev/null +++ b/source/network/NetServerTurnManager.cpp @@ -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 . + */ + +#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& 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::max(); + for (const std::pair& 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>& 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 OOSPlayerNames; + for (const std::pair& 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); +} diff --git a/source/network/NetServerTurnManager.h b/source/network/NetServerTurnManager.h new file mode 100644 index 0000000000..670e830f21 --- /dev/null +++ b/source/network/NetServerTurnManager.h @@ -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 . + */ + +#ifndef INCLUDED_NETSERVERTURNMANAGER +#define INCLUDED_NETSERVERTURNMANAGER + +#include +#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 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 m_ClientsSimulated; + + // Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted + std::map> m_ClientStateHashes; + + // Map of client ID -> playername + std::map m_ClientPlayernames; + + // Current turn length + u32 m_TurnLength; + + // Turn lengths for all previously executed turns + std::vector m_SavedTurnLengths; + + CNetServerWorker& m_NetServer; + + bool m_HasSyncError; +}; + +#endif // INCLUDED_NETSERVERTURNMANAGER diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp deleted file mode 100644 index 19bfb98911..0000000000 --- a/source/network/NetTurnManager.cpp +++ /dev/null @@ -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 . - */ - -#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 - -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::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 commands; - for (std::pair>& 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 commands; - for (std::pair>& 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& 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* 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& 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& 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::max(); - for (const std::pair& 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>& 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 OOSPlayerNames; - for (const std::pair& 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); -} diff --git a/source/network/tests/test_Net.h b/source/network/tests/test_Net.h index 55fcd1da53..be144ce70f 100644 --- a/source/network/tests/test_Net.h +++ b/source/network/tests/test_Net.h @@ -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); diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index eb5ad3d694..4f00fe56a5 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -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(GetTurnManager()); + CReplayTurnManager* replayTurnMgr = static_cast(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()); diff --git a/source/ps/Game.h b/source/ps/Game.h index 862ae4deb4..bfccb8d542 100644 --- a/source/ps/Game.h +++ b/source/ps/Game.h @@ -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 diff --git a/source/ps/Util.h b/source/ps/Util.h index 4bf67239ad..d61ce48b97 100644 --- a/source/ps/Util.h +++ b/source/ps/Util.h @@ -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; diff --git a/source/simulation2/components/CCmpCommandQueue.cpp b/source/simulation2/components/CCmpCommandQueue.cpp index 0dc91e22e4..90d4d3e5eb 100644 --- a/source/simulation2/components/CCmpCommandQueue.cpp +++ b/source/simulation2/components/CCmpCommandQueue.cpp @@ -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 { diff --git a/source/simulation2/system/LocalTurnManager.cpp b/source/simulation2/system/LocalTurnManager.cpp new file mode 100644 index 0000000000..f6fe834a17 --- /dev/null +++ b/source/simulation2/system/LocalTurnManager.cpp @@ -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 . + */ + +#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"); +} diff --git a/source/simulation2/system/LocalTurnManager.h b/source/simulation2/system/LocalTurnManager.h new file mode 100644 index 0000000000..f7ba298e0c --- /dev/null +++ b/source/simulation2/system/LocalTurnManager.h @@ -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 . + */ + +#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 diff --git a/source/simulation2/system/ReplayTurnManager.cpp b/source/simulation2/system/ReplayTurnManager.cpp new file mode 100644 index 0000000000..b1ce296896 --- /dev/null +++ b/source/simulation2/system/ReplayTurnManager.cpp @@ -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 . + */ + +#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>::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& 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())); +} diff --git a/source/simulation2/system/ReplayTurnManager.h b/source/simulation2/system/ReplayTurnManager.h new file mode 100644 index 0000000000..71ad11b7da --- /dev/null +++ b/source/simulation2/system/ReplayTurnManager.h @@ -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 . + */ + +#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>> m_ReplayCommands; + + // Contains the length of every turn + std::map m_ReplayTurnLengths; + + // Contains all replay hash values and weather or not the quick hash method was used + std::map> m_ReplayHash; +}; + +#endif // INCLUDED_REPLAYTURNMANAGER diff --git a/source/simulation2/system/TurnManager.cpp b/source/simulation2/system/TurnManager.cpp new file mode 100644 index 0000000000..9a518d82c8 --- /dev/null +++ b/source/simulation2/system/TurnManager.cpp @@ -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 . + */ + +#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::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 commands; + for (std::pair>& 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 commands; + for (std::pair>& 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); +} diff --git a/source/network/NetTurnManager.h b/source/simulation2/system/TurnManager.h similarity index 51% rename from source/network/NetTurnManager.h rename to source/simulation2/system/TurnManager.h index 806fe77d7c..92d228785d 100644 --- a/source/network/NetTurnManager.h +++ b/source/simulation2/system/TurnManager.h @@ -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 . */ -#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 #include #include -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& 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* 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>> m_ReplayCommands; - - // Contains the length of every turn - std::map m_ReplayTurnLengths; - - // Contains all replay hash values and weather or not the quick hash method was used - std::map> 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 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 m_ClientsSimulated; - - // Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted - std::map> m_ClientStateHashes; - - // Map of client ID -> playername - std::map m_ClientPlayernames; - - // Current turn length - u32 m_TurnLength; - - // Turn lengths for all previously executed turns - std::vector m_SavedTurnLengths; - - CNetServerWorker& m_NetServer; - - bool m_HasSyncError; -}; - -#endif // INCLUDED_NETTURNMANAGER +#endif // INCLUDED_TURNMANAGER