Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "Replay.h"
21 :
22 : #include "graphics/TerrainTextureManager.h"
23 : #include "lib/timer.h"
24 : #include "lib/file/file_system.h"
25 : #include "lib/tex/tex.h"
26 : #include "ps/CLogger.h"
27 : #include "ps/Game.h"
28 : #include "ps/GameSetup/GameSetup.h"
29 : #include "ps/GameSetup/CmdLineArgs.h"
30 : #include "ps/GameSetup/Paths.h"
31 : #include "ps/Loader.h"
32 : #include "ps/Mod.h"
33 : #include "ps/Profile.h"
34 : #include "ps/ProfileViewer.h"
35 : #include "ps/Pyrogenesis.h"
36 : #include "ps/Mod.h"
37 : #include "ps/Util.h"
38 : #include "ps/VisualReplay.h"
39 : #include "scriptinterface/FunctionWrapper.h"
40 : #include "scriptinterface/Object.h"
41 : #include "scriptinterface/ScriptContext.h"
42 : #include "scriptinterface/ScriptInterface.h"
43 : #include "scriptinterface/ScriptRequest.h"
44 : #include "scriptinterface/ScriptStats.h"
45 : #include "scriptinterface/JSON.h"
46 : #include "simulation2/components/ICmpGuiInterface.h"
47 : #include "simulation2/helpers/Player.h"
48 : #include "simulation2/helpers/SimulationCommand.h"
49 : #include "simulation2/Simulation2.h"
50 : #include "simulation2/system/CmpPtr.h"
51 :
52 : #include <ctime>
53 : #include <fstream>
54 :
55 : /**
56 : * Number of turns between two saved profiler snapshots.
57 : * Keep in sync with source/tools/replayprofile/graph.js
58 : */
59 : static const int PROFILE_TURN_INTERVAL = 20;
60 :
61 0 : CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) :
62 0 : m_ScriptInterface(scriptInterface), m_Stream(NULL)
63 : {
64 0 : }
65 :
66 0 : CReplayLogger::~CReplayLogger()
67 : {
68 0 : delete m_Stream;
69 0 : }
70 :
71 0 : void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
72 : {
73 0 : ScriptRequest rq(m_ScriptInterface);
74 :
75 : // Add timestamp, since the file-modification-date can change
76 0 : Script::SetProperty(rq, attribs, "timestamp", (double)std::time(nullptr));
77 :
78 : // Add engine version and currently loaded mods for sanity checks when replaying
79 0 : Script::SetProperty(rq, attribs, "engine_version", engine_version);
80 0 : JS::RootedValue mods(rq.cx);
81 0 : Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData());
82 0 : Script::SetProperty(rq, attribs, "mods", mods);
83 :
84 0 : m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath());
85 0 : debug_printf("FILES| Replay written to '%s'\n", m_Directory.string8().c_str());
86 :
87 0 : m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
88 0 : *m_Stream << "start " << Script::StringifyJSON(rq, attribs, false) << "\n";
89 0 : }
90 :
91 0 : void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
92 : {
93 0 : ScriptRequest rq(m_ScriptInterface);
94 :
95 0 : *m_Stream << "turn " << n << " " << turnLength << "\n";
96 :
97 0 : for (SimulationCommand& command : commands)
98 0 : *m_Stream << "cmd " << command.player << " " << Script::StringifyJSON(rq, &command.data, false) << "\n";
99 :
100 0 : *m_Stream << "end\n";
101 0 : m_Stream->flush();
102 0 : }
103 :
104 0 : void CReplayLogger::Hash(const std::string& hash, bool quick)
105 : {
106 0 : if (quick)
107 0 : *m_Stream << "hash-quick " << Hexify(hash) << "\n";
108 : else
109 0 : *m_Stream << "hash " << Hexify(hash) << "\n";
110 0 : }
111 :
112 0 : void CReplayLogger::SaveMetadata(const CSimulation2& simulation)
113 : {
114 0 : CmpPtr<ICmpGuiInterface> cmpGuiInterface(simulation, SYSTEM_ENTITY);
115 0 : if (!cmpGuiInterface)
116 : {
117 0 : LOGERROR("Could not save replay metadata!");
118 0 : return;
119 : }
120 :
121 0 : ScriptInterface& scriptInterface = simulation.GetScriptInterface();
122 0 : ScriptRequest rq(scriptInterface);
123 :
124 0 : JS::RootedValue arg(rq.cx);
125 0 : JS::RootedValue metadata(rq.cx);
126 0 : cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetReplayMetadata", arg, &metadata);
127 :
128 0 : const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
129 0 : CreateDirectories(fileName.Parent(), 0700);
130 :
131 0 : std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
132 0 : if (stream)
133 : {
134 0 : stream << Script::StringifyJSON(rq, &metadata, false);
135 0 : stream.close();
136 0 : debug_printf("FILES| Replay metadata written to '%s'\n", fileName.string8().c_str());
137 : }
138 : else
139 0 : debug_printf("FILES| Failed to write replay metadata to '%s'\n", fileName.string8().c_str());
140 :
141 : }
142 :
143 0 : OsPath CReplayLogger::GetDirectory() const
144 : {
145 0 : return m_Directory;
146 : }
147 :
148 : ////////////////////////////////////////////////////////////////
149 :
150 0 : CReplayPlayer::CReplayPlayer() :
151 0 : m_Stream(NULL)
152 : {
153 0 : }
154 :
155 0 : CReplayPlayer::~CReplayPlayer()
156 : {
157 0 : delete m_Stream;
158 0 : }
159 :
160 0 : void CReplayPlayer::Load(const OsPath& path)
161 : {
162 0 : ENSURE(!m_Stream);
163 :
164 0 : m_Stream = new std::ifstream(OsString(path).c_str());
165 0 : ENSURE(m_Stream->good());
166 0 : }
167 :
168 : namespace
169 : {
170 0 : CStr ModListToString(const std::vector<const Mod::ModData*>& list)
171 : {
172 0 : CStr text;
173 0 : for (const Mod::ModData* data : list)
174 0 : text += data->m_Pathname + " (" + data->m_Version + ")\n";
175 0 : return text;
176 : }
177 :
178 0 : void CheckReplayMods(const std::vector<Mod::ModData>& replayMods)
179 : {
180 0 : std::vector<const Mod::ModData*> replayData;
181 0 : replayData.reserve(replayMods.size());
182 0 : for (const Mod::ModData& data : replayMods)
183 0 : replayData.push_back(&data);
184 0 : if (!Mod::AreModsPlayCompatible(g_Mods.GetEnabledModsData(), replayData))
185 0 : LOGWARNING("Incompatible replay mods detected.\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s",
186 : ModListToString(replayData), ModListToString(g_Mods.GetEnabledModsData()));
187 0 : }
188 : } // anonymous namespace
189 :
190 0 : void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick)
191 : {
192 0 : ENSURE(m_Stream);
193 :
194 0 : new CProfileViewer;
195 0 : new CProfileManager;
196 0 : g_ScriptStatsTable = new CScriptStatsTable;
197 0 : g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
198 :
199 0 : const int contextSize = 384 * 1024 * 1024;
200 0 : const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
201 0 : g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger);
202 :
203 0 : std::vector<SimulationCommand> commands;
204 0 : u32 turn = 0;
205 0 : u32 turnLength = 0;
206 :
207 : {
208 0 : std::string type;
209 :
210 0 : while ((*m_Stream >> type).good())
211 : {
212 0 : if (type == "start")
213 : {
214 0 : std::string attribsStr;
215 : {
216 0 : ScriptInterface scriptInterface("Engine", "Replay", g_ScriptContext);
217 0 : ScriptRequest rq(scriptInterface);
218 0 : std::getline(*m_Stream, attribsStr);
219 0 : JS::RootedValue attribs(rq.cx);
220 0 : if (!Script::ParseJSON(rq, attribsStr, &attribs))
221 : {
222 0 : LOGERROR("Error parsing JSON attributes: %s", attribsStr);
223 : // TODO: do something cleverer than crashing.
224 0 : ENSURE(false);
225 : }
226 :
227 : // Load the mods specified in the replay.
228 0 : std::vector<Mod::ModData> replayMods;
229 0 : if (!Script::GetProperty(rq, attribs, "mods", replayMods))
230 : {
231 0 : LOGERROR("Could not get replay mod information.");
232 : // TODO: do something cleverer than crashing.
233 0 : ENSURE(false);
234 : }
235 :
236 0 : std::vector<CStr> mods;
237 0 : for (const Mod::ModData& data : replayMods)
238 0 : mods.emplace_back(data.m_Pathname);
239 :
240 : // Ignore the return value, we check below.
241 0 : g_Mods.UpdateAvailableMods(scriptInterface);
242 0 : g_Mods.EnableMods(mods, false);
243 0 : CheckReplayMods(replayMods);
244 :
245 0 : MountMods(Paths(g_CmdLineArgs), g_Mods.GetEnabledMods());
246 : }
247 :
248 0 : g_Game = new CGame(false);
249 0 : if (serializationtest)
250 0 : g_Game->GetSimulation2()->EnableSerializationTest();
251 0 : if (rejointestturn >= 0)
252 0 : g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn);
253 0 : if (ooslog)
254 0 : g_Game->GetSimulation2()->EnableOOSLog();
255 :
256 0 : ScriptRequest rq(g_Game->GetSimulation2()->GetScriptInterface());
257 0 : JS::RootedValue attribs(rq.cx);
258 0 : ENSURE(Script::ParseJSON(rq, attribsStr, &attribs));
259 0 : g_Game->StartGame(&attribs, "");
260 :
261 : // TODO: Non progressive load can fail - need a decent way to handle this
262 0 : LDR_NonprogressiveLoad();
263 :
264 0 : PSRETURN ret = g_Game->ReallyStartGame();
265 0 : ENSURE(ret == PSRETURN_OK);
266 : }
267 0 : else if (type == "turn")
268 : {
269 0 : *m_Stream >> turn >> turnLength;
270 0 : debug_printf("Turn %u (%u)...\n", turn, turnLength);
271 : }
272 0 : else if (type == "cmd")
273 : {
274 : player_id_t player;
275 0 : *m_Stream >> player;
276 :
277 0 : std::string line;
278 0 : std::getline(*m_Stream, line);
279 0 : ScriptRequest rq(g_Game->GetSimulation2()->GetScriptInterface());
280 0 : JS::RootedValue data(rq.cx);
281 0 : Script::ParseJSON(rq, line, &data);
282 0 : Script::FreezeObject(rq, data, true);
283 0 : commands.emplace_back(SimulationCommand(player, rq.cx, data));
284 : }
285 0 : else if (type == "hash" || type == "hash-quick")
286 : {
287 0 : std::string replayHash;
288 0 : *m_Stream >> replayHash;
289 0 : TestHash(type, replayHash, testHashFull, testHashQuick);
290 : }
291 0 : else if (type == "end")
292 : {
293 : {
294 0 : g_Profiler2.RecordFrameStart();
295 0 : PROFILE2("frame");
296 0 : g_Profiler2.IncrementFrameNumber();
297 0 : PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
298 :
299 0 : g_Game->GetSimulation2()->Update(turnLength, commands);
300 0 : commands.clear();
301 : }
302 :
303 0 : g_Profiler.Frame();
304 :
305 0 : if (turn % PROFILE_TURN_INTERVAL == 0)
306 0 : g_ProfileViewer.SaveToFile();
307 : }
308 : else
309 0 : debug_printf("Unrecognised replay token %s\n", type.c_str());
310 : }
311 : }
312 :
313 0 : SAFE_DELETE(m_Stream);
314 :
315 0 : g_Profiler2.SaveToFile();
316 :
317 0 : std::string hash;
318 0 : bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
319 0 : ENSURE(ok);
320 0 : debug_printf("# Final state: %s\n", Hexify(hash).c_str());
321 0 : timer_DisplayClientTotals();
322 :
323 0 : SAFE_DELETE(g_Game);
324 :
325 : // Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when
326 : // it's already destructed.
327 0 : g_ScriptContext.reset();
328 :
329 0 : delete &g_Profiler;
330 0 : delete &g_ProfileViewer;
331 0 : SAFE_DELETE(g_ScriptStatsTable);
332 0 : }
333 :
334 0 : void CReplayPlayer::TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick)
335 : {
336 0 : bool quick = (hashType == "hash-quick");
337 0 : if ((quick && !testHashQuick) || (!quick && !testHashFull))
338 0 : return;
339 :
340 0 : std::string hash;
341 0 : ENSURE(g_Game->GetSimulation2()->ComputeStateHash(hash, quick));
342 :
343 0 : std::string hexHash = Hexify(hash);
344 :
345 0 : if (hexHash == replayHash)
346 0 : debug_printf("%s ok (%s)\n", hashType.c_str(), hexHash.c_str());
347 : else
348 0 : debug_printf("%s MISMATCH (%s != %s)\n", hashType.c_str(), hexHash.c_str(), replayHash.c_str());
349 3 : }
|