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 "Simulation2.h"
21 :
22 : #include "scriptinterface/FunctionWrapper.h"
23 : #include "scriptinterface/ScriptContext.h"
24 : #include "scriptinterface/ScriptInterface.h"
25 : #include "scriptinterface/JSON.h"
26 : #include "scriptinterface/StructuredClone.h"
27 :
28 : #include "simulation2/MessageTypes.h"
29 : #include "simulation2/system/ComponentManager.h"
30 : #include "simulation2/system/ParamNode.h"
31 : #include "simulation2/system/SimContext.h"
32 : #include "simulation2/components/ICmpAIManager.h"
33 : #include "simulation2/components/ICmpCommandQueue.h"
34 : #include "simulation2/components/ICmpTemplateManager.h"
35 :
36 : #include "graphics/MapReader.h"
37 : #include "graphics/Terrain.h"
38 : #include "lib/timer.h"
39 : #include "lib/file/vfs/vfs_util.h"
40 : #include "maths/MathUtil.h"
41 : #include "ps/CLogger.h"
42 : #include "ps/ConfigDB.h"
43 : #include "ps/Filesystem.h"
44 : #include "ps/Loader.h"
45 : #include "ps/Profile.h"
46 : #include "ps/Pyrogenesis.h"
47 : #include "ps/Util.h"
48 : #include "ps/XML/Xeromyces.h"
49 :
50 : #include <fstream>
51 : #include <iomanip>
52 : #include <memory>
53 :
54 : class CSimulation2Impl
55 : {
56 : public:
57 4 : CSimulation2Impl(CUnitManager* unitManager, std::shared_ptr<ScriptContext> cx, CTerrain* terrain) :
58 : m_SimContext(), m_ComponentManager(m_SimContext, cx),
59 : m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false),
60 4 : m_MapSettings(cx->GetGeneralJSContext()), m_InitAttributes(cx->GetGeneralJSContext())
61 : {
62 4 : m_SimContext.m_UnitManager = unitManager;
63 4 : m_SimContext.m_Terrain = terrain;
64 4 : m_ComponentManager.LoadComponentTypes();
65 :
66 4 : RegisterFileReloadFunc(ReloadChangedFileCB, this);
67 :
68 : // Tests won't have config initialised
69 4 : if (CConfigDB::IsInitialised())
70 : {
71 0 : CFG_GET_VAL("ooslog", m_EnableOOSLog);
72 0 : CFG_GET_VAL("serializationtest", m_EnableSerializationTest);
73 0 : CFG_GET_VAL("rejointest", m_RejoinTestTurn);
74 0 : if (m_RejoinTestTurn < 0) // Handle bogus values of the arg
75 0 : m_RejoinTestTurn = -1;
76 : }
77 :
78 4 : if (m_EnableOOSLog)
79 : {
80 0 : m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs");
81 0 : debug_printf("Writing ooslogs to %s\n", m_OOSLogPath.string8().c_str());
82 : }
83 4 : }
84 :
85 4 : ~CSimulation2Impl()
86 4 : {
87 4 : UnregisterFileReloadFunc(ReloadChangedFileCB, this);
88 4 : }
89 :
90 3 : void ResetState(bool skipScriptedComponents, bool skipAI)
91 : {
92 3 : m_DeltaTime = 0.0;
93 3 : m_LastFrameOffset = 0.0f;
94 3 : m_TurnNumber = 0;
95 3 : ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI);
96 3 : }
97 :
98 3 : static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI)
99 : {
100 3 : componentManager.ResetState();
101 3 : componentManager.InitSystemEntity();
102 3 : componentManager.AddSystemComponents(skipScriptedComponents, skipAI);
103 3 : }
104 :
105 : static bool LoadDefaultScripts(CComponentManager& componentManager, std::set<VfsPath>* loadedScripts);
106 : static bool LoadScripts(CComponentManager& componentManager, std::set<VfsPath>* loadedScripts, const VfsPath& path);
107 : static bool LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set<VfsPath>* loadedScripts);
108 : Status ReloadChangedFile(const VfsPath& path);
109 :
110 0 : static Status ReloadChangedFileCB(void* param, const VfsPath& path)
111 : {
112 0 : return static_cast<CSimulation2Impl*>(param)->ReloadChangedFile(path);
113 : }
114 :
115 : int ProgressiveLoad();
116 : void Update(int turnLength, const std::vector<SimulationCommand>& commands);
117 : static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector<SimulationCommand>& commands);
118 : void Interpolate(float simFrameLength, float frameOffset, float realFrameLength);
119 :
120 : void DumpState();
121 :
122 : CSimContext m_SimContext;
123 : CComponentManager m_ComponentManager;
124 : double m_DeltaTime;
125 : float m_LastFrameOffset;
126 :
127 : std::string m_StartupScript;
128 : JS::PersistentRootedValue m_InitAttributes;
129 : JS::PersistentRootedValue m_MapSettings;
130 :
131 : std::set<VfsPath> m_LoadedScripts;
132 :
133 : uint32_t m_TurnNumber;
134 :
135 : bool m_EnableOOSLog;
136 : OsPath m_OOSLogPath;
137 :
138 : // Functions and data for the serialization test mode: (see Update() for relevant comments)
139 :
140 : bool m_EnableSerializationTest;
141 : int m_RejoinTestTurn;
142 : bool m_TestingRejoin;
143 :
144 : // Secondary simulation (NB: order matters for destruction).
145 : std::unique_ptr<CComponentManager> m_SecondaryComponentManager;
146 : std::unique_ptr<CTerrain> m_SecondaryTerrain;
147 : std::unique_ptr<CSimContext> m_SecondaryContext;
148 : std::unique_ptr<std::set<VfsPath>> m_SecondaryLoadedScripts;
149 :
150 0 : struct SerializationTestState
151 : {
152 : std::stringstream state;
153 : std::stringstream debug;
154 : std::string hash;
155 : };
156 :
157 : void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix);
158 :
159 : void ReportSerializationFailure(
160 : SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter,
161 : SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter);
162 :
163 : void InitRNGSeedSimulation();
164 : void InitRNGSeedAI();
165 :
166 0 : static std::vector<SimulationCommand> CloneCommandsFromOtherCompartment(const ScriptInterface& newScript, const ScriptInterface& oldScript,
167 : const std::vector<SimulationCommand>& commands)
168 : {
169 0 : std::vector<SimulationCommand> newCommands;
170 0 : newCommands.reserve(commands.size());
171 :
172 0 : ScriptRequest rqNew(newScript);
173 0 : for (const SimulationCommand& command : commands)
174 : {
175 0 : JS::RootedValue tmpCommand(rqNew.cx, Script::CloneValueFromOtherCompartment(newScript, oldScript, command.data));
176 0 : Script::FreezeObject(rqNew, tmpCommand, true);
177 0 : SimulationCommand cmd(command.player, rqNew.cx, tmpCommand);
178 0 : newCommands.emplace_back(std::move(cmd));
179 : }
180 0 : return newCommands;
181 : }
182 : };
183 :
184 0 : bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set<VfsPath>* loadedScripts)
185 : {
186 : return (
187 0 : LoadScripts(componentManager, loadedScripts, L"simulation/components/interfaces/") &&
188 0 : LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") &&
189 0 : LoadScripts(componentManager, loadedScripts, L"simulation/components/")
190 0 : );
191 : }
192 :
193 3 : bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set<VfsPath>* loadedScripts, const VfsPath& path)
194 : {
195 6 : VfsPaths pathnames;
196 3 : if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0)
197 0 : return false;
198 :
199 3 : bool ok = true;
200 6 : for (const VfsPath& scriptPath : pathnames)
201 : {
202 3 : if (loadedScripts)
203 3 : loadedScripts->insert(scriptPath);
204 3 : LOGMESSAGE("Loading simulation script '%s'", scriptPath.string8());
205 3 : if (!componentManager.LoadScript(scriptPath))
206 0 : ok = false;
207 : }
208 3 : return ok;
209 : }
210 :
211 0 : bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set<VfsPath>* loadedScripts)
212 : {
213 0 : bool ok = true;
214 0 : ScriptRequest rq(componentManager.GetScriptInterface());
215 0 : if (Script::HasProperty(rq, mapSettings, "TriggerScripts"))
216 : {
217 0 : std::vector<std::string> scriptNames;
218 0 : Script::GetProperty(rq, mapSettings, "TriggerScripts", scriptNames);
219 0 : for (const std::string& triggerScript : scriptNames)
220 : {
221 0 : std::string scriptName = "maps/" + triggerScript;
222 0 : if (loadedScripts)
223 : {
224 0 : if (loadedScripts->find(scriptName) != loadedScripts->end())
225 0 : continue;
226 0 : loadedScripts->insert(scriptName);
227 : }
228 0 : LOGMESSAGE("Loading trigger script '%s'", scriptName.c_str());
229 0 : if (!componentManager.LoadScript(scriptName.data()))
230 0 : ok = false;
231 : }
232 : }
233 0 : return ok;
234 : }
235 :
236 5 : Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path)
237 : {
238 : // Ignore if this file wasn't loaded as a script
239 : // (TODO: Maybe we ought to load in any new .js files that are created in the right directories)
240 5 : if (m_LoadedScripts.find(path) == m_LoadedScripts.end())
241 3 : return INFO::OK;
242 :
243 : // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message.
244 : // (Also don't bother trying to 'unload' it from the component manager, because that's not possible)
245 2 : if (!VfsFileExists(path))
246 0 : return INFO::OK;
247 :
248 2 : LOGMESSAGE("Reloading simulation script '%s'", path.string8());
249 2 : if (!m_ComponentManager.LoadScript(path, true))
250 0 : return ERR::FAIL;
251 :
252 2 : return INFO::OK;
253 : }
254 :
255 0 : int CSimulation2Impl::ProgressiveLoad()
256 : {
257 : // yield after this time is reached. balances increased progress bar
258 : // smoothness vs. slowing down loading.
259 0 : const double end_time = timer_Time() + 200e-3;
260 :
261 : int ret;
262 :
263 0 : do
264 : {
265 0 : bool progressed = false;
266 0 : int total = 0;
267 0 : int progress = 0;
268 :
269 0 : CMessageProgressiveLoad msg(&progressed, &total, &progress);
270 :
271 0 : m_ComponentManager.BroadcastMessage(msg);
272 :
273 0 : if (!progressed || total == 0)
274 0 : return 0; // we have nothing left to load
275 :
276 0 : ret = Clamp(100*progress / total, 1, 100);
277 : }
278 0 : while (timer_Time() < end_time);
279 :
280 0 : return ret;
281 : }
282 :
283 0 : void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix)
284 : {
285 0 : if (!state.hash.empty())
286 : {
287 0 : std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc);
288 0 : file << Hexify(state.hash);
289 : }
290 :
291 0 : if (!state.debug.str().empty())
292 : {
293 0 : std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc);
294 0 : file << state.debug.str();
295 : }
296 :
297 0 : if (!state.state.str().empty())
298 : {
299 0 : std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
300 0 : file << state.state.str();
301 : }
302 0 : }
303 :
304 0 : void CSimulation2Impl::ReportSerializationFailure(
305 : SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter,
306 : SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter)
307 : {
308 0 : const OsPath path = createDateIndexSubdirectory(psLogDir() / "serializationtest");
309 0 : debug_printf("Writing serializationtest-data to %s\n", path.string8().c_str());
310 :
311 : // Clean up obsolete files from previous runs
312 0 : wunlink(path / "hash.before.a");
313 0 : wunlink(path / "hash.before.b");
314 0 : wunlink(path / "debug.before.a");
315 0 : wunlink(path / "debug.before.b");
316 0 : wunlink(path / "state.before.a");
317 0 : wunlink(path / "state.before.b");
318 0 : wunlink(path / "hash.after.a");
319 0 : wunlink(path / "hash.after.b");
320 0 : wunlink(path / "debug.after.a");
321 0 : wunlink(path / "debug.after.b");
322 0 : wunlink(path / "state.after.a");
323 0 : wunlink(path / "state.after.b");
324 :
325 0 : if (primaryStateBefore)
326 0 : DumpSerializationTestState(*primaryStateBefore, path, L"before.a");
327 0 : if (primaryStateAfter)
328 0 : DumpSerializationTestState(*primaryStateAfter, path, L"after.a");
329 0 : if (secondaryStateBefore)
330 0 : DumpSerializationTestState(*secondaryStateBefore, path, L"before.b");
331 0 : if (secondaryStateAfter)
332 0 : DumpSerializationTestState(*secondaryStateAfter, path, L"after.b");
333 :
334 0 : debug_warn(L"Serialization test failure");
335 0 : }
336 :
337 0 : void CSimulation2Impl::InitRNGSeedSimulation()
338 : {
339 0 : u32 seed = 0;
340 0 : ScriptRequest rq(m_ComponentManager.GetScriptInterface());
341 0 : if (!Script::HasProperty(rq, m_MapSettings, "Seed") ||
342 0 : !Script::GetProperty(rq, m_MapSettings, "Seed", seed))
343 0 : LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed);
344 :
345 0 : m_ComponentManager.SetRNGSeed(seed);
346 0 : }
347 :
348 0 : void CSimulation2Impl::InitRNGSeedAI()
349 : {
350 0 : u32 seed = 0;
351 0 : ScriptRequest rq(m_ComponentManager.GetScriptInterface());
352 0 : if (!Script::HasProperty(rq, m_MapSettings, "AISeed") ||
353 0 : !Script::GetProperty(rq, m_MapSettings, "AISeed", seed))
354 0 : LOGWARNING("CSimulation2Impl::InitRNGSeedAI: No seed value specified - using %d", seed);
355 :
356 0 : CmpPtr<ICmpAIManager> cmpAIManager(m_SimContext, SYSTEM_ENTITY);
357 0 : if (cmpAIManager)
358 0 : cmpAIManager->SetRNGSeed(seed);
359 0 : }
360 :
361 0 : void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationCommand>& commands)
362 : {
363 0 : PROFILE3("sim update");
364 0 : PROFILE2_ATTR("turn %d", (int)m_TurnNumber);
365 :
366 0 : fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
367 :
368 : /*
369 : * In serialization test mode, we save the original (primary) simulation state before each turn update.
370 : * We run the update, then load the saved state into a secondary context.
371 : * We serialize that again and compare to the original serialization (to check that
372 : * serialize->deserialize->serialize is equivalent to serialize).
373 : * Then we run the update on the secondary context, and check that its new serialized
374 : * state matches the primary context after the update (to check that the simulation doesn't depend
375 : * on anything that's not serialized).
376 : *
377 : * In rejoin test mode, the secondary simulation is initialized from serialized data at turn N, then both
378 : * simulations run independantly while comparing their states each turn. This is way faster than a
379 : * complete serialization test and allows us to reproduce OOSes on rejoin.
380 : */
381 :
382 0 : const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow)
383 0 : const bool serializationTestHash = true; // set true to save and compare hash of state
384 :
385 0 : SerializationTestState primaryStateBefore;
386 0 : const ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface();
387 :
388 0 : const bool startRejoinTest = (int64_t) m_RejoinTestTurn == m_TurnNumber;
389 0 : if (startRejoinTest)
390 0 : m_TestingRejoin = true;
391 :
392 0 : if (m_EnableSerializationTest || m_TestingRejoin)
393 : {
394 0 : ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state));
395 : if (serializationTestDebugDump)
396 : ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false));
397 : if (serializationTestHash)
398 0 : ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false));
399 : }
400 :
401 0 : UpdateComponents(m_SimContext, turnLengthFixed, commands);
402 :
403 0 : if (m_EnableSerializationTest || startRejoinTest)
404 : {
405 0 : if (startRejoinTest)
406 0 : debug_printf("Initializing the secondary simulation\n");
407 :
408 0 : m_SecondaryTerrain = std::make_unique<CTerrain>();
409 :
410 0 : m_SecondaryContext = std::make_unique<CSimContext>();
411 0 : m_SecondaryContext->m_Terrain = m_SecondaryTerrain.get();
412 :
413 0 : m_SecondaryComponentManager = std::make_unique<CComponentManager>(*m_SecondaryContext, scriptInterface.GetContext());
414 0 : m_SecondaryComponentManager->LoadComponentTypes();
415 :
416 0 : m_SecondaryLoadedScripts = std::make_unique<std::set<VfsPath>>();
417 0 : ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts.get()));
418 0 : ResetComponentState(*m_SecondaryComponentManager, false, false);
419 :
420 0 : ScriptRequest rq(scriptInterface);
421 :
422 : // Load the trigger scripts after we have loaded the simulation.
423 : {
424 0 : ScriptRequest rq2(m_SecondaryComponentManager->GetScriptInterface());
425 0 : JS::RootedValue mapSettingsCloned(rq2.cx, Script::CloneValueFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, m_MapSettings));
426 0 : ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts.get()));
427 : }
428 :
429 : // Load the map into the secondary simulation
430 :
431 0 : LDR_BeginRegistering();
432 0 : std::unique_ptr<CMapReader> mapReader = std::make_unique<CMapReader>();
433 :
434 0 : std::string mapType;
435 0 : Script::GetProperty(rq, m_InitAttributes, "mapType", mapType);
436 0 : if (mapType == "random")
437 : {
438 : // TODO: support random map scripts
439 0 : debug_warn(L"Serialization test mode does not support random maps");
440 : }
441 : else
442 : {
443 0 : std::wstring mapFile;
444 0 : Script::GetProperty(rq, m_InitAttributes, "map", mapFile);
445 :
446 0 : VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp");
447 0 : mapReader->LoadMap(mapfilename, *scriptInterface.GetContext(), JS::UndefinedHandleValue,
448 : m_SecondaryTerrain.get(), NULL, NULL, NULL, NULL, NULL, NULL,
449 0 : NULL, NULL, m_SecondaryContext.get(), INVALID_PLAYER, true); // throws exception on failure
450 : }
451 :
452 0 : LDR_EndRegistering();
453 0 : ENSURE(LDR_NonprogressiveLoad() == INFO::OK);
454 0 : ENSURE(m_SecondaryComponentManager->DeserializeState(primaryStateBefore.state));
455 : }
456 :
457 0 : if (m_EnableSerializationTest || m_TestingRejoin)
458 : {
459 0 : SerializationTestState secondaryStateBefore;
460 0 : ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateBefore.state));
461 : if (serializationTestDebugDump)
462 : ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateBefore.debug, false));
463 : if (serializationTestHash)
464 0 : ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateBefore.hash, false));
465 :
466 0 : if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() ||
467 0 : primaryStateBefore.hash != secondaryStateBefore.hash)
468 : {
469 0 : ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL);
470 : }
471 :
472 0 : SerializationTestState primaryStateAfter;
473 0 : ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state));
474 : if (serializationTestHash)
475 0 : ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false));
476 :
477 0 : UpdateComponents(*m_SecondaryContext, turnLengthFixed,
478 0 : CloneCommandsFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, commands));
479 0 : SerializationTestState secondaryStateAfter;
480 0 : ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateAfter.state));
481 : if (serializationTestHash)
482 0 : ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateAfter.hash, false));
483 :
484 0 : if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() ||
485 0 : primaryStateAfter.hash != secondaryStateAfter.hash)
486 : {
487 : // Only do the (slow) dumping now we know we're going to need to report it
488 0 : ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false));
489 0 : ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateAfter.debug, false));
490 :
491 0 : ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter);
492 : }
493 : }
494 :
495 : // Run the GC occasionally
496 : // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are
497 : // much more turns in the same time than in normal games.
498 : // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code.
499 : // Based on testing, this seems to be a good compromise between memory usage and performance.
500 : // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic:
501 : // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323
502 : //
503 : // (TODO: we ought to schedule this for a frame where we're not
504 : // running the sim update, to spread the load)
505 0 : if (m_TurnNumber % 500 == 0)
506 0 : scriptInterface.GetContext()->ShrinkingGC();
507 : else
508 0 : scriptInterface.GetContext()->MaybeIncrementalGC(0.0f);
509 :
510 0 : if (m_EnableOOSLog)
511 0 : DumpState();
512 :
513 0 : ++m_TurnNumber;
514 0 : }
515 :
516 0 : void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector<SimulationCommand>& commands)
517 : {
518 : // TODO: the update process is pretty ugly, with lots of messages and dependencies
519 : // between different components. Ought to work out a nicer way to do this.
520 :
521 0 : CComponentManager& componentManager = simContext.GetComponentManager();
522 :
523 0 : CmpPtr<ICmpPathfinder> cmpPathfinder(simContext, SYSTEM_ENTITY);
524 0 : if (cmpPathfinder)
525 0 : cmpPathfinder->SendRequestedPaths();
526 :
527 : {
528 0 : PROFILE2("Sim - Update Start");
529 0 : CMessageTurnStart msgTurnStart;
530 0 : componentManager.BroadcastMessage(msgTurnStart);
531 : }
532 :
533 :
534 0 : CmpPtr<ICmpCommandQueue> cmpCommandQueue(simContext, SYSTEM_ENTITY);
535 0 : if (cmpCommandQueue)
536 0 : cmpCommandQueue->FlushTurn(commands);
537 :
538 : // Process newly generated move commands so the UI feels snappy
539 0 : if (cmpPathfinder)
540 : {
541 0 : cmpPathfinder->StartProcessingMoves(true);
542 0 : cmpPathfinder->SendRequestedPaths();
543 : }
544 : // Send all the update phases
545 : {
546 0 : PROFILE2("Sim - Update");
547 0 : CMessageUpdate msgUpdate(turnLengthFixed);
548 0 : componentManager.BroadcastMessage(msgUpdate);
549 : }
550 :
551 : {
552 0 : CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed);
553 0 : componentManager.BroadcastMessage(msgUpdate);
554 : }
555 :
556 : // Process move commands for formations (group proxy)
557 0 : if (cmpPathfinder)
558 : {
559 0 : cmpPathfinder->StartProcessingMoves(true);
560 0 : cmpPathfinder->SendRequestedPaths();
561 : }
562 :
563 : {
564 0 : PROFILE2("Sim - Motion Unit");
565 0 : CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed);
566 0 : componentManager.BroadcastMessage(msgUpdate);
567 : }
568 : {
569 0 : PROFILE2("Sim - Update Final");
570 0 : CMessageUpdate_Final msgUpdate(turnLengthFixed);
571 0 : componentManager.BroadcastMessage(msgUpdate);
572 : }
573 :
574 : // Clean up any entities destroyed during the simulation update
575 0 : componentManager.FlushDestroyedComponents();
576 :
577 : // Compute AI immediately at turn's end.
578 0 : CmpPtr<ICmpAIManager> cmpAIManager(simContext, SYSTEM_ENTITY);
579 0 : if (cmpAIManager)
580 : {
581 0 : cmpAIManager->StartComputation();
582 0 : cmpAIManager->PushCommands();
583 : }
584 :
585 : // Process all remaining moves
586 0 : if (cmpPathfinder)
587 : {
588 0 : cmpPathfinder->UpdateGrid();
589 0 : cmpPathfinder->StartProcessingMoves(false);
590 : }
591 0 : }
592 :
593 0 : void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength)
594 : {
595 0 : PROFILE3("sim interpolate");
596 :
597 0 : m_LastFrameOffset = frameOffset;
598 :
599 0 : CMessageInterpolate msg(simFrameLength, frameOffset, realFrameLength);
600 0 : m_ComponentManager.BroadcastMessage(msg);
601 :
602 : // Clean up any entities destroyed during interpolate (e.g. local corpses)
603 0 : m_ComponentManager.FlushDestroyedComponents();
604 0 : }
605 :
606 0 : void CSimulation2Impl::DumpState()
607 : {
608 0 : PROFILE("DumpState");
609 :
610 0 : std::stringstream name;\
611 0 : name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt";
612 0 : const OsPath path = m_OOSLogPath / name.str();
613 0 : std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
614 :
615 0 : if (!DirectoryExists(m_OOSLogPath))
616 : {
617 0 : LOGWARNING("OOS-log directory %s was deleted, creating it again.", m_OOSLogPath.string8().c_str());
618 0 : CreateDirectories(m_OOSLogPath, 0700);
619 : }
620 :
621 0 : file << "State hash: " << std::hex;
622 0 : std::string hashRaw;
623 0 : m_ComponentManager.ComputeStateHash(hashRaw, false);
624 0 : for (size_t i = 0; i < hashRaw.size(); ++i)
625 0 : file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i];
626 0 : file << std::dec << "\n";
627 :
628 0 : file << "\n";
629 :
630 0 : m_ComponentManager.DumpDebugState(file, true);
631 :
632 0 : std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
633 0 : m_ComponentManager.SerializeState(binfile);
634 0 : }
635 :
636 : ////////////////////////////////////////////////////////////////
637 :
638 4 : CSimulation2::CSimulation2(CUnitManager* unitManager, std::shared_ptr<ScriptContext> cx, CTerrain* terrain) :
639 4 : m(new CSimulation2Impl(unitManager, cx, terrain))
640 : {
641 4 : }
642 :
643 8 : CSimulation2::~CSimulation2()
644 : {
645 4 : delete m;
646 4 : }
647 :
648 : // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods:
649 :
650 0 : void CSimulation2::EnableSerializationTest()
651 : {
652 0 : m->m_EnableSerializationTest = true;
653 0 : }
654 :
655 0 : void CSimulation2::EnableRejoinTest(int rejoinTestTurn)
656 : {
657 0 : m->m_RejoinTestTurn = rejoinTestTurn;
658 0 : }
659 :
660 0 : void CSimulation2::EnableOOSLog()
661 : {
662 0 : if (m->m_EnableOOSLog)
663 0 : return;
664 :
665 0 : m->m_EnableOOSLog = true;
666 0 : m->m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs");
667 :
668 0 : debug_printf("Writing ooslogs to %s\n", m->m_OOSLogPath.string8().c_str());
669 : }
670 :
671 6 : entity_id_t CSimulation2::AddEntity(const std::wstring& templateName)
672 : {
673 6 : return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity());
674 : }
675 :
676 0 : entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId)
677 : {
678 0 : return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId));
679 : }
680 :
681 0 : entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName)
682 : {
683 0 : return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity());
684 : }
685 :
686 5 : void CSimulation2::DestroyEntity(entity_id_t ent)
687 : {
688 5 : m->m_ComponentManager.DestroyComponentsSoon(ent);
689 5 : }
690 :
691 5 : void CSimulation2::FlushDestroyedEntities()
692 : {
693 5 : m->m_ComponentManager.FlushDestroyedComponents();
694 5 : }
695 :
696 23 : IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const
697 : {
698 23 : return m->m_ComponentManager.QueryInterface(ent, iid);
699 : }
700 :
701 0 : void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const
702 : {
703 0 : m->m_ComponentManager.PostMessage(ent, msg);
704 0 : }
705 :
706 1 : void CSimulation2::BroadcastMessage(const CMessage& msg) const
707 : {
708 1 : m->m_ComponentManager.BroadcastMessage(msg);
709 1 : }
710 :
711 0 : CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid)
712 : {
713 0 : return m->m_ComponentManager.GetEntitiesWithInterface(iid);
714 : }
715 :
716 0 : const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid)
717 : {
718 0 : return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid);
719 : }
720 :
721 0 : const CSimContext& CSimulation2::GetSimContext() const
722 : {
723 0 : return m->m_SimContext;
724 : }
725 :
726 0 : ScriptInterface& CSimulation2::GetScriptInterface() const
727 : {
728 0 : return m->m_ComponentManager.GetScriptInterface();
729 : }
730 :
731 0 : void CSimulation2::PreInitGame()
732 : {
733 0 : ScriptRequest rq(GetScriptInterface());
734 0 : JS::RootedValue global(rq.cx, rq.globalValue());
735 0 : ScriptFunction::CallVoid(rq, global, "PreInitGame");
736 0 : }
737 :
738 0 : void CSimulation2::InitGame()
739 : {
740 0 : ScriptRequest rq(GetScriptInterface());
741 0 : JS::RootedValue global(rq.cx, rq.globalValue());
742 :
743 0 : JS::RootedValue settings(rq.cx);
744 0 : JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes());
745 0 : Script::GetProperty(rq, tmpInitAttributes, "settings", &settings);
746 :
747 0 : ScriptFunction::CallVoid(rq, global, "InitGame", settings);
748 0 : }
749 :
750 0 : void CSimulation2::Update(int turnLength)
751 : {
752 0 : std::vector<SimulationCommand> commands;
753 0 : m->Update(turnLength, commands);
754 0 : }
755 :
756 0 : void CSimulation2::Update(int turnLength, const std::vector<SimulationCommand>& commands)
757 : {
758 0 : m->Update(turnLength, commands);
759 0 : }
760 :
761 0 : void CSimulation2::Interpolate(float simFrameLength, float frameOffset, float realFrameLength)
762 : {
763 0 : m->Interpolate(simFrameLength, frameOffset, realFrameLength);
764 0 : }
765 :
766 0 : void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
767 : {
768 0 : PROFILE3("sim submit");
769 :
770 0 : CMessageRenderSubmit msg(collector, frustum, culling);
771 0 : m->m_ComponentManager.BroadcastMessage(msg);
772 0 : }
773 :
774 0 : float CSimulation2::GetLastFrameOffset() const
775 : {
776 0 : return m->m_LastFrameOffset;
777 : }
778 :
779 3 : bool CSimulation2::LoadScripts(const VfsPath& path)
780 : {
781 3 : return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path);
782 : }
783 :
784 0 : bool CSimulation2::LoadDefaultScripts()
785 : {
786 0 : return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts);
787 : }
788 :
789 0 : void CSimulation2::SetStartupScript(const std::string& code)
790 : {
791 0 : m->m_StartupScript = code;
792 0 : }
793 :
794 0 : const std::string& CSimulation2::GetStartupScript()
795 : {
796 0 : return m->m_StartupScript;
797 : }
798 :
799 0 : void CSimulation2::SetInitAttributes(JS::HandleValue attribs)
800 : {
801 0 : m->m_InitAttributes = attribs;
802 0 : }
803 :
804 0 : JS::Value CSimulation2::GetInitAttributes()
805 : {
806 0 : return m->m_InitAttributes.get();
807 : }
808 :
809 0 : void CSimulation2::GetInitAttributes(JS::MutableHandleValue ret)
810 : {
811 0 : ret.set(m->m_InitAttributes);
812 0 : }
813 :
814 0 : void CSimulation2::SetMapSettings(const std::string& settings)
815 : {
816 0 : Script::ParseJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), settings, &m->m_MapSettings);
817 0 : }
818 :
819 0 : void CSimulation2::SetMapSettings(JS::HandleValue settings)
820 : {
821 0 : m->m_MapSettings = settings;
822 :
823 0 : m->InitRNGSeedSimulation();
824 0 : m->InitRNGSeedAI();
825 0 : }
826 :
827 0 : std::string CSimulation2::GetMapSettingsString()
828 : {
829 0 : return Script::StringifyJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), &m->m_MapSettings);
830 : }
831 :
832 0 : void CSimulation2::GetMapSettings(JS::MutableHandleValue ret)
833 : {
834 0 : ret.set(m->m_MapSettings);
835 0 : }
836 :
837 0 : void CSimulation2::LoadPlayerSettings(bool newPlayers)
838 : {
839 0 : ScriptRequest rq(GetScriptInterface());
840 0 : JS::RootedValue global(rq.cx, rq.globalValue());
841 0 : ScriptFunction::CallVoid(rq, global, "LoadPlayerSettings", m->m_MapSettings, newPlayers);
842 0 : }
843 :
844 0 : void CSimulation2::LoadMapSettings()
845 : {
846 0 : ScriptRequest rq(GetScriptInterface());
847 :
848 0 : JS::RootedValue global(rq.cx, rq.globalValue());
849 :
850 : // Initialize here instead of in Update()
851 0 : ScriptFunction::CallVoid(rq, global, "LoadMapSettings", m->m_MapSettings);
852 :
853 0 : Script::FreezeObject(rq, m->m_InitAttributes, true);
854 0 : GetScriptInterface().SetGlobal("InitAttributes", m->m_InitAttributes, true, true, true);
855 :
856 0 : if (!m->m_StartupScript.empty())
857 0 : GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript);
858 :
859 : // Load the trigger scripts after we have loaded the simulation and the map.
860 0 : m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts);
861 0 : }
862 :
863 0 : int CSimulation2::ProgressiveLoad()
864 : {
865 0 : return m->ProgressiveLoad();
866 : }
867 :
868 5 : Status CSimulation2::ReloadChangedFile(const VfsPath& path)
869 : {
870 5 : return m->ReloadChangedFile(path);
871 : }
872 :
873 3 : void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI)
874 : {
875 3 : m->ResetState(skipScriptedComponents, skipAI);
876 3 : }
877 :
878 0 : bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick)
879 : {
880 0 : return m->m_ComponentManager.ComputeStateHash(outHash, quick);
881 : }
882 :
883 0 : bool CSimulation2::DumpDebugState(std::ostream& stream)
884 : {
885 0 : stream << "sim turn: " << m->m_TurnNumber << std::endl;
886 0 : return m->m_ComponentManager.DumpDebugState(stream, true);
887 : }
888 :
889 0 : bool CSimulation2::SerializeState(std::ostream& stream)
890 : {
891 0 : return m->m_ComponentManager.SerializeState(stream);
892 : }
893 :
894 0 : bool CSimulation2::DeserializeState(std::istream& stream)
895 : {
896 : // TODO: need to make sure the required SYSTEM_ENTITY components get constructed
897 0 : return m->m_ComponentManager.DeserializeState(stream);
898 : }
899 :
900 0 : void CSimulation2::ActivateRejoinTest(int turn)
901 : {
902 0 : if (m->m_RejoinTestTurn != -1)
903 0 : return;
904 0 : LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber);
905 0 : m->m_RejoinTestTurn = turn;
906 : }
907 :
908 0 : std::string CSimulation2::GenerateSchema()
909 : {
910 0 : return m->m_ComponentManager.GenerateSchema();
911 : }
912 :
913 0 : static std::vector<std::string> GetJSONData(const VfsPath& path)
914 : {
915 0 : VfsPaths pathnames;
916 0 : Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames);
917 0 : if (ret != INFO::OK)
918 : {
919 : // Some error reading directory
920 : wchar_t error[200];
921 0 : LOGERROR("Error reading directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
922 0 : return std::vector<std::string>();
923 : }
924 :
925 0 : std::vector<std::string> data;
926 0 : for (const VfsPath& p : pathnames)
927 : {
928 : // Load JSON file
929 0 : CVFSFile file;
930 0 : PSRETURN loadStatus = file.Load(g_VFS, p);
931 0 : if (loadStatus != PSRETURN_OK)
932 : {
933 0 : LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(loadStatus));
934 0 : continue;
935 : }
936 :
937 0 : data.push_back(file.DecodeUTF8()); // assume it's UTF-8
938 : }
939 :
940 0 : return data;
941 : }
942 :
943 0 : std::vector<std::string> CSimulation2::GetRMSData()
944 : {
945 0 : return GetJSONData(L"maps/random/");
946 : }
947 :
948 0 : std::vector<std::string> CSimulation2::GetVictoryConditiondData()
949 : {
950 0 : return GetJSONData(L"simulation/data/settings/victory_conditions/");
951 : }
952 :
953 0 : static std::string ReadJSON(const VfsPath& path)
954 : {
955 0 : if (!VfsFileExists(path))
956 : {
957 0 : LOGERROR("File '%s' does not exist", path.string8());
958 0 : return std::string();
959 : }
960 :
961 : // Load JSON file
962 0 : CVFSFile file;
963 0 : PSRETURN ret = file.Load(g_VFS, path);
964 0 : if (ret != PSRETURN_OK)
965 : {
966 0 : LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret));
967 0 : return std::string();
968 : }
969 :
970 0 : return file.DecodeUTF8(); // assume it's UTF-8
971 : }
972 :
973 0 : std::string CSimulation2::GetPlayerDefaults()
974 : {
975 0 : return ReadJSON(L"simulation/data/settings/player_defaults.json");
976 : }
977 :
978 0 : std::string CSimulation2::GetMapSizes()
979 : {
980 0 : return ReadJSON(L"simulation/data/settings/map_sizes.json");
981 : }
982 :
983 0 : std::string CSimulation2::GetAIData()
984 : {
985 0 : const ScriptInterface& scriptInterface = GetScriptInterface();
986 0 : ScriptRequest rq(scriptInterface);
987 0 : JS::RootedValue aiData(rq.cx, ICmpAIManager::GetAIs(scriptInterface));
988 :
989 : // Build single JSON string with array of AI data
990 0 : JS::RootedValue ais(rq.cx);
991 :
992 0 : if (!Script::CreateObject(rq, &ais, "AIData", aiData))
993 0 : return std::string();
994 :
995 0 : return Script::StringifyJSON(rq, &ais);
996 3 : }
|