Index: ps/trunk/source/simulation2/system/TurnManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/TurnManager.cpp (revision 25015) +++ ps/trunk/source/simulation2/system/TurnManager.cpp (revision 25016) @@ -1,367 +1,367 @@ /* Copyright (C) 2020 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/Profile.h" #include "ps/CLogger.h" #include "ps/Replay.h" #include "ps/Util.h" #include "scriptinterface/ScriptExtraHeaders.h" // StructuredClone #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #if 0 #define NETTURN_LOG(...) debug_printf(__VA_ARGS__) #else #define NETTURN_LOG(...) #endif /** * Maximum number of turns between two clients. * When we are on turn n, we schedule new commands for n+COMMAND_DELAY. * We know that all other clients have finished scheduling commands for n, * else we couldn't have got here, which means they're at least on turn n-COMMAND_DELAY+1. * We know we have not yet finished scheduling commands for n+COMMAND_DELAY, so no client can be there. * Hence other clients can be on turns [n-COMMAND_DELAY+1, ..., n+COMMAND_DELAY-1], and no other, * hence any two clients can only be this many turns apart. */ constexpr int MaxClientTurnDelta(int commandDelay) { return 2 * (commandDelay - 1); } const CStr CTurnManager::EventNameSavegameLoaded = "SavegameLoaded"; CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, u32 commandDelay, int clientId, IReplayLogger& replay) : m_Simulation2(simulation), m_CurrentTurn(0), m_CommandDelay(commandDelay), m_ReadyTurn(commandDelay - 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), m_QuickSaveMetadata(m_Simulation2.GetScriptInterface().GetGeneralJSContext()) { // Lag between any two clients is bounded. Add 1 for inclusive bounds. m_QueuedCommands.resize(MaxClientTurnDelta(m_CommandDelay) + 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 && m_CommandDelay > 1) { // 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 && m_CommandDelay > 1) break; NotifyFinishedOwnCommands(m_CurrentTurn + m_CommandDelay); // 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 current=%d, ready=%d)\n", client, player, turn, m_CurrentTurn, m_ReadyTurn); // Reject commands for turns that we should not be able to compute (in the past or too far future). if (m_CurrentTurn >= turn || turn > m_CurrentTurn + MaxClientTurnDelta(m_CommandDelay) + 1) { debug_warn(L"Received command for invalid turn"); return; } m_Simulation2.GetScriptInterface().FreezeObject(data, true); ScriptRequest rq(m_Simulation2.GetScriptInterface()); m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, rq.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); + ResetState(1, m_CommandDelay); } void CTurnManager::QuickSave(JS::HandleValue GUIMetadata) { TIMER(L"QuickSave"); std::stringstream stream; if (!m_Simulation2.SerializeState(stream)) { LOGERROR("Failed to quicksave game"); return; } m_QuickSaveState = stream.str(); ScriptRequest rq(m_Simulation2.GetScriptInterface()); if (JS_StructuredClone(rq.cx, GUIMetadata, &m_QuickSaveMetadata, nullptr, nullptr)) { // Freeze state to ensure that consectuvie loads don't modify the state m_Simulation2.GetScriptInterface().FreezeObject(m_QuickSaveMetadata, true); } else { LOGERROR("Could not copy savegame GUI metadata"); m_QuickSaveMetadata = JS::UndefinedValue(); } 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; } // See RewindTimeWarp - ResetState(0, 1); + ResetState(1, m_CommandDelay); if (!g_GUI) return; ScriptRequest rq(m_Simulation2.GetScriptInterface()); // Provide a copy, so that GUI components don't have to clone to get mutable objects JS::RootedValue quickSaveMetadataClone(rq.cx); if (!JS_StructuredClone(rq.cx, m_QuickSaveMetadata, &quickSaveMetadataClone, nullptr, nullptr)) { LOGERROR("Failed to clone quicksave state!"); return; } JS::RootedValueArray<1> paramData(rq.cx); paramData[0].set(quickSaveMetadataClone); g_GUI->SendEventToAll(EventNameSavegameLoaded, paramData); LOGMESSAGERENDER("Quickloaded game"); }