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