Ticket #4095: rename.patch
File rename.patch, 58.3 KB (added by , 8 years ago) |
---|
-
source/gui/scripting/ScriptFunctions.cpp
35 35 #include "lib/timer.h" 36 36 #include "lib/utf8.h" 37 37 #include "lobby/scripting/JSInterface_Lobby.h" 38 38 #include "maths/FixedVector3D.h" 39 39 #include "network/NetClient.h" 40 #include "network/NetMessage.h" 40 41 #include "network/NetServer.h" 41 #include "network/NetTurnManager.h"42 42 #include "ps/CConsole.h" 43 43 #include "ps/CLogger.h" 44 44 #include "ps/Errors.h" 45 45 #include "ps/GUID.h" 46 46 #include "ps/Game.h" … … 68 68 #include "simulation2/components/ICmpPlayerManager.h" 69 69 #include "simulation2/components/ICmpRangeManager.h" 70 70 #include "simulation2/components/ICmpSelectable.h" 71 71 #include "simulation2/components/ICmpTemplateManager.h" 72 72 #include "simulation2/helpers/Selection.h" 73 #include "simulation2/system/TurnManager.h" 73 74 #include "soundmanager/SoundManager.h" 74 75 #include "soundmanager/scripting/JSInterface_Sound.h" 75 76 #include "tools/atlas/GameInterface/GameLoop.h" 76 77 77 78 /* -
source/network/NetTurnManager.cpp
16 16 */ 17 17 18 18 #include "precompiled.h" 19 19 20 20 #include "NetTurnManager.h" 21 #include "NetMessage.h"22 21 23 22 #include "network/NetServer.h" 24 23 #include "network/NetClient.h" 25 #include "network/NetMessage.h"26 24 27 25 #include "gui/GUIManager.h" 28 #include "maths/MathUtil.h"29 26 #include "ps/CLogger.h" 30 #include "ps/Profile.h"31 27 #include "ps/Pyrogenesis.h" 32 28 #include "ps/Replay.h" 33 #include "ps/SavedGame.h"34 #include "scriptinterface/ScriptInterface.h"35 29 #include "simulation2/Simulation2.h" 36 30 37 31 #include <sstream> 38 32 #include <fstream> 39 33 #include <iomanip> 40 34 41 const u32 DEFAULT_TURN_LENGTH_MP = 500;42 const u32 DEFAULT_TURN_LENGTH_SP = 200;43 44 static const int COMMAND_DELAY = 2;45 46 35 #if 0 47 36 #define NETTURN_LOG(args) debug_printf args 48 37 #else 49 38 #define NETTURN_LOG(args) 50 39 #endif … … static std::string Hexify(const std::str 56 45 for (size_t i = 0; i < s.size(); ++i) 57 46 str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i]; 58 47 return str.str(); 59 48 } 60 49 61 CNetTurnManager::CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay) :62 m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength), m_DeltaSimTime(0),63 m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay),64 m_TimeWarpNumTurns(0), m_FinalTurn(std::numeric_limits<u32>::max())65 {66 // When we are on turn n, we schedule new commands for n+2.67 // We know that all other clients have finished scheduling commands for n (else we couldn't have got here).68 // We know we have not yet finished scheduling commands for n+2.69 // Hence other clients can be on turn n-1, n, n+1, and no other.70 // So they can be sending us commands scheduled for n+1, n+2, n+3.71 // So we need a 3-element buffer:72 m_QueuedCommands.resize(COMMAND_DELAY + 1);73 }74 75 void CNetTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)76 {77 m_CurrentTurn = newCurrentTurn;78 m_ReadyTurn = newReadyTurn;79 m_DeltaSimTime = 0;80 size_t queuedCommandsSize = m_QueuedCommands.size();81 m_QueuedCommands.clear();82 m_QueuedCommands.resize(queuedCommandsSize);83 }84 85 void CNetTurnManager::SetPlayerID(int playerId)86 {87 m_PlayerId = playerId;88 }89 90 bool CNetTurnManager::WillUpdate(float simFrameLength)91 {92 // Keep this in sync with the return value of Update()93 94 if (m_CurrentTurn > m_FinalTurn)95 return false;96 97 if (m_DeltaSimTime + simFrameLength < 0)98 return false;99 100 if (m_ReadyTurn <= m_CurrentTurn)101 return false;102 103 return true;104 }105 106 bool CNetTurnManager::Update(float simFrameLength, size_t maxTurns)107 {108 if (m_CurrentTurn > m_FinalTurn)109 return false;110 111 m_DeltaSimTime += simFrameLength;112 113 // If the game becomes laggy, m_DeltaSimTime increases progressively.114 // The engine will fast forward accordingly to catch up.115 // To keep the game playable, stop fast forwarding after 2 turn lengths.116 m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);117 118 // If we haven't reached the next turn yet, do nothing119 if (m_DeltaSimTime < 0)120 return false;121 122 NETTURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));123 124 // Check that the next turn is ready for execution125 if (m_ReadyTurn <= m_CurrentTurn)126 {127 // Oops, we wanted to start the next turn but it's not ready yet -128 // there must be too much network lag.129 // TODO: complain to the user.130 // TODO: send feedback to the server to increase the turn length.131 132 // Reset the next-turn timer to 0 so we try again next update but133 // so we don't rush to catch up in subsequent turns.134 // TODO: we should do clever rate adjustment instead of just pausing like this.135 m_DeltaSimTime = 0;136 137 return false;138 }139 140 maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn141 142 for (size_t i = 0; i < maxTurns; ++i)143 {144 // Check that we've reached the i'th next turn145 if (m_DeltaSimTime < 0)146 break;147 148 // Check that the i'th next turn is still ready149 if (m_ReadyTurn <= m_CurrentTurn)150 break;151 152 NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);153 154 // Increase now, so Update can send new commands for a subsequent turn155 ++m_CurrentTurn;156 157 // Clean up any destroyed entities since the last turn (e.g. placement previews158 // or rally point flags generated by the GUI). (Must do this before the time warp159 // serialization.)160 m_Simulation2.FlushDestroyedEntities();161 162 // Save the current state for rewinding, if enabled163 if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)164 {165 PROFILE3("time warp serialization");166 std::stringstream stream;167 m_Simulation2.SerializeState(stream);168 m_TimeWarpStates.push_back(stream.str());169 }170 171 // Put all the client commands into a single list, in a globally consistent order172 std::vector<SimulationCommand> commands;173 for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])174 commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));175 176 m_QueuedCommands.pop_front();177 m_QueuedCommands.resize(m_QueuedCommands.size() + 1);178 179 m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);180 181 NETTURN_LOG((L"Running %d cmds\n", commands.size()));182 183 m_Simulation2.Update(m_TurnLength, commands);184 185 NotifyFinishedUpdate(m_CurrentTurn);186 187 // Set the time for the next turn update188 m_DeltaSimTime -= m_TurnLength / 1000.f;189 }190 191 return true;192 }193 194 bool CNetTurnManager::UpdateFastForward()195 {196 m_DeltaSimTime = 0;197 198 NETTURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));199 200 // Check that the next turn is ready for execution201 if (m_ReadyTurn <= m_CurrentTurn)202 return false;203 204 while (m_ReadyTurn > m_CurrentTurn)205 {206 // TODO: It would be nice to remove some of the duplication with Update()207 // (This is similar but doesn't call any Notify functions or update DeltaTime,208 // it just updates the simulation state)209 210 ++m_CurrentTurn;211 212 m_Simulation2.FlushDestroyedEntities();213 214 // Put all the client commands into a single list, in a globally consistent order215 std::vector<SimulationCommand> commands;216 for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0])217 commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));218 219 m_QueuedCommands.pop_front();220 m_QueuedCommands.resize(m_QueuedCommands.size() + 1);221 222 m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);223 224 NETTURN_LOG((L"Running %d cmds\n", commands.size()));225 226 m_Simulation2.Update(m_TurnLength, commands);227 }228 229 return true;230 }231 232 void CNetTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames)233 {234 NETTURN_LOG((L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str()));235 236 // Only complain the first time237 if (m_HasSyncError)238 return;239 240 bool quick = !TurnNeedsFullHash(turn);241 std::string hash;242 ENSURE(m_Simulation2.ComputeStateHash(hash, quick));243 244 OsPath path = psLogDir() / "oos_dump.txt";245 std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);246 m_Simulation2.DumpDebugState(file);247 file.close();248 249 hash = Hexify(hash);250 const std::string& expectedHashHex = Hexify(expectedHash);251 252 DisplayOOSError(turn, hash, expectedHashHex, false, &playerNames, &path);253 }254 255 void CNetTurnManager::DisplayOOSError(u32 turn, const CStr& hash, const CStr& expectedHash, bool isReplay, std::vector<CSyncErrorMessage::S_m_PlayerNames>* playerNames = NULL, OsPath* path = NULL)256 {257 m_HasSyncError = true;258 259 std::stringstream msg;260 msg << "Out of sync on turn " << turn;261 262 if (playerNames)263 for (size_t i = 0; i < playerNames->size(); ++i)264 msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring((*playerNames)[i].m_Name);265 266 if (isReplay)267 msg << "\n\n" << "The current game state is different from the original game state.";268 else269 msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") << " the hosts game state.";270 271 if (path)272 msg << "\n\n" << "Dumping current state to " << CStr(path->string8()).EscapeToPrintableASCII();273 274 LOGERROR("%s", msg.str());275 276 if (g_GUI)277 g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));278 }279 280 void CNetTurnManager::Interpolate(float simFrameLength, float realFrameLength)281 {282 // TODO: using m_TurnLength might be a bit dodgy when length changes - maybe283 // we need to save the previous turn length?284 285 float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);286 287 // Stop animations while still updating the selection highlight288 if (m_CurrentTurn > m_FinalTurn)289 simFrameLength = 0;290 291 m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);292 }293 294 void CNetTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)295 {296 NETTURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn));297 298 if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))299 {300 debug_warn(L"Received command for invalid turn");301 return;302 }303 304 m_Simulation2.GetScriptInterface().FreezeObject(data, true);305 m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, m_Simulation2.GetScriptInterface().GetContext(), data);306 }307 308 void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)309 {310 NETTURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength));311 312 ENSURE(turn == m_ReadyTurn + 1);313 m_ReadyTurn = turn;314 m_TurnLength = turnLength;315 }316 317 bool CNetTurnManager::TurnNeedsFullHash(u32 turn)318 {319 // Check immediately for errors caused by e.g. inconsistent game versions320 // (The hash is computed after the first sim update, so we start at turn == 1)321 if (turn == 1)322 return true;323 324 // Otherwise check the full state every ~10 seconds in multiplayer games325 // (TODO: should probably remove this when we're reasonably sure the game326 // isn't too buggy, since the full hash is still pretty slow)327 if (turn % 20 == 0)328 return true;329 330 return false;331 }332 333 void CNetTurnManager::EnableTimeWarpRecording(size_t numTurns)334 {335 m_TimeWarpStates.clear();336 m_TimeWarpNumTurns = numTurns;337 }338 339 void CNetTurnManager::RewindTimeWarp()340 {341 if (m_TimeWarpStates.empty())342 return;343 344 std::stringstream stream(m_TimeWarpStates.back());345 m_Simulation2.DeserializeState(stream);346 m_TimeWarpStates.pop_back();347 348 // Reset the turn manager state, so we won't execute stray commands and349 // won't do the next snapshot until the appropriate time.350 // (Ideally we ought to serialise the turn manager state and restore it351 // here, but this is simpler for now.)352 ResetState(0, 1);353 }354 355 void CNetTurnManager::QuickSave()356 {357 TIMER(L"QuickSave");358 359 std::stringstream stream;360 if (!m_Simulation2.SerializeState(stream))361 {362 LOGERROR("Failed to quicksave game");363 return;364 }365 366 m_QuickSaveState = stream.str();367 if (g_GUI)368 m_QuickSaveMetadata = g_GUI->GetSavedGameData();369 else370 m_QuickSaveMetadata = std::string();371 372 LOGMESSAGERENDER("Quicksaved game");373 374 }375 376 void CNetTurnManager::QuickLoad()377 {378 TIMER(L"QuickLoad");379 380 if (m_QuickSaveState.empty())381 {382 LOGERROR("Cannot quickload game - no game was quicksaved");383 return;384 }385 386 std::stringstream stream(m_QuickSaveState);387 if (!m_Simulation2.DeserializeState(stream))388 {389 LOGERROR("Failed to quickload game");390 return;391 }392 393 if (g_GUI && !m_QuickSaveMetadata.empty())394 g_GUI->RestoreSavedGameData(m_QuickSaveMetadata);395 396 LOGMESSAGERENDER("Quickloaded game");397 398 // See RewindTimeWarp399 ResetState(0, 1);400 }401 402 403 50 CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) : 404 C NetTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)51 CTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client) 405 52 { 406 53 } 407 54 408 55 void CNetClientTurnManager::PostCommand(JS::HandleValue data) 409 56 { … … void CNetClientTurnManager::NotifyFinish 461 108 void CNetClientTurnManager::OnDestroyConnection() 462 109 { 463 110 NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY); 464 111 } 465 112 466 void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg) 467 { 468 // Command received from the server - store it for later execution 469 AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn); 470 } 471 472 473 CNetLocalTurnManager::CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) : 474 CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay) 113 void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames) 475 114 { 476 } 477 478 void CNetLocalTurnManager::PostCommand(JS::HandleValue data) 479 { 480 // Add directly to the next turn, ignoring COMMAND_DELAY, 481 // because we don't need to compensate for network latency 482 AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1); 483 } 115 NETTURN_LOG((L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str())); 484 116 485 void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn) 486 { 487 FinishedAllCommands(turn, m_TurnLength); 488 } 117 // Only complain the first time 118 if (m_HasSyncError) 119 return; 489 120 490 void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn)) 491 { 492 #if 0 // this hurts performance and is only useful for verifying log replays 121 bool quick = !TurnNeedsFullHash(turn); 493 122 std::string hash; 494 { 495 PROFILE3("state hash check"); 496 ENSURE(m_Simulation2.ComputeStateHash(hash)); 497 } 498 m_Replay.Hash(hash); 499 #endif 500 } 501 502 void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg)) 503 { 504 debug_warn(L"This should never be called"); 505 } 506 507 CNetReplayTurnManager::CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay) : 508 CNetLocalTurnManager(simulation, replay) 509 { 510 } 511 512 void CNetReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command) 513 { 514 // Using the pair we make sure that commands per turn will be processed in the correct order 515 m_ReplayCommands[turn].emplace_back(player, command); 516 } 517 518 void CNetReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick) 519 { 520 m_ReplayHash[turn] = std::make_pair(hash, quick); 521 } 522 523 void CNetReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength) 524 { 525 m_ReplayTurnLengths[turn] = turnLength; 526 527 // Initialize turn length 528 if (turn == 0) 529 m_TurnLength = m_ReplayTurnLengths[0]; 530 } 123 ENSURE(m_Simulation2.ComputeStateHash(hash, quick)); 531 124 532 void CNetReplayTurnManager::StoreFinalReplayTurn(u32 turn) 533 { 534 m_ FinalTurn = turn;535 } 125 OsPath path = psLogDir() / "oos_dump.txt"; 126 std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); 127 m_Simulation2.DumpDebugState(file); 128 file.close(); 536 129 537 void CNetReplayTurnManager::NotifyFinishedUpdate(u32 turn) 538 { 539 if (turn == 1 && m_FinalTurn == 0) 540 g_GUI->SendEventToAll("ReplayFinished"); 130 hash = Hexify(hash); 131 const std::string& expectedHashHex = Hexify(expectedHash); 541 132 542 if (turn > m_FinalTurn) 543 return; 133 m_HasSyncError = true; 544 134 545 DoTurn(turn); 135 std::stringstream msg; 136 msg << "Out of sync on turn " << turn; 546 137 547 // Compare hash if it exists in the replay and if we didn't have an OOS already 548 if (m_HasSyncError || m_ReplayHash.find(turn) == m_ReplayHash.end()) 549 return; 138 for (size_t i = 0; i < playerNames.size(); ++i) 139 msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring(playerNames[i].m_Name); 550 140 551 std::string expectedHash = m_ReplayHash[turn].first;552 bool quickHash = m_ReplayHash[turn].second;141 msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") 142 << " the hosts game state." << "\n\n" << "Dumping current state to " << CStr(path.string8()).EscapeToPrintableASCII(); 553 143 554 // Compute hash 555 std::string hash; 556 ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash)); 557 hash = Hexify(hash); 144 LOGERROR("%s", msg.str()); 558 145 559 if ( hash != expectedHash)560 DisplayOOSError(turn, hash, expectedHash, true);146 if (g_GUI) 147 g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str())); 561 148 } 562 149 563 void CNet ReplayTurnManager::DoTurn(u32 turn)150 void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg) 564 151 { 565 debug_printf("Executing turn %u of %u\n", turn, m_FinalTurn); 566 567 m_TurnLength = m_ReplayTurnLengths[turn]; 568 569 // Simulate commands for that turn 570 for (const std::pair<player_id_t, std::string>& p : m_ReplayCommands[turn]) 571 { 572 JS::RootedValue command(m_Simulation2.GetScriptInterface().GetContext()); 573 m_Simulation2.GetScriptInterface().ParseJSON(p.second, &command); 574 AddCommand(m_ClientId, p.first, command, m_CurrentTurn + 1); 575 } 576 577 if (turn == m_FinalTurn) 578 g_GUI->SendEventToAll("ReplayFinished"); 152 // Command received from the server - store it for later execution 153 AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn); 579 154 } 580 155 581 156 CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) : 582 157 m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false) 583 158 { -
source/network/NetTurnManager.h
16 16 */ 17 17 18 18 #ifndef INCLUDED_NETTURNMANAGER 19 19 #define INCLUDED_NETTURNMANAGER 20 20 21 #include "simulation2/helpers/SimulationCommand.h" 22 #include "lib/os_path.h" 23 #include "NetMessage.h" 24 25 #include <list> 26 #include <map> 27 #include <vector> 28 29 extern const u32 DEFAULT_TURN_LENGTH_MP; 30 extern const u32 DEFAULT_TURN_LENGTH_SP; 21 #include "network/NetMessage.h" 22 #include "simulation2/system/TurnManager.h" 31 23 32 24 class CNetServerWorker; 33 25 class CNetClient; 34 class CSimulationMessage;35 class CSimulation2;36 class IReplayLogger;37 38 /*39 * This file deals with the logic of the network turn system. The basic idea is as in40 * http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.php?print=141 *42 * Each player performs the simulation for turn N.43 * User input is translated into commands scheduled for execution in turn N+2 which are44 * distributed to all other clients.45 * After a while, a player wants to perform the simulation for turn N+1,46 * which first requires that it has all the other clients' commands for turn N+1.47 * In that case, it does the simulation and tells all the other clients (via the server)48 * it has finished sending commands for turn N+2, and it starts sending commands for turn N+3.49 *50 * Commands are redistributed immediately by the server.51 * To ensure a consistent execution of commands, they are each associated with a52 * client session ID (which is globally unique and consistent), which is used to sort them.53 */54 55 /**56 * Common network turn system (used by clients and offline games).57 */58 class CNetTurnManager59 {60 NONCOPYABLE(CNetTurnManager);61 public:62 /**63 * Construct for a given network session ID.64 */65 CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay);66 67 virtual ~CNetTurnManager() { }68 69 void ResetState(u32 newCurrentTurn, u32 newReadyTurn);70 71 /**72 * Set the current user's player ID, which will be added into command messages.73 */74 void SetPlayerID(int playerId);75 76 /**77 * Advance the simulation by a certain time. If this brings us past the current78 * turn length, the next turns are processed and the function returns true.79 * Otherwise, nothing happens and it returns false.80 *81 * @param simFrameLength Length of the previous frame, in simulation seconds82 * @param maxTurns Maximum number of turns to simulate at once83 */84 bool Update(float simFrameLength, size_t maxTurns);85 86 /**87 * Advance the simulation by as much as possible. Intended for catching up88 * over a small number of turns when rejoining a multiplayer match.89 * Returns true if it advanced by at least one turn.90 */91 bool UpdateFastForward();92 93 /**94 * Returns whether Update(simFrameLength, ...) will process at least one new turn.95 * @param simFrameLength Length of the previous frame, in simulation seconds96 */97 bool WillUpdate(float simFrameLength);98 99 /**100 * Advance the graphics by a certain time.101 * @param simFrameLength Length of the previous frame, in simulation seconds102 * @param realFrameLength Length of the previous frame, in real time seconds103 */104 void Interpolate(float simFrameLength, float realFrameLength);105 106 /**107 * Called by networking code when a simulation message is received.108 */109 virtual void OnSimulationMessage(CSimulationMessage* msg) = 0;110 111 /**112 * Called when there has been an out-of-sync error.113 */114 virtual void OnSyncError(u32 turn, const CStr& expectedHash, std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames);115 116 /**117 * Shows a message box when an out of sync error has been detected in the session or visual replay.118 */119 virtual void DisplayOOSError(u32 turn, const CStr& hash, const CStr& expectedHash, bool isReplay, std::vector<CSyncErrorMessage::S_m_PlayerNames>* playerNames, OsPath* path);120 121 /**122 * Called by simulation code, to add a new command to be distributed to all clients and executed soon.123 */124 virtual void PostCommand(JS::HandleValue data) = 0;125 126 /**127 * Called when all commands for a given turn have been received.128 * This allows Update to progress to that turn.129 */130 void FinishedAllCommands(u32 turn, u32 turnLength);131 132 /**133 * Enables the recording of state snapshots every @p numTurns,134 * which can be jumped back to via RewindTimeWarp().135 * If @p numTurns is 0 then recording is disabled.136 */137 void EnableTimeWarpRecording(size_t numTurns);138 139 /**140 * Jumps back to the latest recorded state snapshot (if any).141 */142 void RewindTimeWarp();143 144 void QuickSave();145 void QuickLoad();146 147 u32 GetCurrentTurn() { return m_CurrentTurn; }148 149 protected:150 /**151 * Store a command to be executed at a given turn.152 */153 void AddCommand(int client, int player, JS::HandleValue data, u32 turn);154 155 /**156 * Called when this client has finished sending all its commands scheduled for the given turn.157 */158 virtual void NotifyFinishedOwnCommands(u32 turn) = 0;159 160 /**161 * Called when this client has finished a simulation update.162 */163 virtual void NotifyFinishedUpdate(u32 turn) = 0;164 165 /**166 * Returns whether we should compute a complete state hash for the given turn,167 * instead of a quick less-complete hash.168 */169 bool TurnNeedsFullHash(u32 turn);170 171 CSimulation2& m_Simulation2;172 173 /// The turn that we have most recently executed174 u32 m_CurrentTurn;175 176 /// The latest turn for which we have received all commands from all clients177 u32 m_ReadyTurn;178 179 // Current turn length180 u32 m_TurnLength;181 182 /// Commands queued at each turn (index 0 is for m_CurrentTurn+1)183 std::deque<std::map<u32, std::vector<SimulationCommand>>> m_QueuedCommands;184 185 int m_PlayerId;186 uint m_ClientId;187 188 /// Simulation time remaining until we ought to execute the next turn (as a negative value to189 /// add elapsed time increments to until we reach 0).190 float m_DeltaSimTime;191 192 bool m_HasSyncError;193 194 IReplayLogger& m_Replay;195 196 // The number of the last turn that is allowed to be executed (used for replays)197 u32 m_FinalTurn;198 199 private:200 size_t m_TimeWarpNumTurns; // 0 if disabled201 std::list<std::string> m_TimeWarpStates;202 std::string m_QuickSaveState; // TODO: should implement a proper disk-based quicksave system203 std::string m_QuickSaveMetadata;204 };205 206 26 207 27 /** 208 * Implementation of C NetTurnManager for network clients.28 * Implementation of CTurnManager for network clients. 209 29 */ 210 class CNetClientTurnManager : public C NetTurnManager30 class CNetClientTurnManager : public CTurnManager 211 31 { 212 32 public: 213 33 CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay); 214 34 215 35 virtual void OnSimulationMessage(CSimulationMessage* msg); 216 36 217 37 virtual void PostCommand(JS::HandleValue data); 218 38 219 39 /** 220 * Notif iy the server that all commands are sent to prepare the connection for termination.40 * Notify the server that all commands are sent to prepare the connection for termination. 221 41 */ 222 42 void OnDestroyConnection(); 223 43 44 void OnSyncError(u32 turn, const CStr& expectedHash, const std::vector<CSyncErrorMessage::S_m_PlayerNames>& playerNames); 45 224 46 protected: 225 47 virtual void NotifyFinishedOwnCommands(u32 turn); 226 48 227 49 virtual void NotifyFinishedUpdate(u32 turn); 228 50 229 51 CNetClient& m_NetClient; 230 52 }; 231 53 232 54 /** 233 * Implementation of CNetTurnManager for offline games.234 */235 class CNetLocalTurnManager : public CNetTurnManager236 {237 public:238 CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay);239 240 virtual void OnSimulationMessage(CSimulationMessage* msg);241 242 virtual void PostCommand(JS::HandleValue data);243 244 protected:245 virtual void NotifyFinishedOwnCommands(u32 turn);246 247 virtual void NotifyFinishedUpdate(u32 turn);248 };249 250 251 252 /**253 * Implementation of CNetTurnManager for replay games.254 */255 class CNetReplayTurnManager : public CNetLocalTurnManager256 {257 public:258 CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay);259 260 void StoreReplayCommand(u32 turn, int player, const std::string& command);261 262 void StoreReplayTurnLength(u32 turn, u32 turnLength);263 264 void StoreReplayHash(u32 turn, const std::string& hash, bool quick);265 266 void StoreFinalReplayTurn(u32 turn);267 268 269 protected:270 virtual void NotifyFinishedUpdate(u32 turn);271 272 void DoTurn(u32 turn);273 274 // Contains the commands of every player on each turn275 std::map<u32, std::vector<std::pair<player_id_t, std::string>>> m_ReplayCommands;276 277 // Contains the length of every turn278 std::map<u32, u32> m_ReplayTurnLengths;279 280 // Contains all replay hash values and weather or not the quick hash method was used281 std::map<u32, std::pair<std::string, bool>> m_ReplayHash;282 };283 /**284 55 * The server-side counterpart to CNetClientTurnManager. 285 56 * Records the turn state of each client, and sends turn advancement messages 286 57 * when all clients are ready. 287 58 * 288 59 * Thread-safety: -
source/network/tests/test_Net.h
21 21 #include "lib/external_libraries/enet.h" 22 22 #include "lib/external_libraries/libsdl.h" 23 23 #include "lib/tex/tex.h" 24 24 #include "network/NetServer.h" 25 25 #include "network/NetClient.h" 26 #include "network/NetTurnManager.h"27 26 #include "network/NetMessage.h" 28 27 #include "network/NetMessages.h" 29 28 #include "ps/CLogger.h" 30 29 #include "ps/Game.h" 31 30 #include "ps/Filesystem.h" 32 31 #include "ps/Loader.h" 33 32 #include "ps/XML/Xeromyces.h" 34 33 #include "scriptinterface/ScriptInterface.h" 35 34 #include "simulation2/Simulation2.h" 35 #include "simulation2/system/TurnManager.h" 36 36 37 37 class TestNetComms : public CxxTest::TestSuite 38 38 { 39 39 public: 40 40 void setUp() … … public: 137 137 // and prints a load of debug output so you can see if anything funny's going on 138 138 139 139 ScriptInterface scriptInterface("Engine", "Test", g_ScriptRuntime); 140 140 JSContext* cx = scriptInterface.GetContext(); 141 141 JSAutoRequest rq(cx); 142 142 143 143 TestStdoutLogger logger; 144 144 145 145 std::vector<CNetClient*> clients; 146 146 147 147 CGame client1Game(true); … … public: 202 202 void test_rejoin_DISABLED() 203 203 { 204 204 ScriptInterface scriptInterface("Engine", "Test", g_ScriptRuntime); 205 205 JSContext* cx = scriptInterface.GetContext(); 206 206 JSAutoRequest rq(cx); 207 207 208 208 TestStdoutLogger logger; 209 209 210 210 std::vector<CNetClient*> clients; 211 211 212 212 CGame client1Game(true); … … public: 308 308 client1Game.GetTurnManager()->Update(1.0f, 1); 309 309 client3Game.GetTurnManager()->Update(1.0f, 1); 310 310 wait(clients, 100); 311 311 312 312 // (This SetTurnLength thing doesn't actually detect errors unless you change 313 // C NetTurnManager::TurnNeedsFullHash to always return true)313 // CTurnManager::TurnNeedsFullHash to always return true) 314 314 315 315 { 316 316 JS::RootedValue cmd(cx); 317 317 client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 3]\\n'})", &cmd); 318 318 client1Game.GetTurnManager()->PostCommand(cmd); -
source/ps/Game.cpp
27 27 #include "gui/CGUI.h" 28 28 #include "lib/config2.h" 29 29 #include "lib/timer.h" 30 30 #include "network/NetClient.h" 31 31 #include "network/NetServer.h" 32 #include "network/NetTurnManager.h"33 32 #include "ps/CConsole.h" 34 33 #include "ps/CLogger.h" 35 34 #include "ps/CStr.h" 36 35 #include "ps/Loader.h" 37 36 #include "ps/LoaderThunks.h" … … 45 44 #include "renderer/WaterManager.h" 46 45 #include "scriptinterface/ScriptInterface.h" 47 46 #include "simulation2/Simulation2.h" 48 47 #include "simulation2/components/ICmpPlayer.h" 49 48 #include "simulation2/components/ICmpPlayerManager.h" 49 #include "simulation2/system/TurnManager.h" 50 50 #include "soundmanager/ISoundManager.h" 51 51 52 52 #include "tools/atlas/GameInterface/GameLoop.h" 53 53 54 54 extern bool g_GameRestarted; … … CGame::CGame(bool disableGraphics, bool 85 85 // Need to set the CObjectManager references after various objects have 86 86 // been initialised, so do it here rather than via the initialisers above. 87 87 if (m_GameView) 88 88 m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); 89 89 90 m_TurnManager = new C NetLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client90 m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client 91 91 92 92 m_Simulation2->LoadDefaultScripts(); 93 93 } 94 94 95 95 /** … … CGame::~CGame() 108 108 delete m_World; 109 109 delete m_ReplayLogger; 110 110 delete m_ReplayStream; 111 111 } 112 112 113 void CGame::SetTurnManager(C NetTurnManager* turnManager)113 void CGame::SetTurnManager(CTurnManager* turnManager) 114 114 { 115 115 if (m_TurnManager) 116 116 delete m_TurnManager; 117 117 118 118 m_TurnManager = turnManager; … … int CGame::LoadVisualReplayData() 125 125 { 126 126 ENSURE(m_IsVisualReplay); 127 127 ENSURE(!m_ReplayPath.empty()); 128 128 ENSURE(m_ReplayStream); 129 129 130 C NetReplayTurnManager* replayTurnMgr = static_cast<CNetReplayTurnManager*>(GetTurnManager());130 CReplayTurnManager* replayTurnMgr = static_cast<CReplayTurnManager*>(GetTurnManager()); 131 131 132 132 u32 currentTurn = 0; 133 133 std::string type; 134 134 while ((*m_ReplayStream >> type).good()) 135 135 { … … bool CGame::StartVisualReplay(const std: 173 173 debug_printf("Starting to replay %s\n", replayPath.c_str()); 174 174 175 175 m_IsVisualReplay = true; 176 176 ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); 177 177 178 SetTurnManager(new C NetReplayTurnManager(*m_Simulation2, GetReplayLogger()));178 SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); 179 179 180 180 m_ReplayPath = replayPath; 181 181 m_ReplayStream = new std::ifstream(m_ReplayPath.c_str()); 182 182 183 183 std::string type; … … bool CGame::StartVisualReplay(const std: 200 200 void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState) 201 201 { 202 202 ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); 203 203 JSContext* cx = scriptInterface.GetContext(); 204 204 JSAutoRequest rq(cx); 205 205 206 206 m_InitialSavedState = savedState; 207 207 m_IsSavedGame = !savedState.empty(); 208 208 209 209 m_Simulation2->SetInitAttributes(attribs); 210 210 … … int CGame::LoadInitialState() 286 286 **/ 287 287 PSRETURN CGame::ReallyStartGame() 288 288 { 289 289 JSContext* cx = m_Simulation2->GetScriptInterface().GetContext(); 290 290 JSAutoRequest rq(cx); 291 291 292 292 // Call the script function InitGame only for new games, not saved games 293 293 if (!m_IsSavedGame) 294 294 { 295 // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) 295 // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) 296 296 // that needs to be done before setting up the AI and shouldn't be done in Atlas 297 297 if (!g_AtlasGameLoop->running) 298 298 m_Simulation2->PreInitGame(); 299 299 300 300 JS::RootedValue settings(cx); … … PSRETURN CGame::ReallyStartGame() 308 308 // and we could end up rendering before having set up any models (so they'd 309 309 // all be invisible) 310 310 Interpolate(0, 0); 311 311 312 312 m_GameStarted=true; 313 313 314 314 // Render a frame to begin loading assets 315 315 if (CRenderer::IsInitialised()) 316 316 Render(); 317 317 318 318 if (g_NetClient) … … void CGame::Update(const double deltaRea 378 378 { 379 379 if (m_Paused || !m_TurnManager) 380 380 return; 381 381 382 382 const double deltaSimTime = deltaRealTime * m_SimRate; 383 383 384 384 if (deltaSimTime) 385 385 { 386 386 // To avoid confusing the profiler, we need to trigger the new turn 387 387 // while we're not nested inside any PROFILE blocks 388 388 if (m_TurnManager->WillUpdate(deltaSimTime)) -
source/ps/Game.h
25 25 #include "simulation2/helpers/Player.h" 26 26 27 27 class CWorld; 28 28 class CSimulation2; 29 29 class CGameView; 30 class C NetTurnManager;30 class CTurnManager; 31 31 class IReplayLogger; 32 32 struct CColor; 33 33 34 34 /** 35 35 * The container that holds the rules, resources and attributes of the game. … … class CGame 75 75 /** 76 76 * Differs from m_PlayerID if a defeated player or observer views another player. 77 77 */ 78 78 player_id_t m_ViewedPlayerID; 79 79 80 C NetTurnManager* m_TurnManager;80 CTurnManager* m_TurnManager; 81 81 82 82 public: 83 83 CGame(bool disableGraphics = false, bool replayLog = true); 84 84 ~CGame(); 85 85 … … public: 183 183 184 184 /** 185 185 * Replace the current turn manager. 186 186 * This class will take ownership of the pointer. 187 187 */ 188 void SetTurnManager(C NetTurnManager* turnManager);188 void SetTurnManager(CTurnManager* turnManager); 189 189 190 C NetTurnManager* GetTurnManager() const190 CTurnManager* GetTurnManager() const 191 191 { return m_TurnManager; } 192 192 193 193 IReplayLogger& GetReplayLogger() const 194 194 { return *m_ReplayLogger; } 195 195 -
source/simulation2/components/CCmpCommandQueue.cpp
1 /* Copyright (C) 201 0Wildfire Games.1 /* Copyright (C) 2016 Wildfire Games. 2 2 * This file is part of 0 A.D. 3 3 * 4 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 5 * it under the terms of the GNU General Public License as published by 6 6 * the Free Software Foundation, either version 2 of the License, or … … 21 21 #include "ICmpCommandQueue.h" 22 22 23 23 #include "ps/CLogger.h" 24 24 #include "ps/Game.h" 25 25 #include "ps/Profile.h" 26 #include " network/NetTurnManager.h"26 #include "simulation2/system/TurnManager.h" 27 27 28 28 class CCmpCommandQueue : public ICmpCommandQueue 29 29 { 30 30 public: 31 31 static void ClassInit(CComponentManager& UNUSED(componentManager)) … … public: 64 64 65 65 virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) 66 66 { 67 67 JSContext* cx = GetSimContext().GetScriptInterface().GetContext(); 68 68 JSAutoRequest rq(cx); 69 69 70 70 u32 numCmds; 71 71 deserialize.NumberU32_Unbounded("num commands", numCmds); 72 72 for (size_t i = 0; i < numCmds; ++i) 73 73 { 74 74 i32 player; … … public: 87 87 88 88 virtual void PostNetworkCommand(JS::HandleValue cmd1) 89 89 { 90 90 JSContext* cx = GetSimContext().GetScriptInterface().GetContext(); 91 91 JSAutoRequest rq(cx); 92 92 93 93 // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. 94 94 JS::RootedValue cmd(cx, cmd1.get()); 95 95 96 96 PROFILE2_EVENT("post net command"); 97 97 PROFILE2_ATTR("command: %s", GetSimContext().GetScriptInterface().StringifyJSON(&cmd, false).c_str()); … … public: 104 104 virtual void FlushTurn(const std::vector<SimulationCommand>& commands) 105 105 { 106 106 ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); 107 107 JSContext* cx = scriptInterface.GetContext(); 108 108 JSAutoRequest rq(cx); 109 109 110 110 JS::RootedValue global(cx, scriptInterface.GetGlobalObject()); 111 111 std::vector<SimulationCommand> localCommands; 112 112 m_LocalQueue.swap(localCommands); 113 113 114 114 for (size_t i = 0; i < localCommands.size(); ++i) -
source/simulation2/system/TurnManager.cpp
1 /* Copyright (C) 2016 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "precompiled.h" 19 20 #include "TurnManager.h" 21 22 #include "gui/GUIManager.h" 23 #include "maths/MathUtil.h" 24 #include "ps/CLogger.h" 25 #include "ps/Profile.h" 26 #include "ps/Pyrogenesis.h" 27 #include "ps/Replay.h" 28 #include "simulation2/Simulation2.h" 29 30 #include <sstream> 31 #include <fstream> 32 #include <iomanip> 33 34 const u32 DEFAULT_TURN_LENGTH_MP = 500; 35 const u32 DEFAULT_TURN_LENGTH_SP = 200; 36 const int COMMAND_DELAY = 2; 37 38 #if 0 39 #define TURN_LOG(args) debug_printf args 40 #else 41 #define TURN_LOG(args) 42 #endif 43 44 static std::string Hexify(const std::string& s) 45 { 46 std::stringstream str; 47 str << std::hex; 48 for (size_t i = 0; i < s.size(); ++i) 49 str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i]; 50 return str.str(); 51 } 52 53 CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay) : 54 m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength), m_DeltaSimTime(0), 55 m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay), 56 m_TimeWarpNumTurns(0), m_FinalTurn(std::numeric_limits<u32>::max()) 57 { 58 // When we are on turn n, we schedule new commands for n+2. 59 // We know that all other clients have finished scheduling commands for n (else we couldn't have got here). 60 // We know we have not yet finished scheduling commands for n+2. 61 // Hence other clients can be on turn n-1, n, n+1, and no other. 62 // So they can be sending us commands scheduled for n+1, n+2, n+3. 63 // So we need a 3-element buffer: 64 m_QueuedCommands.resize(COMMAND_DELAY + 1); 65 } 66 67 void CTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn) 68 { 69 m_CurrentTurn = newCurrentTurn; 70 m_ReadyTurn = newReadyTurn; 71 m_DeltaSimTime = 0; 72 size_t queuedCommandsSize = m_QueuedCommands.size(); 73 m_QueuedCommands.clear(); 74 m_QueuedCommands.resize(queuedCommandsSize); 75 } 76 77 void CTurnManager::SetPlayerID(int playerId) 78 { 79 m_PlayerId = playerId; 80 } 81 82 bool CTurnManager::WillUpdate(float simFrameLength) 83 { 84 // Keep this in sync with the return value of Update() 85 86 if (m_CurrentTurn > m_FinalTurn) 87 return false; 88 89 if (m_DeltaSimTime + simFrameLength < 0) 90 return false; 91 92 if (m_ReadyTurn <= m_CurrentTurn) 93 return false; 94 95 return true; 96 } 97 98 bool CTurnManager::Update(float simFrameLength, size_t maxTurns) 99 { 100 if (m_CurrentTurn > m_FinalTurn) 101 return false; 102 103 m_DeltaSimTime += simFrameLength; 104 105 // If the game becomes laggy, m_DeltaSimTime increases progressively. 106 // The engine will fast forward accordingly to catch up. 107 // To keep the game playable, stop fast forwarding after 2 turn lengths. 108 m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f); 109 110 // If we haven't reached the next turn yet, do nothing 111 if (m_DeltaSimTime < 0) 112 return false; 113 114 TURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn)); 115 116 // Check that the next turn is ready for execution 117 if (m_ReadyTurn <= m_CurrentTurn) 118 { 119 // Oops, we wanted to start the next turn but it's not ready yet - 120 // there must be too much network lag. 121 // TODO: complain to the user. 122 // TODO: send feedback to the server to increase the turn length. 123 124 // Reset the next-turn timer to 0 so we try again next update but 125 // so we don't rush to catch up in subsequent turns. 126 // TODO: we should do clever rate adjustment instead of just pausing like this. 127 m_DeltaSimTime = 0; 128 129 return false; 130 } 131 132 maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn 133 134 for (size_t i = 0; i < maxTurns; ++i) 135 { 136 // Check that we've reached the i'th next turn 137 if (m_DeltaSimTime < 0) 138 break; 139 140 // Check that the i'th next turn is still ready 141 if (m_ReadyTurn <= m_CurrentTurn) 142 break; 143 144 NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY); 145 146 // Increase now, so Update can send new commands for a subsequent turn 147 ++m_CurrentTurn; 148 149 // Clean up any destroyed entities since the last turn (e.g. placement previews 150 // or rally point flags generated by the GUI). (Must do this before the time warp 151 // serialization.) 152 m_Simulation2.FlushDestroyedEntities(); 153 154 // Save the current state for rewinding, if enabled 155 if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0) 156 { 157 PROFILE3("time warp serialization"); 158 std::stringstream stream; 159 m_Simulation2.SerializeState(stream); 160 m_TimeWarpStates.push_back(stream.str()); 161 } 162 163 // Put all the client commands into a single list, in a globally consistent order 164 std::vector<SimulationCommand> commands; 165 for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0]) 166 commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end())); 167 168 m_QueuedCommands.pop_front(); 169 m_QueuedCommands.resize(m_QueuedCommands.size() + 1); 170 171 m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands); 172 173 TURN_LOG((L"Running %d cmds\n", commands.size())); 174 175 m_Simulation2.Update(m_TurnLength, commands); 176 177 NotifyFinishedUpdate(m_CurrentTurn); 178 179 // Set the time for the next turn update 180 m_DeltaSimTime -= m_TurnLength / 1000.f; 181 } 182 183 return true; 184 } 185 186 bool CTurnManager::UpdateFastForward() 187 { 188 m_DeltaSimTime = 0; 189 190 TURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn)); 191 192 // Check that the next turn is ready for execution 193 if (m_ReadyTurn <= m_CurrentTurn) 194 return false; 195 196 while (m_ReadyTurn > m_CurrentTurn) 197 { 198 // TODO: It would be nice to remove some of the duplication with Update() 199 // (This is similar but doesn't call any Notify functions or update DeltaTime, 200 // it just updates the simulation state) 201 202 ++m_CurrentTurn; 203 204 m_Simulation2.FlushDestroyedEntities(); 205 206 // Put all the client commands into a single list, in a globally consistent order 207 std::vector<SimulationCommand> commands; 208 for (std::pair<const u32, std::vector<SimulationCommand>>& p : m_QueuedCommands[0]) 209 commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end())); 210 211 m_QueuedCommands.pop_front(); 212 m_QueuedCommands.resize(m_QueuedCommands.size() + 1); 213 214 m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands); 215 216 TURN_LOG((L"Running %d cmds\n", commands.size())); 217 218 m_Simulation2.Update(m_TurnLength, commands); 219 } 220 221 return true; 222 } 223 224 void CTurnManager::Interpolate(float simFrameLength, float realFrameLength) 225 { 226 // TODO: using m_TurnLength might be a bit dodgy when length changes - maybe 227 // we need to save the previous turn length? 228 229 float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0); 230 231 // Stop animations while still updating the selection highlight 232 if (m_CurrentTurn > m_FinalTurn) 233 simFrameLength = 0; 234 235 m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength); 236 } 237 238 void CTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn) 239 { 240 TURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn)); 241 242 if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1)) 243 { 244 debug_warn(L"Received command for invalid turn"); 245 return; 246 } 247 248 m_Simulation2.GetScriptInterface().FreezeObject(data, true); 249 m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, m_Simulation2.GetScriptInterface().GetContext(), data); 250 } 251 252 void CTurnManager::FinishedAllCommands(u32 turn, u32 turnLength) 253 { 254 TURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength)); 255 256 ENSURE(turn == m_ReadyTurn + 1); 257 m_ReadyTurn = turn; 258 m_TurnLength = turnLength; 259 } 260 261 bool CTurnManager::TurnNeedsFullHash(u32 turn) 262 { 263 // Check immediately for errors caused by e.g. inconsistent game versions 264 // (The hash is computed after the first sim update, so we start at turn == 1) 265 if (turn == 1) 266 return true; 267 268 // Otherwise check the full state every ~10 seconds in multiplayer games 269 // (TODO: should probably remove this when we're reasonably sure the game 270 // isn't too buggy, since the full hash is still pretty slow) 271 if (turn % 20 == 0) 272 return true; 273 274 return false; 275 } 276 277 void CTurnManager::EnableTimeWarpRecording(size_t numTurns) 278 { 279 m_TimeWarpStates.clear(); 280 m_TimeWarpNumTurns = numTurns; 281 } 282 283 void CTurnManager::RewindTimeWarp() 284 { 285 if (m_TimeWarpStates.empty()) 286 return; 287 288 std::stringstream stream(m_TimeWarpStates.back()); 289 m_Simulation2.DeserializeState(stream); 290 m_TimeWarpStates.pop_back(); 291 292 // Reset the turn manager state, so we won't execute stray commands and 293 // won't do the next snapshot until the appropriate time. 294 // (Ideally we ought to serialise the turn manager state and restore it 295 // here, but this is simpler for now.) 296 ResetState(0, 1); 297 } 298 299 void CTurnManager::QuickSave() 300 { 301 TIMER(L"QuickSave"); 302 303 std::stringstream stream; 304 if (!m_Simulation2.SerializeState(stream)) 305 { 306 LOGERROR("Failed to quicksave game"); 307 return; 308 } 309 310 m_QuickSaveState = stream.str(); 311 if (g_GUI) 312 m_QuickSaveMetadata = g_GUI->GetSavedGameData(); 313 else 314 m_QuickSaveMetadata = std::string(); 315 316 LOGMESSAGERENDER("Quicksaved game"); 317 318 } 319 320 void CTurnManager::QuickLoad() 321 { 322 TIMER(L"QuickLoad"); 323 324 if (m_QuickSaveState.empty()) 325 { 326 LOGERROR("Cannot quickload game - no game was quicksaved"); 327 return; 328 } 329 330 std::stringstream stream(m_QuickSaveState); 331 if (!m_Simulation2.DeserializeState(stream)) 332 { 333 LOGERROR("Failed to quickload game"); 334 return; 335 } 336 337 if (g_GUI && !m_QuickSaveMetadata.empty()) 338 g_GUI->RestoreSavedGameData(m_QuickSaveMetadata); 339 340 LOGMESSAGERENDER("Quickloaded game"); 341 342 // See RewindTimeWarp 343 ResetState(0, 1); 344 } 345 346 CLocalTurnManager::CLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) : 347 CTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay) 348 { 349 } 350 351 void CLocalTurnManager::PostCommand(JS::HandleValue data) 352 { 353 // Add directly to the next turn, ignoring COMMAND_DELAY, 354 // because we don't need to compensate for network latency 355 AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1); 356 } 357 358 void CLocalTurnManager::NotifyFinishedOwnCommands(u32 turn) 359 { 360 FinishedAllCommands(turn, m_TurnLength); 361 } 362 363 void CLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn)) 364 { 365 #if 0 // this hurts performance and is only useful for verifying log replays 366 std::string hash; 367 { 368 PROFILE3("state hash check"); 369 ENSURE(m_Simulation2.ComputeStateHash(hash)); 370 } 371 m_Replay.Hash(hash); 372 #endif 373 } 374 375 void CLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg)) 376 { 377 debug_warn(L"This should never be called"); 378 } 379 380 CReplayTurnManager::CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay) : 381 CLocalTurnManager(simulation, replay) 382 { 383 } 384 385 void CReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command) 386 { 387 // Using the pair we make sure that commands per turn will be processed in the correct order 388 m_ReplayCommands[turn].emplace_back(player, command); 389 } 390 391 void CReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick) 392 { 393 m_ReplayHash[turn] = std::make_pair(hash, quick); 394 } 395 396 void CReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength) 397 { 398 m_ReplayTurnLengths[turn] = turnLength; 399 400 // Initialize turn length 401 if (turn == 0) 402 m_TurnLength = m_ReplayTurnLengths[0]; 403 } 404 405 void CReplayTurnManager::StoreFinalReplayTurn(u32 turn) 406 { 407 m_FinalTurn = turn; 408 } 409 410 void CReplayTurnManager::OnSyncError(u32 turn) 411 { 412 m_HasSyncError = true; 413 414 std::stringstream msg; 415 msg << "Out of sync on turn " << turn << "\n\n" << "The current game state is different from the original game state."; 416 417 LOGERROR("%s", msg.str()); 418 419 if (g_GUI) 420 g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str())); 421 } 422 423 void CReplayTurnManager::NotifyFinishedUpdate(u32 turn) 424 { 425 if (turn == 1 && m_FinalTurn == 0) 426 g_GUI->SendEventToAll("ReplayFinished"); 427 428 if (turn > m_FinalTurn) 429 return; 430 431 DoTurn(turn); 432 433 // Compare hash if it exists in the replay and if we didn't have an OOS already 434 if (m_HasSyncError || m_ReplayHash.find(turn) == m_ReplayHash.end()) 435 return; 436 437 std::string expectedHash = m_ReplayHash[turn].first; 438 bool quickHash = m_ReplayHash[turn].second; 439 440 // Compute hash 441 std::string hash; 442 ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash)); 443 hash = Hexify(hash); 444 445 if (hash != expectedHash) 446 OnSyncError(turn); 447 } 448 449 void CReplayTurnManager::DoTurn(u32 turn) 450 { 451 debug_printf("Executing turn %u of %u\n", turn, m_FinalTurn); 452 453 m_TurnLength = m_ReplayTurnLengths[turn]; 454 455 // Simulate commands for that turn 456 for (const std::pair<player_id_t, std::string>& p : m_ReplayCommands[turn]) 457 { 458 JS::RootedValue command(m_Simulation2.GetScriptInterface().GetContext()); 459 m_Simulation2.GetScriptInterface().ParseJSON(p.second, &command); 460 AddCommand(m_ClientId, p.first, command, m_CurrentTurn + 1); 461 } 462 463 if (turn == m_FinalTurn) 464 g_GUI->SendEventToAll("ReplayFinished"); 465 } -
source/simulation2/system/TurnManager.h
1 /* Copyright (C) 2016 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #ifndef INCLUDED_TURNMANAGER 19 #define INCLUDED_TURNMANAGER 20 21 #include "ps/CStr.h" 22 #include "simulation2/helpers/SimulationCommand.h" 23 24 #include <list> 25 #include <map> 26 #include <vector> 27 28 extern const u32 DEFAULT_TURN_LENGTH_MP; 29 extern const u32 DEFAULT_TURN_LENGTH_SP; 30 extern const int COMMAND_DELAY; 31 32 class CSimulationMessage; 33 class CSimulation2; 34 class IReplayLogger; 35 36 /* 37 * This file deals with the logic of the turn system. The basic idea is as in 38 * http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.php?print=1 39 * 40 * Each player performs the simulation for turn N. 41 * User input is translated into commands scheduled for execution in turn N+2 which are 42 * distributed to all other clients. 43 * After a while, a player wants to perform the simulation for turn N+1, 44 * which first requires that it has all the other clients' commands for turn N+1. 45 * In that case, it does the simulation and tells all the other clients (via the server) 46 * it has finished sending commands for turn N+2, and it starts sending commands for turn N+3. 47 * 48 * Commands are redistributed immediately by the server. 49 * To ensure a consistent execution of commands, they are each associated with a 50 * client session ID (which is globally unique and consistent), which is used to sort them. 51 */ 52 53 /** 54 * Common turn system (used by clients and offline games). 55 */ 56 class CTurnManager 57 { 58 NONCOPYABLE(CTurnManager); 59 public: 60 /** 61 * Construct for a given session ID. 62 */ 63 CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay); 64 65 virtual ~CTurnManager() { } 66 67 void ResetState(u32 newCurrentTurn, u32 newReadyTurn); 68 69 /** 70 * Set the current user's player ID, which will be added into command messages. 71 */ 72 void SetPlayerID(int playerId); 73 74 /** 75 * Advance the simulation by a certain time. If this brings us past the current 76 * turn length, the next turns are processed and the function returns true. 77 * Otherwise, nothing happens and it returns false. 78 * 79 * @param simFrameLength Length of the previous frame, in simulation seconds 80 * @param maxTurns Maximum number of turns to simulate at once 81 */ 82 bool Update(float simFrameLength, size_t maxTurns); 83 84 /** 85 * Advance the simulation by as much as possible. Intended for catching up 86 * over a small number of turns when rejoining a multiplayer match. 87 * Returns true if it advanced by at least one turn. 88 */ 89 bool UpdateFastForward(); 90 91 /** 92 * Returns whether Update(simFrameLength, ...) will process at least one new turn. 93 * @param simFrameLength Length of the previous frame, in simulation seconds 94 */ 95 bool WillUpdate(float simFrameLength); 96 97 /** 98 * Advance the graphics by a certain time. 99 * @param simFrameLength Length of the previous frame, in simulation seconds 100 * @param realFrameLength Length of the previous frame, in real time seconds 101 */ 102 void Interpolate(float simFrameLength, float realFrameLength); 103 104 /** 105 * Called when a simulation message is received. 106 */ 107 virtual void OnSimulationMessage(CSimulationMessage* msg) = 0; 108 109 /** 110 * Called by simulation code, to add a new command to be distributed to all clients and executed soon. 111 */ 112 virtual void PostCommand(JS::HandleValue data) = 0; 113 114 /** 115 * Called when all commands for a given turn have been received. 116 * This allows Update to progress to that turn. 117 */ 118 void FinishedAllCommands(u32 turn, u32 turnLength); 119 120 /** 121 * Enables the recording of state snapshots every @p numTurns, 122 * which can be jumped back to via RewindTimeWarp(). 123 * If @p numTurns is 0 then recording is disabled. 124 */ 125 void EnableTimeWarpRecording(size_t numTurns); 126 127 /** 128 * Jumps back to the latest recorded state snapshot (if any). 129 */ 130 void RewindTimeWarp(); 131 132 void QuickSave(); 133 void QuickLoad(); 134 135 u32 GetCurrentTurn() { return m_CurrentTurn; } 136 137 protected: 138 /** 139 * Store a command to be executed at a given turn. 140 */ 141 void AddCommand(int client, int player, JS::HandleValue data, u32 turn); 142 143 /** 144 * Called when this client has finished sending all its commands scheduled for the given turn. 145 */ 146 virtual void NotifyFinishedOwnCommands(u32 turn) = 0; 147 148 /** 149 * Called when this client has finished a simulation update. 150 */ 151 virtual void NotifyFinishedUpdate(u32 turn) = 0; 152 153 /** 154 * Returns whether we should compute a complete state hash for the given turn, 155 * instead of a quick less-complete hash. 156 */ 157 bool TurnNeedsFullHash(u32 turn); 158 159 CSimulation2& m_Simulation2; 160 161 /// The turn that we have most recently executed 162 u32 m_CurrentTurn; 163 164 /// The latest turn for which we have received all commands from all clients 165 u32 m_ReadyTurn; 166 167 // Current turn length 168 u32 m_TurnLength; 169 170 /// Commands queued at each turn (index 0 is for m_CurrentTurn+1) 171 std::deque<std::map<u32, std::vector<SimulationCommand>>> m_QueuedCommands; 172 173 int m_PlayerId; 174 uint m_ClientId; 175 176 /// Simulation time remaining until we ought to execute the next turn (as a negative value to 177 /// add elapsed time increments to until we reach 0). 178 float m_DeltaSimTime; 179 180 bool m_HasSyncError; 181 182 IReplayLogger& m_Replay; 183 184 // The number of the last turn that is allowed to be executed (used for replays) 185 u32 m_FinalTurn; 186 187 private: 188 size_t m_TimeWarpNumTurns; // 0 if disabled 189 std::list<std::string> m_TimeWarpStates; 190 std::string m_QuickSaveState; // TODO: should implement a proper disk-based quicksave system 191 std::string m_QuickSaveMetadata; 192 }; 193 194 /** 195 * Implementation of CTurnManager for offline games. 196 */ 197 class CLocalTurnManager : public CTurnManager 198 { 199 public: 200 CLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay); 201 202 virtual void OnSimulationMessage(CSimulationMessage* msg); 203 204 virtual void PostCommand(JS::HandleValue data); 205 206 protected: 207 virtual void NotifyFinishedOwnCommands(u32 turn); 208 209 virtual void NotifyFinishedUpdate(u32 turn); 210 }; 211 212 /** 213 * Implementation of CTurnManager for replay games. 214 */ 215 class CReplayTurnManager : public CLocalTurnManager 216 { 217 public: 218 CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay); 219 220 void StoreReplayCommand(u32 turn, int player, const std::string& command); 221 222 void StoreReplayTurnLength(u32 turn, u32 turnLength); 223 224 void StoreReplayHash(u32 turn, const std::string& hash, bool quick); 225 226 void StoreFinalReplayTurn(u32 turn); 227 228 void OnSyncError(u32 turn); 229 230 protected: 231 virtual void NotifyFinishedUpdate(u32 turn); 232 233 void DoTurn(u32 turn); 234 235 // Contains the commands of every player on each turn 236 std::map<u32, std::vector<std::pair<player_id_t, std::string>>> m_ReplayCommands; 237 238 // Contains the length of every turn 239 std::map<u32, u32> m_ReplayTurnLengths; 240 241 // Contains all replay hash values and weather or not the quick hash method was used 242 std::map<u32, std::pair<std::string, bool>> m_ReplayHash; 243 }; 244 245 #endif // INCLUDED_TURNMANAGER