Visual replay of command files, patch by elexis.

Works with the command line argument
"-replay-visual=/path/to/commands.txt". It is not integrated to the main
menu GUI yet.

Refs #9.

This was SVN commit r16727.
This commit is contained in:
Nicolas Auvray 2015-06-06 08:45:49 +00:00
parent e9c27d4066
commit be93b31411
9 changed files with 309 additions and 32 deletions

View File

@ -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

View File

@ -22,6 +22,10 @@
onSimulationUpdate();
</action>
<action on="ReplayFinished">
onReplayFinished();
</action>
<action on="Press">
this.hidden = !this.hidden;
</action>

View File

@ -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"))
{

View File

@ -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)

View File

@ -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 <list>
#include <map>
#include <vector>
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<u32, std::map<int, std::vector<std::string> > > m_ReplayCommands;
// Contains the length of every turn
std::map<u32, u32> m_ReplayTurnLengths;
// Contains all replay hash values and weather or not the quick hash method was used
std::map<u32, std::pair<std::string, bool> > 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

View File

@ -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<CNetReplayTurnManager*>(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;

View File

@ -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 <vector>
#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;

View File

@ -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<ScriptInterface> pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();

View File

@ -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<SimulationCommand>& UNUSED(commands)) { }
virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector<SimulationCommand>& UNUSED(commands)) { }
virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { }
};