diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 00f07723e9..9337a8ebe5 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -557,6 +557,16 @@ function onSimulationUpdate() } } +function onReplayFinished() +{ + closeMenu(); + closeOpenDialogs(); + pauseGame(); + var btCaptions = [translateWithContext("replayFinished", "Yes"), translateWithContext("replayFinished", "No")]; + var btCode = [leaveGame, resumeGame]; + messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished","Confirmation"), 0, btCaptions, btCode); +} + /** * updates a status bar on the GUI * nameOfBar: name of the bar diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml index e5aed0bc8e..622759b518 100644 --- a/binaries/data/mods/public/gui/session/session.xml +++ b/binaries/data/mods/public/gui/session/session.xml @@ -22,6 +22,10 @@ onSimulationUpdate(); + + onReplayFinished(); + + this.hidden = !this.hidden; diff --git a/source/main.cpp b/source/main.cpp index b9c1444f70..68d03589c4 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -442,6 +442,12 @@ static void RunGameOrAtlas(int argc, const char* argv[]) // run non-visual simulation replay if requested if (args.Has("replay")) { + std::string replayFile = args.Get("replay"); + if (!FileExists(OsPath(replayFile))) + { + debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.c_str()); + return; + } Paths paths(args); g_VFS = CreateVfs(20 * MiB); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); @@ -449,7 +455,7 @@ static void RunGameOrAtlas(int argc, const char* argv[]) { CReplayPlayer replay; - replay.Load(args.Get("replay")); + replay.Load(replayFile); replay.Replay(args.Has("serializationtest"), args.Has("ooslog")); } @@ -459,6 +465,17 @@ static void RunGameOrAtlas(int argc, const char* argv[]) return; } + // If visual replay file does not exist, quit before starting the renderer + if (args.Has("replay-visual")) + { + std::string replayFile = args.Get("replay-visual"); + if (!FileExists(OsPath(replayFile))) + { + debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.c_str()); + return; + } + } + // run in archive-building mode if requested if (args.Has("archivebuild")) { diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp index 5d5e44d88b..7e389e7cbc 100644 --- a/source/network/NetTurnManager.cpp +++ b/source/network/NetTurnManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2015 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -225,26 +225,49 @@ void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash) // Only complain the first time if (m_HasSyncError) return; - m_HasSyncError = true; bool quick = !TurnNeedsFullHash(turn); std::string hash; - bool ok = m_Simulation2.ComputeStateHash(hash, quick); - ENSURE(ok); + ENSURE(m_Simulation2.ComputeStateHash(hash, quick)); OsPath path = psLogDir()/"oos_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); m_Simulation2.DumpDebugState(file); file.close(); + hash = Hexify(hash); + const std::string& expectedHashHex = Hexify(expectedHash); + + DisplayOOSError(turn, hash, expectedHashHex, false, &path); +} + +void CNetTurnManager::DisplayOOSError(u32 turn, std::string& hash, const std::string& expectedHash, const bool isReplay, OsPath* path = NULL) +{ + m_HasSyncError = true; + std::stringstream msg; - msg << "Out of sync on turn " << turn << ": expected hash " << Hexify(expectedHash) << "\n\n"; - msg << "Current state: turn " << m_CurrentTurn << ", hash " << Hexify(hash) << "\n\n"; - msg << "Dumping current state to " << utf8_from_wstring(path.string()); + msg << "Out of sync on turn " << turn << ": expected hash " << expectedHash << "\n"; + + if (expectedHash != hash || m_CurrentTurn != turn) + msg << "\nCurrent state: turn " << m_CurrentTurn << ", hash " << hash << "\n\n"; + + if (isReplay) + msg << "\nThe current game state is different from the original game state.\n\n"; + else + { + if (expectedHash == hash) + msg << "Your game state is identical to the hosts game state.\n\n"; + else + msg << "Your game state is different from the hosts game state.\n\n"; + } + + if (path) + msg << "Dumping current state to " << utf8_from_wstring(OsPath(*path).string()); + + LOGERROR("%s", msg.str()); + if (g_GUI) g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str())); - else - LOGERROR("%s", msg.str()); } void CNetTurnManager::Interpolate(float simFrameLength, float realFrameLength) @@ -322,8 +345,7 @@ void CNetTurnManager::QuickSave() TIMER(L"QuickSave"); std::stringstream stream; - bool ok = m_Simulation2.SerializeState(stream); - if (!ok) + if (!m_Simulation2.SerializeState(stream)) { LOGERROR("Failed to quicksave game"); return; @@ -350,8 +372,7 @@ void CNetTurnManager::QuickLoad() } std::stringstream stream(m_QuickSaveState); - bool ok = m_Simulation2.DeserializeState(stream); - if (!ok) + if (!m_Simulation2.DeserializeState(stream)) { LOGERROR("Failed to quickload game"); return; @@ -402,8 +423,7 @@ void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn) std::string hash; { PROFILE3("state hash check"); - bool ok = m_Simulation2.ComputeStateHash(hash, quick); - ENSURE(ok); + ENSURE(m_Simulation2.ComputeStateHash(hash, quick)); } NETTURN_LOG((L"NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str())); @@ -452,8 +472,7 @@ void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn)) std::string hash; { PROFILE3("state hash check"); - bool ok = m_Simulation2.ComputeStateHash(hash); - ENSURE(ok); + ENSURE(m_Simulation2.ComputeStateHash(hash)); } m_Replay.Hash(hash); #endif @@ -464,8 +483,75 @@ void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg)) debug_warn(L"This should never be called"); } +CNetReplayTurnManager::CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay) : + CNetLocalTurnManager(simulation, replay) +{ +} +void CNetReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command) +{ + m_ReplayCommands[turn][player].push_back(command); +} +void CNetReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick) +{ + m_ReplayHash[turn] = std::make_pair(hash, quick); +} + +void CNetReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength) +{ + m_ReplayTurnLengths[turn] = turnLength; + + // Initialize turn length + if (turn == 0) + m_TurnLength = m_ReplayTurnLengths[0]; +} + +void CNetReplayTurnManager::StoreFinalReplayTurn(u32 turn) +{ + m_FinalReplayTurn = turn; +} + +void CNetReplayTurnManager::NotifyFinishedUpdate(u32 turn) +{ + if (turn > m_FinalReplayTurn) + return; + + debug_printf("Executing turn %d of %d\n", turn, m_FinalReplayTurn); + DoTurn(turn); + + // Compare hash if it exists in the replay and if we didn't have an oos already + if (m_HasSyncError || m_ReplayHash.find(turn) == m_ReplayHash.end()) + return; + + std::string expectedHash = m_ReplayHash[turn].first; + bool quickHash = m_ReplayHash[turn].second; + + // Compute hash + std::string hash; + ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash)); + hash = Hexify(hash); + + if (hash != expectedHash) + DisplayOOSError(turn, hash, expectedHash, true); +} + +void CNetReplayTurnManager::DoTurn(u32 turn) +{ + // Save turn length + m_TurnLength = m_ReplayTurnLengths[turn]; + + // Simulate commands for that turn + for (auto& command : m_ReplayCommands[turn]) + { + for (size_t i = 0; i < command.second.size(); ++i) + { + JS::RootedValue data(m_Simulation2.GetScriptInterface().GetContext()); + m_Simulation2.GetScriptInterface().ParseJSON(command.second[i], &data); + AddCommand(m_ClientId, command.first, data, m_CurrentTurn + 1); + } + } +} CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) : m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP) diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h index 60faa3c521..3de52105f1 100644 --- a/source/network/NetTurnManager.h +++ b/source/network/NetTurnManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2015 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,9 +19,11 @@ #define INCLUDED_NETTURNMANAGER #include "simulation2/helpers/SimulationCommand.h" +#include "lib/os_path.h" #include #include +#include class CNetServerWorker; class CNetClient; @@ -107,6 +109,11 @@ public: */ virtual void OnSyncError(u32 turn, const std::string& expectedHash); + /** + * Shows a message box when an out of sync error has been detected in the session or visual replay. + */ + virtual void DisplayOOSError(u32 turn, std::string& hash, const std::string& expectedHash, const bool isReplay, OsPath* path); + /** * Called by simulation code, to add a new command to be distributed to all clients and executed soon. */ @@ -189,6 +196,7 @@ private: std::string m_QuickSaveMetadata; }; + /** * Implementation of CNetTurnManager for network clients. */ @@ -233,6 +241,42 @@ protected: }; + +/** + * Implementation of CNetTurnManager for replay games. + */ +class CNetReplayTurnManager : public CNetLocalTurnManager +{ +public: + CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay); + + void StoreReplayCommand(u32 turn, int player, const std::string& command); + + void StoreReplayTurnLength(u32 turn, u32 turnLength); + + void StoreReplayHash(u32 turn, const std::string& hash, bool quick); + + void StoreFinalReplayTurn(u32 turn); + + +protected: + virtual void NotifyFinishedUpdate(u32 turn); + + void DoTurn(u32 turn); + + // Contains the commands of every player on each turn + std::map > > m_ReplayCommands; + + // Contains the length of every turn + std::map m_ReplayTurnLengths; + + // Contains all replay hash values and weather or not the quick hash method was used + std::map > m_ReplayHash; + + // The number of the last turn in the replay + u32 m_FinalReplayTurn; + +}; /** * The server-side counterpart to CNetClientTurnManager. * Records the turn state of each client, and sends turn advancement messages diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index 753521809b..a311079d2f 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -63,7 +63,7 @@ CGame *g_Game=NULL; * Constructor * **/ -CGame::CGame(bool disableGraphics): +CGame::CGame(bool disableGraphics, bool replayLog): m_World(new CWorld(this)), m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptRuntime, m_World->GetTerrain())), m_GameView(disableGraphics ? NULL : new CGameView(this)), @@ -71,10 +71,15 @@ CGame::CGame(bool disableGraphics): m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1), - m_IsSavedGame(false) + m_IsSavedGame(false), + m_IsReplay(false), + m_ReplayStream(NULL) { - m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? + if (replayLog) + m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); + else + m_ReplayLogger = new CDummyReplayLogger(); // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. @@ -101,6 +106,7 @@ CGame::~CGame() delete m_Simulation2; delete m_World; delete m_ReplayLogger; + delete m_ReplayStream; } void CGame::SetTurnManager(CNetTurnManager* turnManager) @@ -114,6 +120,76 @@ void CGame::SetTurnManager(CNetTurnManager* turnManager) m_TurnManager->SetPlayerID(m_PlayerID); } +int CGame::LoadReplayData() +{ + ENSURE(m_IsReplay); + ENSURE(!m_ReplayPath.empty()); + + CNetReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager()); + + u32 currentTurn = 0; + std::string type; + while ((*m_ReplayStream >> type).good()) + { + if (type == "turn") + { + u32 turn = 0; + u32 turnLength = 0; + *m_ReplayStream >> turn >> turnLength; + ENSURE(turn == currentTurn); + replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); + } + else if (type == "cmd") + { + player_id_t player; + *m_ReplayStream >> player; + + std::string line; + std::getline(*m_ReplayStream, line); + replayTurnMgr->StoreReplayCommand(currentTurn, player, line); + } + else if (type == "hash" || type == "hash-quick") + { + bool quick = (type == "hash-quick"); + std::string replayHash; + *m_ReplayStream >> replayHash; + replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick); + } + else if (type == "end") + { + currentTurn++; + } + else + { + CancelLoad(L"Failed to load replay data (unrecognized content)"); + } + } + m_FinalReplayTurn = currentTurn; + replayTurnMgr->StoreFinalReplayTurn(currentTurn); + return 0; +} + +bool CGame::StartReplay(const std::string& replayPath) +{ + m_IsReplay = true; + ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); + + SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger())); + + m_ReplayPath = replayPath; + m_ReplayStream = new std::ifstream(m_ReplayPath.c_str()); + + std::string type; + ENSURE((*m_ReplayStream >> type).good() && type == "start"); + + std::string line; + std::getline(*m_ReplayStream, line); + JS::RootedValue attribs(scriptInterface.GetContext()); + scriptInterface.ParseJSON(line, &attribs); + StartGame(&attribs, ""); + + return true; +} /** * Initializes the game with the set of attributes provided. @@ -176,6 +252,9 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved if (m_IsSavedGame) RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000); + if (m_IsReplay) + RegMemFun(this, &CGame::LoadReplayData, L"Loading replay data", 1000); + LDR_EndRegistering(); } @@ -263,7 +342,7 @@ int CGame::GetPlayerID() return m_PlayerID; } -void CGame::SetPlayerID(int playerID) +void CGame::SetPlayerID(player_id_t playerID) { m_PlayerID = playerID; if (m_TurnManager) @@ -272,7 +351,9 @@ void CGame::SetPlayerID(int playerID) void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { - m_ReplayLogger->StartGame(attribs); + if (m_ReplayLogger != false) + m_ReplayLogger->StartGame(attribs); + RegisterInit(attribs, savedState); } @@ -310,6 +391,8 @@ bool CGame::Update(const double deltaRealTime, bool doInterpolate) PROFILE3("gui sim update"); g_GUI->SendEventToAll("SimulationUpdate"); } + if (m_IsReplay && m_TurnManager->GetCurrentTurn() == m_FinalReplayTurn - 1) + g_GUI->SendEventToAll("ReplayFinished"); GetView()->GetLOSTexture().MakeDirty(); } @@ -362,7 +445,7 @@ void CGame::CachePlayerColors() } -CColor CGame::GetPlayerColor(int player) const +CColor CGame::GetPlayerColor(player_id_t player) const { if (player < 0 || player >= (int)m_PlayerColors.size()) return BrokenColor; diff --git a/source/ps/Game.h b/source/ps/Game.h index 2509d0168d..0971def8d9 100644 --- a/source/ps/Game.h +++ b/source/ps/Game.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2015 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,6 +22,7 @@ #include #include "scriptinterface/ScriptVal.h" +#include "simulation2/helpers/Player.h" class CWorld; class CSimulation2; @@ -60,12 +61,12 @@ class CGame **/ float m_SimRate; - int m_PlayerID; + player_id_t m_PlayerID; CNetTurnManager* m_TurnManager; public: - CGame(bool disableGraphics = false); + CGame(bool disableGraphics = false, bool replayLog = true); ~CGame(); /** @@ -76,6 +77,8 @@ public: void StartGame(JS::MutableHandleValue attribs, const std::string& savedState); PSRETURN ReallyStartGame(); + bool StartReplay(const std::string& replayPath); + /** * Periodic heartbeat that controls the process. performs all per-frame updates. * Simulation update is called and game status update is called. @@ -90,7 +93,7 @@ public: void Interpolate(float simFrameLength, float realFrameLength); int GetPlayerID(); - void SetPlayerID(int playerID); + void SetPlayerID(player_id_t playerID); /** * Retrieving player colors from scripts is slow, so this updates an @@ -100,7 +103,7 @@ public: */ void CachePlayerColors(); - CColor GetPlayerColor(int player) const; + CColor GetPlayerColor(player_id_t player) const; /** * Get m_GameStarted. @@ -171,6 +174,12 @@ private: int LoadInitialState(); std::string m_InitialSavedState; // valid between RegisterInit and LoadInitialState bool m_IsSavedGame; // true if loading a saved game; false for a new game + + int LoadReplayData(); + std::string m_ReplayPath; + bool m_IsReplay; + std::istream* m_ReplayStream; + u32 m_FinalReplayTurn; }; extern CGame *g_Game; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index a764e821de..b276fef4a5 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -881,6 +881,9 @@ void EarlyInit() bool Autostart(const CmdLineArgs& args); +// Returns true if and only if the user has intended to replay a file +bool VisualReplay(const std::string replayFile); + bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); @@ -1073,7 +1076,7 @@ void InitGraphics(const CmdLineArgs& args, int flags) try { - if (!Autostart(args)) + if (!VisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup @@ -1470,6 +1473,27 @@ bool Autostart(const CmdLineArgs& args) return true; } +bool VisualReplay(const std::string replayFile) +{ + if (!FileExists(OsPath(replayFile))) + return false; + + g_Game = new CGame(false, false); + g_Game->SetPlayerID(-1); + g_Game->StartReplay(replayFile); + + // TODO: Non progressive load can fail - need a decent way to handle this + LDR_NonprogressiveLoad(); + + PSRETURN ret = g_Game->ReallyStartGame(); + ENSURE(ret == PSRETURN_OK); + + ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); + + InitPs(true, L"page_session.xml", &scriptInterface, JS::UndefinedHandleValue); + return true; +} + void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); diff --git a/source/ps/Replay.h b/source/ps/Replay.h index 01d6e5cc12..21fe627750 100644 --- a/source/ps/Replay.h +++ b/source/ps/Replay.h @@ -56,7 +56,7 @@ class CDummyReplayLogger : public IReplayLogger { public: virtual void StartGame(JS::MutableHandleValue UNUSED(attribs)) { } - virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), const std::vector& UNUSED(commands)) { } + virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector& UNUSED(commands)) { } virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { } };