diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg
index f1e91b94f1..0c3f91e106 100644
--- a/binaries/data/config/default.cfg
+++ b/binaries/data/config/default.cfg
@@ -157,6 +157,8 @@ hotkey.session.batchtrain = Shift ; Modifier to train units in batches
hotkey.session.deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
hotkey.session.rotate.cw = RightBracket ; Rotate building placement preview clockwise
hotkey.session.rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
+hotkey.timewarp.fastforward = Space
+hotkey.timewarp.rewind = Backspace
; > OVERLAY KEYS
hotkey.fps.toggle = "Shift+F" ; Toggle frame counter
diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index 1ada8d6599..8c492acbc8 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -577,6 +577,17 @@ function handleInputBeforeGui(ev, hoveredObject)
function handleInputAfterGui(ev)
{
+ // Handle the time-warp testing features, restricted to single-player
+ if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked)
+ {
+ if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward")
+ Engine.SetSimRate(20.0);
+ else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward")
+ Engine.SetSimRate(1.0);
+ else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind")
+ Engine.RewindTimeWarp();
+ }
+
// State-machine processing:
switch (inputState)
diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml
index 8c5da9470c..39018cdee5 100644
--- a/binaries/data/mods/public/gui/session/session.xml
+++ b/binaries/data/mods/public/gui/session/session.xml
@@ -98,7 +98,7 @@
/>
-
diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
index d03c039152..e5f69c4acf 100644
--- a/source/gui/scripting/ScriptFunctions.cpp
+++ b/source/gui/scripting/ScriptFunctions.cpp
@@ -386,6 +386,16 @@ void DumpSimState(void* UNUSED(cbdata))
g_Game->GetSimulation2()->DumpDebugState(file);
}
+void EnableTimeWarpRecording(void* UNUSED(cbdata), unsigned int numTurns)
+{
+ g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns);
+}
+
+void RewindTimeWarp(void* UNUSED(cbdata))
+{
+ g_Game->GetTurnManager()->RewindTimeWarp();
+}
+
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
@@ -438,4 +448,6 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction("DebugWarn");
scriptInterface.RegisterFunction("ForceGC");
scriptInterface.RegisterFunction("DumpSimState");
+ scriptInterface.RegisterFunction("EnableTimeWarpRecording");
+ scriptInterface.RegisterFunction("RewindTimeWarp");
}
diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp
index 4013647dfa..065c7e4c80 100644
--- a/source/network/NetTurnManager.cpp
+++ b/source/network/NetTurnManager.cpp
@@ -73,7 +73,7 @@ void CNetTurnManager::SetPlayerID(int playerId)
m_PlayerId = playerId;
}
-bool CNetTurnManager::Update(float frameLength)
+bool CNetTurnManager::Update(float frameLength, size_t maxTurns)
{
m_DeltaTime += frameLength;
@@ -84,12 +84,46 @@ bool CNetTurnManager::Update(float frameLength)
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)
+ 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_DeltaTime = 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_DeltaTime < 0)
+ break;
+
+ // Check that the i'th next turn is still ready
+ if (m_ReadyTurn <= m_CurrentTurn)
+ break;
+
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
m_CurrentTurn += 1; // increase the turn number now, so Update can send new commands for a subsequent turn
+ // Save the current state for rewinding, if enabled
+ if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
+ {
+ PROFILE("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::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it)
@@ -109,23 +143,9 @@ bool CNetTurnManager::Update(float frameLength)
// Set the time for the next turn update
m_DeltaTime -= m_TurnLength / 1000.f;
-
- return true;
}
- else
- {
- // 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_DeltaTime = 0;
-
- return false;
- }
+ return true;
}
void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash)
@@ -187,6 +207,34 @@ void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
m_TurnLength = turnLength;
}
+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.)
+ m_CurrentTurn = 0;
+ m_ReadyTurn = 1;
+ m_DeltaTime = 0;
+ size_t queuedCommandsSize = m_QueuedCommands.size();
+ m_QueuedCommands.clear();
+ m_QueuedCommands.resize(queuedCommandsSize);
+}
+
+
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) :
CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h
index 153d0cf95b..a4a4866bff 100644
--- a/source/network/NetTurnManager.h
+++ b/source/network/NetTurnManager.h
@@ -66,11 +66,12 @@ public:
/**
* Advance the simulation by a certain time. If this brings us past the current
- * turn length, the next turn is processed and the function returns true.
+ * turn length, the next turns are processed and the function returns true.
* Otherwise, nothing happens and it returns false.
* @param frameLength length of the previous frame in seconds
+ * @param maxTurns maximum number of turns to simulate at once
*/
- bool Update(float frameLength);
+ bool Update(float frameLength, size_t maxTurns);
/**
* Advance the graphics by a certain time.
@@ -99,6 +100,18 @@ public:
*/
void FinishedAllCommands(u32 turn, u32 turnLength);
+ /**
+ * Enables the recording of state snapshots every @p numTurns,
+ * which can be jumped back to via RewindTimeWarp().
+ * If @p numTurns is 0 then recording is disabled.
+ */
+ void EnableTimeWarpRecording(size_t numTurns);
+
+ /**
+ * Jumps back to the latest recorded state snapshot (if any).
+ */
+ void RewindTimeWarp();
+
protected:
/**
* Store a command to be executed at a given turn.
@@ -138,6 +151,10 @@ protected:
bool m_HasSyncError;
IReplayLogger& m_Replay;
+
+private:
+ size_t m_TimeWarpNumTurns; // 0 if disabled
+ std::list m_TimeWarpStates;
};
/**
diff --git a/source/network/tests/test_Net.h b/source/network/tests/test_Net.h
index 8758f2b576..9090bb8d0c 100644
--- a/source/network/tests/test_Net.h
+++ b/source/network/tests/test_Net.h
@@ -186,13 +186,13 @@ public:
}
wait(clients, 100);
- client1Game.GetTurnManager()->Update(1.0f);
- client2Game.GetTurnManager()->Update(1.0f);
- client3Game.GetTurnManager()->Update(1.0f);
+ client1Game.GetTurnManager()->Update(1.0f, 1);
+ client2Game.GetTurnManager()->Update(1.0f, 1);
+ client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
- client1Game.GetTurnManager()->Update(1.0f);
- client2Game.GetTurnManager()->Update(1.0f);
- client3Game.GetTurnManager()->Update(1.0f);
+ client1Game.GetTurnManager()->Update(1.0f, 1);
+ client2Game.GetTurnManager()->Update(1.0f, 1);
+ client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
}
};
diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp
index 43cdbade47..b7b3f2f354 100644
--- a/source/ps/Game.cpp
+++ b/source/ps/Game.cpp
@@ -229,8 +229,14 @@ bool CGame::Update(double deltaTime, bool doInterpolate)
bool ok = true;
if (deltaTime)
{
+ // At the normal sim rate, we currently want to render at least one
+ // frame per simulation turn, so let maxTurns be 1. But for fast-forward
+ // sim rates we want to allow more, so it's not bounded by framerate,
+ // so just use the sim rate itself as the number of turns per frame.
+ size_t maxTurns = (size_t)m_SimRate;
+
PROFILE("update");
- if (m_TurnManager->Update(deltaTime))
+ if (m_TurnManager->Update(deltaTime, maxTurns))
g_GUI->SendEventToAll("SimulationUpdate");
}