diff --git a/source/main.cpp b/source/main.cpp index 8dbbb5a1a4..fbe6ec0fb0 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -49,12 +49,14 @@ that of Atlas depending on commandline parameters. #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" +#include "ps/Replay.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" +#include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" @@ -407,6 +409,28 @@ static void RunGameOrAtlas(int argc, const char* argv[]) if(ran_atlas) return; + // run non-visual simulation replay if requested + if (args.Has("replay")) + { + g_DisableAudio = true; + + Paths paths(args); + g_VFS = CreateVfs(20 * MiB); + g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); + g_VFS->Mount(L"", paths.RData()/L"mods/public", VFS_MOUNT_MUST_EXIST); + + { + CReplayPlayer replay; + replay.Load(args.Get("replay")); + replay.Replay(); + } + + g_VFS.reset(); + + CXeromyces::Terminate(); + return; + } + const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 6bfb1bccc5..d88d991a92 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -353,7 +353,8 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event) if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; - client->m_ClientTurnManager = new CNetClientTurnManager(*client->m_Game->GetSimulation2(), *client, client->m_HostID); + client->m_ClientTurnManager = new CNetClientTurnManager( + *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(client->m_GameAttributes); diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp index d2a90ef325..75be939969 100644 --- a/source/network/NetTurnManager.cpp +++ b/source/network/NetTurnManager.cpp @@ -27,6 +27,8 @@ #include "maths/MathUtil.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" +#include "ps/Replay.h" +#include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include @@ -48,9 +50,9 @@ static std::string Hexify(const std::string& s) return str.str(); } -CNetTurnManager::CNetTurnManager(CSimulation2& simulation, int clientId) : +CNetTurnManager::CNetTurnManager(CSimulation2& simulation, int clientId, IReplayLogger& replay) : m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_DeltaTime(0), - m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false) + m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay) { // 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). @@ -94,6 +96,8 @@ bool CNetTurnManager::Update(float frameLength) m_QueuedCommands.pop_front(); m_QueuedCommands.resize(m_QueuedCommands.size() + 1); + m_Replay.Turn(m_CurrentTurn-1, TURN_LENGTH, commands); + #ifdef NETTURN_LOG NETTURN_LOG(L"Running %d cmds\n", commands.size()); #endif @@ -225,6 +229,8 @@ void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn) debug_assert(ok); } + m_Replay.Hash(hash); + // Send message to the server CSyncCheckMessage msg; msg.m_Turn = turn; diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h index 3f440c696d..1a7b11b360 100644 --- a/source/network/NetTurnManager.h +++ b/source/network/NetTurnManager.h @@ -26,6 +26,7 @@ class CNetServer; class CNetClient; class CSimulationMessage; class CSimulation2; +class IReplayLogger; /* * This file deals with the logic of the network turn system. The basic idea is as in @@ -54,7 +55,7 @@ public: /** * Construct for a given network session ID. */ - CNetTurnManager(CSimulation2& simulation, int clientId); + CNetTurnManager(CSimulation2& simulation, int clientId, IReplayLogger& replay); virtual ~CNetTurnManager() { } @@ -132,6 +133,8 @@ protected: float m_DeltaTime; bool m_HasSyncError; + + IReplayLogger& m_Replay; }; /** @@ -140,8 +143,8 @@ protected: class CNetClientTurnManager : public CNetTurnManager { public: - CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId) : - CNetTurnManager(simulation, clientId), m_NetClient(client) + CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) : + CNetTurnManager(simulation, clientId, replay), m_NetClient(client) { } @@ -163,8 +166,8 @@ protected: class CNetLocalTurnManager : public CNetTurnManager { public: - CNetLocalTurnManager(CSimulation2& simulation) : - CNetTurnManager(simulation, 0) + CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) : + CNetTurnManager(simulation, 0, replay) { } diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index 484555aee7..bba29d0fe8 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -37,6 +37,7 @@ #include "ps/Loader.h" #include "ps/Overlay.h" #include "ps/Profile.h" +#include "ps/Replay.h" #include "ps/World.h" #include "scripting/ScriptingHost.h" #include "scriptinterface/ScriptInterface.h" @@ -66,12 +67,15 @@ CGame::CGame(bool disableGraphics): m_SimRate(1.0f), m_PlayerID(-1) { + m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); + // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? + // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); - m_TurnManager = new CNetLocalTurnManager(*m_Simulation2); // this will get replaced if we're a net server/client + m_TurnManager = new CNetLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); m_Simulation2->ResetState(); @@ -94,6 +98,7 @@ CGame::~CGame() delete m_GameView; delete m_Simulation2; delete m_World; + delete m_ReplayLogger; } void CGame::SetTurnManager(CNetTurnManager* turnManager) @@ -179,6 +184,8 @@ void CGame::SetPlayerID(int playerID) void CGame::StartGame(const CScriptValRooted& attribs) { + m_ReplayLogger->StartGame(attribs); + RegisterInit(attribs); } diff --git a/source/ps/Game.h b/source/ps/Game.h index fcc8f59081..5d0f4cdd07 100644 --- a/source/ps/Game.h +++ b/source/ps/Game.h @@ -32,6 +32,7 @@ class CSimulation2; class CGameView; class CNetTurnManager; class CScriptValRooted; +class IReplayLogger; struct CColor; /** @@ -152,8 +153,12 @@ public: CNetTurnManager* GetTurnManager() const { return m_TurnManager; } + IReplayLogger& GetReplayLogger() const + { return *m_ReplayLogger; } + private: void RegisterInit(const CScriptValRooted& attribs); + IReplayLogger* m_ReplayLogger; }; extern CGame *g_Game; diff --git a/source/ps/Replay.cpp b/source/ps/Replay.cpp new file mode 100644 index 0000000000..62fbee00f0 --- /dev/null +++ b/source/ps/Replay.cpp @@ -0,0 +1,157 @@ +/* Copyright (C) 2010 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 "Replay.h" + +#include "lib/utf8.h" +#include "lib/file/file_system.h" +#include "ps/Game.h" +#include "ps/Loader.h" +#include "scriptinterface/ScriptInterface.h" +#include "simulation2/Simulation2.h" +#include "simulation2/helpers/SimulationCommand.h" + +static std::string Hexify(const std::string& s) +{ + std::stringstream str; + str << std::hex; + for (size_t i = 0; i < s.size(); ++i) + str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i]; + return str.str(); +} + +CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) : + m_ScriptInterface(scriptInterface) +{ + std::wstringstream name; + name << L"sim_log/" << getpid() << L"/commands.txt"; + fs::wpath path (psLogDir()/name.str()); + CreateDirectories(path.branch_path(), 0700); + m_Stream = new std::ofstream(path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); +} + +CReplayLogger::~CReplayLogger() +{ + delete m_Stream; +} + +void CReplayLogger::StartGame(const CScriptValRooted& attribs) +{ + *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs.get(), false) << "\n"; +} + +void CReplayLogger::Turn(u32 n, u32 turnLength, const std::vector& commands) +{ + *m_Stream << "turn " << n << " " << turnLength << "\n"; + for (size_t i = 0; i < commands.size(); ++i) + { + *m_Stream << "cmd " << commands[i].player << " " << m_ScriptInterface.StringifyJSON(commands[i].data.get(), false) << "\n"; + } + *m_Stream << "end\n"; + m_Stream->flush(); +} + +void CReplayLogger::Hash(const std::string& hash) +{ + *m_Stream << "hash " << Hexify(hash) << "\n"; +} + +//////////////////////////////////////////////////////////////// + +CReplayPlayer::CReplayPlayer() : + m_Stream(NULL) +{ +} + +CReplayPlayer::~CReplayPlayer() +{ + delete m_Stream; +} + +void CReplayPlayer::Load(const fs::path& path) +{ + debug_assert(!m_Stream); + + m_Stream = new std::ifstream(path.external_file_string().c_str()); + debug_assert(m_Stream->good()); +} + +void CReplayPlayer::Replay() +{ + debug_assert(m_Stream); + + CGame game(true); + g_Game = &game; + + std::vector commands; + u32 turnLength; + + std::string type; + while ((*m_Stream >> type).good()) + { + if (type == "start") + { + std::string line; + std::getline(*m_Stream, line); + std::wstring linew = wstring_from_utf8(line); + utf16string line16(linew.begin(), linew.end()); + CScriptValRooted attribs = game.GetSimulation2()->GetScriptInterface().ParseJSON(line16); + + game.StartGame(attribs); + LDR_NonprogressiveLoad(); + PSRETURN ret = game.ReallyStartGame(); + debug_assert(ret == PSRETURN_OK); + } + else if (type == "turn") + { + u32 turn; + *m_Stream >> turn >> turnLength; + printf("Turn %d (%d)... ", turn, turnLength); + } + else if (type == "cmd") + { + u32 player; + *m_Stream >> player; + + std::string line; + std::getline(*m_Stream, line); + std::wstring linew = wstring_from_utf8(line); + utf16string line16(linew.begin(), linew.end()); + CScriptValRooted data = game.GetSimulation2()->GetScriptInterface().ParseJSON(line16); + + SimulationCommand cmd = { player, data }; + commands.push_back(cmd); + } + else if (type == "end") + { + game.GetSimulation2()->Update(turnLength, commands); + commands.clear(); + + std::string hash; + bool ok = game.GetSimulation2()->ComputeStateHash(hash); + debug_assert(ok); + + printf("%s\n", Hexify(hash).c_str()); + } + else + { + printf("Unrecognised replay token %s\n", type.c_str()); + } + } +} diff --git a/source/ps/Replay.h b/source/ps/Replay.h new file mode 100644 index 0000000000..1606452f4f --- /dev/null +++ b/source/ps/Replay.h @@ -0,0 +1,96 @@ +/* Copyright (C) 2010 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 . + */ + +#ifndef INCLUDED_REPLAY +#define INCLUDED_REPLAY + +class CScriptValRooted; +struct SimulationCommand; +class ScriptInterface; + +/** + * Replay log recorder interface. + * Call its methods at appropriate times during the game. + */ +class IReplayLogger +{ +public: + IReplayLogger() { } + virtual ~IReplayLogger() { } + + /** + * Started the game with the given game attributes. + */ + virtual void StartGame(const CScriptValRooted& attribs) = 0; + + /** + * Run the given turn with the given collection of player commands. + */ + virtual void Turn(u32 n, u32 turnLength, const std::vector& commands) = 0; + + /** + * Optional hash of simulation state (for sync checking). + */ + virtual void Hash(const std::string& hash) = 0; +}; + +/** + * Implementation of IReplayLogger that simply throws away all data. + */ +class CDummyReplayLogger : public IReplayLogger +{ +public: + virtual void StartGame(const CScriptValRooted& UNUSED(attribs)) { } + virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), const std::vector& UNUSED(commands)) { } + virtual void Hash(const std::string& UNUSED(hash)) { } +}; + +/** + * Implementation of IReplayLogger that saves data to a file in the logs directory. + */ +class CReplayLogger : public IReplayLogger +{ +public: + CReplayLogger(ScriptInterface& scriptInterface); + ~CReplayLogger(); + + virtual void StartGame(const CScriptValRooted& attribs); + virtual void Turn(u32 n, u32 turnLength, const std::vector& commands); + virtual void Hash(const std::string& hash); + +private: + ScriptInterface& m_ScriptInterface; + std::ostream* m_Stream; +}; + +/** + * Replay log replayer. Runs the log with no graphics and dumps some info to stdout. + */ +class CReplayPlayer +{ +public: + CReplayPlayer(); + ~CReplayPlayer(); + + void Load(const fs::path& path); + void Replay(); + +private: + std::istream* m_Stream; +}; + +#endif // INCLUDED_REPLAY diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index bb7c550c82..0927b9511b 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -669,10 +669,10 @@ struct Stringifier std::stringstream stream; }; -std::string ScriptInterface::StringifyJSON(jsval obj) +std::string ScriptInterface::StringifyJSON(jsval obj, bool indent) { Stringifier str; - if (!JS_Stringify(m->m_cx, &obj, NULL, INT_TO_JSVAL(2), &Stringifier::callback, &str)) + if (!JS_Stringify(m->m_cx, &obj, NULL, indent ? INT_TO_JSVAL(2) : JSVAL_VOID, &Stringifier::callback, &str)) { LOGERROR(L"StringifyJSON failed"); return ""; diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index 599361de72..a300a3c65f 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/source/scriptinterface/ScriptInterface.h @@ -164,7 +164,7 @@ public: /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ - std::string StringifyJSON(jsval obj); + std::string StringifyJSON(jsval obj, bool indent = true); /** * Report the given error message through the JS error reporting mechanism,