diff --git a/source/graphics/MapReader.cpp b/source/graphics/MapReader.cpp index 1b3d011d40..5ef8d4d0cf 100644 --- a/source/graphics/MapReader.cpp +++ b/source/graphics/MapReader.cpp @@ -62,7 +62,7 @@ CMapReader::CMapReader() void CMapReader::LoadMap(const VfsPath& pathname, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, - CSimulation2 *pSimulation2_, int playerID_) + CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities) { // latch parameters (held until DelayedLoadFinished) pTerrain = pTerrain_; @@ -73,7 +73,9 @@ void CMapReader::LoadMap(const VfsPath& pathname, CTerrain *pTerrain_, pCinema = pCinema_; pTrigMan = pTrigMan_; pSimulation2 = pSimulation2_; + pSimContext = pSimContext_; m_PlayerID = playerID_; + m_SkipEntities = skipEntities; m_StartingCameraTarget = INVALID_ENTITY; filename_xml = pathname.ChangeExtension(L".xml"); @@ -144,7 +146,9 @@ void CMapReader::LoadRandomMap(const CStrW& scriptFile, const CScriptValRooted& pCinema = pCinema_; pTrigMan = pTrigMan_; pSimulation2 = pSimulation2_; + pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL; m_PlayerID = playerID_; + m_SkipEntities = false; m_StartingCameraTarget = INVALID_ENTITY; // delete all existing entities @@ -282,7 +286,7 @@ int CMapReader::ApplyData() if (pLightEnv) *pLightEnv = m_LightEnv; - CmpPtr cmpPlayerManager(*pSimulation2, SYSTEM_ENTITY); + CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY); if (pGameView && !cmpPlayerManager.null()) { @@ -290,7 +294,7 @@ int CMapReader::ApplyData() pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus()); // TODO: Starting rotation? - CmpPtr cmpPlayer(*pSimulation2, cmpPlayerManager->GetPlayerByID(m_PlayerID)); + CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID)); if (!cmpPlayer.null() && cmpPlayer->HasStartingCamera()) { // Use player starting camera @@ -300,7 +304,7 @@ int CMapReader::ApplyData() else if (m_StartingCameraTarget != INVALID_ENTITY) { // Point camera at entity - CmpPtr cmpPosition(*pSimulation2, m_StartingCameraTarget); + CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget); if (!cmpPosition.null()) { CFixedVector3D pos = cmpPosition->GetPosition(); @@ -309,7 +313,7 @@ int CMapReader::ApplyData() } } - CmpPtr cmpTerrain(*pSimulation2, SYSTEM_ENTITY); + CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY); if (!cmpTerrain.null()) cmpTerrain->ReloadTerrain(); @@ -630,7 +634,7 @@ void CXMLReader::ReadEnvironment(XMBElement parent) int element_name = waterelement.GetNodeName(); if (element_name == el_height) { - CmpPtr cmpWaterMan(*m_MapReader.pSimulation2, SYSTEM_ENTITY); + CmpPtr cmpWaterMan(*m_MapReader.pSimContext, SYSTEM_ENTITY); ENSURE(!cmpWaterMan.null()); cmpWaterMan->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText())); continue; @@ -859,6 +863,7 @@ int CXMLReader::ReadEntities(XMBElement parent, double end_time) { XMBElementList entities = parent.GetChildNodes(); + ENSURE(m_MapReader.pSimulation2); CSimulation2& sim = *m_MapReader.pSimulation2; CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); @@ -981,9 +986,12 @@ int CXMLReader::ProgressiveRead() } else if (name == "Entities") { - ret = ReadEntities(node, end_time); - if (ret != 0) // error or timed out - return ret; + if (!m_MapReader.m_SkipEntities) + { + ret = ReadEntities(node, end_time); + if (ret != 0) // error or timed out + return ret; + } } else if (name == "Paths") { @@ -995,7 +1003,8 @@ int CXMLReader::ProgressiveRead() } else if (name == "Script") { - m_MapReader.pSimulation2->SetStartupScript(node.GetText().FromUTF8()); + if (m_MapReader.pSimulation2) + m_MapReader.pSimulation2->SetStartupScript(node.GetText().FromUTF8()); } else { @@ -1021,7 +1030,8 @@ int CMapReader::LoadScriptSettings() xml_reader = new CXMLReader(filename_xml, *this); // parse the script settings - pSimulation2->SetMapSettings(xml_reader->ReadScriptSettings()); + if (pSimulation2) + pSimulation2->SetMapSettings(xml_reader->ReadScriptSettings()); return 0; } @@ -1029,14 +1039,16 @@ int CMapReader::LoadScriptSettings() // load player settings script int CMapReader::LoadPlayerSettings() { - pSimulation2->LoadPlayerSettings(true); + if (pSimulation2) + pSimulation2->LoadPlayerSettings(true); return 0; } // load map settings script int CMapReader::LoadMapSettings() { - pSimulation2->LoadMapSettings(); + if (pSimulation2) + pSimulation2->LoadMapSettings(); return 0; } diff --git a/source/graphics/MapReader.h b/source/graphics/MapReader.h index e3fde58e96..740b417d35 100644 --- a/source/graphics/MapReader.h +++ b/source/graphics/MapReader.h @@ -34,6 +34,7 @@ class CLightEnv; class CCinemaManager; class CTriggerManager; class CSimulation2; +class CSimContext; class CTerrainTextureEntry; class CScriptValRooted; class ScriptInterface; @@ -52,7 +53,7 @@ public: // LoadMap: try to load the map from given file; reinitialise the scene to new data if successful void LoadMap(const VfsPath& pathname, CTerrain*, WaterManager*, SkyManager*, CLightEnv*, CGameView*, - CCinemaManager*, CTriggerManager*, CSimulation2*, int playerID); + CCinemaManager*, CTriggerManager*, CSimulation2*, const CSimContext*, int playerID, bool skipEntities); void LoadRandomMap(const CStrW& scriptFile, const CScriptValRooted& settings, CTerrain*, WaterManager*, SkyManager*, CLightEnv*, CGameView*, CCinemaManager*, CTriggerManager*, CSimulation2*, int playerID); @@ -133,7 +134,9 @@ private: CCinemaManager* pCinema; CTriggerManager* pTrigMan; CSimulation2* pSimulation2; + const CSimContext* pSimContext; int m_PlayerID; + bool m_SkipEntities; VfsPath filename_xml; bool only_xml; u32 file_format_version; diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index f7f950697d..f8fe27b010 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -84,9 +84,6 @@ CGame::CGame(bool disableGraphics): **/ CGame::~CGame() { - // Clear rooted value before destroying its context - m_RegisteredAttribs = CScriptValRooted(); - // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); @@ -117,7 +114,7 @@ void CGame::SetTurnManager(CNetTurnManager* turnManager) **/ void CGame::RegisterInit(const CScriptValRooted& attribs) { - m_RegisteredAttribs = attribs; // save the attributes for ReallyStartGame + m_Simulation2->SetInitAttributes(attribs); std::string mapType; m_Simulation2->GetScriptInterface().GetProperty(attribs.get(), "mapType", mapType); @@ -165,7 +162,7 @@ void CGame::RegisterInit(const CScriptValRooted& attribs) PSRETURN CGame::ReallyStartGame() { CScriptVal settings; - m_Simulation2->GetScriptInterface().GetProperty(m_RegisteredAttribs.get(), "settings", settings); + m_Simulation2->GetScriptInterface().GetProperty(m_Simulation2->GetInitAttributes().get(), "settings", settings); m_Simulation2->InitGame(settings); // Call the reallyStartGame GUI function, but only if it exists diff --git a/source/ps/Game.h b/source/ps/Game.h index d8897a12e7..d1f47748c7 100644 --- a/source/ps/Game.h +++ b/source/ps/Game.h @@ -162,7 +162,6 @@ public: private: void RegisterInit(const CScriptValRooted& attribs); IReplayLogger* m_ReplayLogger; - CScriptValRooted m_RegisteredAttribs; std::vector m_PlayerColours; }; diff --git a/source/ps/World.cpp b/source/ps/World.cpp index a5bc71b1c3..5c9f95096c 100644 --- a/source/ps/World.cpp +++ b/source/ps/World.cpp @@ -82,7 +82,7 @@ void CWorld::RegisterInit(const CStrW& mapFile, int playerID) CRenderer::IsInitialised() ? g_Renderer.GetSkyManager() : NULL, &g_LightEnv, m_pGame->GetView(), m_pGame->GetView() ? m_pGame->GetView()->GetCinema() : NULL, - pTriggerManager, m_pGame->GetSimulation2(), playerID); + pTriggerManager, m_pGame->GetSimulation2(), &m_pGame->GetSimulation2()->GetSimContext(), playerID, false); // fails immediately, or registers for delay loading } catch (PSERROR_File& err) diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index 0243253a6a..92c08b2afe 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -27,11 +27,14 @@ #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" +#include "graphics/MapReader.h" +#include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_util.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" +#include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/XML/Xeromyces.h" @@ -43,11 +46,21 @@ #define getpid _getpid // use the non-deprecated function name #endif +static std::string Hexify(const std::string& s) // TODO: shouldn't duplicate this function in so many places +{ + 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(); +} + class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, CTerrain* terrain) : - m_SimContext(), m_ComponentManager(m_SimContext), m_EnableOOSLog(false) + m_SimContext(), m_ComponentManager(m_SimContext), + m_EnableOOSLog(false), m_EnableSerializationTest(false) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; @@ -56,6 +69,7 @@ public: RegisterFileReloadFunc(ReloadChangedFileCB, this); // m_EnableOOSLog = true; // TODO: this should be a command-line flag or similar +// m_EnableSerializationTest = true; // TODO: this should too } ~CSimulation2Impl() @@ -65,41 +79,45 @@ public: void ResetState(bool skipScriptedComponents, bool skipAI) { - m_ComponentManager.ResetState(); - m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; + ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI); + } + + static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI) + { + componentManager.ResetState(); CParamNode noParam; CComponentManager::ComponentTypeId cid; // Add native system components: - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_TemplateManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_TemplateManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_CommandQueue, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ObstructionManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_RangeManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_SoundManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_TerritoryManager, noParam); - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_CommandQueue, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_ObstructionManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_RangeManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_SoundManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_TerritoryManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam); if (!skipAI) { - m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_AIManager, noParam); + componentManager.AddComponent(SYSTEM_ENTITY, CID_AIManager, noParam); } // Add scripted system components: if (!skipScriptedComponents) { #define LOAD_SCRIPTED_COMPONENT(name) \ - cid = m_ComponentManager.LookupCID(name); \ + cid = componentManager.LookupCID(name); \ if (cid == CID__Invalid) \ LOGERROR(L"Can't find component type " L##name); \ - m_ComponentManager.AddComponent(SYSTEM_ENTITY, cid, noParam) + componentManager.AddComponent(SYSTEM_ENTITY, cid, noParam) LOAD_SCRIPTED_COMPONENT("AIInterface"); LOAD_SCRIPTED_COMPONENT("EndGameManager"); @@ -111,7 +129,8 @@ public: } } - bool LoadScripts(const VfsPath& path); + static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts); + static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path); Status ReloadChangedFile(const VfsPath& path); static Status ReloadChangedFileCB(void* param, const VfsPath& path) @@ -120,7 +139,8 @@ public: } int ProgressiveLoad(); - bool Update(int turnLength, const std::vector& commands); + void Update(int turnLength, const std::vector& commands); + static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands); void Interpolate(float frameLength, float frameOffset); void DumpState(); @@ -131,6 +151,7 @@ public: float m_LastFrameOffset; std::wstring m_StartupScript; + CScriptValRooted m_InitAttributes; CScriptValRooted m_MapSettings; std::set m_LoadedScripts; @@ -138,9 +159,48 @@ public: uint32_t m_TurnNumber; bool m_EnableOOSLog; + + + // Functions and data for the serialization test mode: (see Update() for relevant comments) + + bool m_EnableSerializationTest; + + struct SerializationTestState + { + std::stringstream state; + std::stringstream debug; + std::string hash; + }; + + void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix); + + void ReportSerializationFailure( + SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, + SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter); + + static std::vector CloneCommandsFromOtherContext(ScriptInterface& oldScript, ScriptInterface& newScript, + const std::vector& commands) + { + std::vector newCommands = commands; + for (size_t i = 0; i < newCommands.size(); ++i) + { + newCommands[i].data = CScriptValRooted(newScript.GetContext(), + newScript.CloneValueFromOtherContext(oldScript, newCommands[i].data.get())); + } + return newCommands; + } }; -bool CSimulation2Impl::LoadScripts(const VfsPath& path) +bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts) +{ + return ( + LoadScripts(componentManager, loadedScripts, "simulation/components/interfaces/") && + LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") && + LoadScripts(componentManager, loadedScripts, L"simulation/components/") + ); +} + +bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path) { VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) @@ -150,9 +210,10 @@ bool CSimulation2Impl::LoadScripts(const VfsPath& path) for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) { VfsPath filename = *it; - m_LoadedScripts.insert(filename); + if (loadedScripts) + loadedScripts->insert(filename); LOGMESSAGE(L"Loading simulation script '%ls'", filename.string().c_str()); - if (! m_ComponentManager.LoadScript(filename)) + if (! componentManager.LoadScript(filename)) ok = false; } return ok; @@ -207,63 +268,167 @@ int CSimulation2Impl::ProgressiveLoad() return ret; } -bool CSimulation2Impl::Update(int turnLength, const std::vector& commands) +void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix) +{ + if (!state.hash.empty()) + { + std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); + file << Hexify(state.hash); + } + + if (!state.debug.str().empty()) + { + std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); + file << state.debug.str(); + } + + if (!state.state.str().empty()) + { + std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); + file << state.state.str(); + } +} + +void CSimulation2Impl::ReportSerializationFailure( + SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, + SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter) +{ + OsPath path = psLogDir() / "oos_log"; + CreateDirectories(path, 0700); + + // Clean up obsolete files from previous runs + wunlink(path / "hash.before.a"); + wunlink(path / "hash.before.b"); + wunlink(path / "debug.before.a"); + wunlink(path / "debug.before.b"); + wunlink(path / "state.before.a"); + wunlink(path / "state.before.b"); + wunlink(path / "hash.after.a"); + wunlink(path / "hash.after.b"); + wunlink(path / "debug.after.a"); + wunlink(path / "debug.after.b"); + wunlink(path / "state.after.a"); + wunlink(path / "state.after.b"); + + if (primaryStateBefore) + DumpSerializationTestState(*primaryStateBefore, path, L"before.a"); + if (primaryStateAfter) + DumpSerializationTestState(*primaryStateAfter, path, L"after.a"); + if (secondaryStateBefore) + DumpSerializationTestState(*secondaryStateBefore, path, L"before.b"); + if (secondaryStateAfter) + DumpSerializationTestState(*secondaryStateAfter, path, L"after.b"); + + debug_warn(L"Serialization test failure"); +} + +void CSimulation2Impl::Update(int turnLength, const std::vector& commands) { fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; - // TODO: the update process is pretty ugly, with lots of messages and dependencies - // between different components. Ought to work out a nicer way to do this. + /* + * In serialization test mode, we save the original (primary) simulation state before each turn update. + * We run the update, then load the saved state into a secondary context. + * We serialize that again and compare to the original serialization (to check that + * serialize->deserialize->serialize is equivalent to serialize). + * Then we run the update on the secondary context, and check that its new serialized + * state matches the primary context after the update (to check that the simulation doesn't depend + * on anything that's not serialized). + */ - CMessageTurnStart msgTurnStart; - m_ComponentManager.BroadcastMessage(msgTurnStart); + const bool serializationTestDebugDump = false; // set true to save human-readable state dumps, for debugging (but slow) + const bool serializationTestHash = true; // set true to save and compare hash of state - CmpPtr cmpPathfinder(m_SimContext, SYSTEM_ENTITY); - if (!cmpPathfinder.null()) - cmpPathfinder->FinishAsyncRequests(); - - // Push AI commands onto the queue before we use them - CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); - if (!cmpAIManager.null()) - cmpAIManager->PushCommands(); - - CmpPtr cmpCommandQueue(m_SimContext, SYSTEM_ENTITY); - if (!cmpCommandQueue.null()) - cmpCommandQueue->FlushTurn(commands); - - // Process newly generated move commands so the UI feels snappy - if (!cmpPathfinder.null()) - cmpPathfinder->ProcessSameTurnMoves(); - - // Send all the update phases + SerializationTestState primaryStateBefore; + if (m_EnableSerializationTest) { - CMessageUpdate msgUpdate(turnLengthFixed); - m_ComponentManager.BroadcastMessage(msgUpdate); - } - { - CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); - m_ComponentManager.BroadcastMessage(msgUpdate); + ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state)); + if (serializationTestDebugDump) + ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false)); + if (serializationTestHash) + ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false)); } - // Process move commands for formations (group proxy) - if (!cmpPathfinder.null()) - cmpPathfinder->ProcessSameTurnMoves(); + UpdateComponents(m_SimContext, turnLengthFixed, commands); + + + if (m_EnableSerializationTest) { - CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); - m_ComponentManager.BroadcastMessage(msgUpdate); + // Initialise the secondary simulation + CTerrain secondaryTerrain; + CSimContext secondaryContext; + secondaryContext.m_Terrain = &secondaryTerrain; + CComponentManager secondaryComponentManager(secondaryContext); + secondaryComponentManager.LoadComponentTypes(); + ENSURE(LoadDefaultScripts(secondaryComponentManager, NULL)); + ResetComponentState(secondaryComponentManager, false, false); + + // Load the map into the secondary simulation + + LDR_BeginRegistering(); + CMapReader* mapReader = new CMapReader; // automatically deletes itself + + // TODO: this duplicates CWorld::RegisterInit and could probably be cleaned up a bit + std::string mapType; + m_ComponentManager.GetScriptInterface().GetProperty(m_InitAttributes.get(), "mapType", mapType); + if (mapType == "scenario") + { + // Load scenario attributes + std::wstring mapFile; + m_ComponentManager.GetScriptInterface().GetProperty(m_InitAttributes.get(), "map", mapFile); + + VfsPath mapfilename(VfsPath("maps/scenarios") / (mapFile + L".pmp")); + mapReader->LoadMap(mapfilename, &secondaryTerrain, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, &secondaryContext, INVALID_PLAYER, true); // throws exception on failure + } + else + { + // TODO: support random map scripts + debug_warn(L"Serialization test mode only supports scenarios"); + } + + LDR_EndRegistering(); + ENSURE(LDR_NonprogressiveLoad() == INFO::OK); + + ENSURE(secondaryComponentManager.DeserializeState(primaryStateBefore.state)); + + SerializationTestState secondaryStateBefore; + ENSURE(secondaryComponentManager.SerializeState(secondaryStateBefore.state)); + if (serializationTestDebugDump) + ENSURE(secondaryComponentManager.DumpDebugState(secondaryStateBefore.debug, false)); + if (serializationTestHash) + ENSURE(secondaryComponentManager.ComputeStateHash(secondaryStateBefore.hash, false)); + + if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() || + primaryStateBefore.hash != secondaryStateBefore.hash) + { + ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL); + } + + SerializationTestState primaryStateAfter; + ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state)); + if (serializationTestDebugDump) + ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false)); + if (serializationTestHash) + ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false)); + + UpdateComponents(secondaryContext, turnLengthFixed, + CloneCommandsFromOtherContext(m_ComponentManager.GetScriptInterface(), secondaryComponentManager.GetScriptInterface(), commands)); + + SerializationTestState secondaryStateAfter; + ENSURE(secondaryComponentManager.SerializeState(secondaryStateAfter.state)); + if (serializationTestDebugDump) + ENSURE(secondaryComponentManager.DumpDebugState(secondaryStateAfter.debug, false)); + if (serializationTestHash) + ENSURE(secondaryComponentManager.ComputeStateHash(secondaryStateAfter.hash, false)); + + if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() || + primaryStateAfter.hash != secondaryStateAfter.hash) + { + ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter); + } } - { - CMessageUpdate_Final msgUpdate(turnLengthFixed); - m_ComponentManager.BroadcastMessage(msgUpdate); - } - - // Process moves resulting from group proxy movement (unit needs to catch up or realign) and any others - if (!cmpPathfinder.null()) - cmpPathfinder->ProcessSameTurnMoves(); - - - // Clean up any entities destroyed during the simulation update - m_ComponentManager.FlushDestroyedComponents(); // if (m_TurnNumber == 0) // m_ComponentManager.GetScriptInterface().DumpHeap(); @@ -278,12 +443,69 @@ bool CSimulation2Impl::Update(int turnLength, const std::vector cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (!cmpAIManager.null()) cmpAIManager->StartComputation(); ++m_TurnNumber; +} - return true; // TODO: don't bother with bool return +void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands) +{ + // TODO: the update process is pretty ugly, with lots of messages and dependencies + // between different components. Ought to work out a nicer way to do this. + + CComponentManager& componentManager = simContext.GetComponentManager(); + + CMessageTurnStart msgTurnStart; + componentManager.BroadcastMessage(msgTurnStart); + + CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY); + if (!cmpPathfinder.null()) + cmpPathfinder->FinishAsyncRequests(); + + // Push AI commands onto the queue before we use them + CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); + if (!cmpAIManager.null()) + cmpAIManager->PushCommands(); + + CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); + if (!cmpCommandQueue.null()) + cmpCommandQueue->FlushTurn(commands); + + // Process newly generated move commands so the UI feels snappy + if (!cmpPathfinder.null()) + cmpPathfinder->ProcessSameTurnMoves(); + + // Send all the update phases + { + CMessageUpdate msgUpdate(turnLengthFixed); + componentManager.BroadcastMessage(msgUpdate); + } + { + CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); + componentManager.BroadcastMessage(msgUpdate); + } + + // Process move commands for formations (group proxy) + if (!cmpPathfinder.null()) + cmpPathfinder->ProcessSameTurnMoves(); + + { + CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); + componentManager.BroadcastMessage(msgUpdate); + } + { + CMessageUpdate_Final msgUpdate(turnLengthFixed); + componentManager.BroadcastMessage(msgUpdate); + } + + // Process moves resulting from group proxy movement (unit needs to catch up or realign) and any others + if (!cmpPathfinder.null()) + cmpPathfinder->ProcessSameTurnMoves(); + + // Clean up any entities destroyed during the simulation update + componentManager.FlushDestroyedComponents(); } void CSimulation2Impl::Interpolate(float frameLength, float frameOffset) @@ -316,7 +538,7 @@ void CSimulation2Impl::DumpState() file << "\n"; - m_ComponentManager.DumpDebugState(file); + m_ComponentManager.DumpDebugState(file, true); std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); @@ -407,15 +629,15 @@ void CSimulation2::InitGame(const CScriptVal& data) GetScriptInterface().CallFunction(GetScriptInterface().GetGlobalObject(), "InitGame", data, ret); } -bool CSimulation2::Update(int turnLength) +void CSimulation2::Update(int turnLength) { std::vector commands; - return m->Update(turnLength, commands); + m->Update(turnLength, commands); } -bool CSimulation2::Update(int turnLength, const std::vector& commands) +void CSimulation2::Update(int turnLength, const std::vector& commands) { - return m->Update(turnLength, commands); + m->Update(turnLength, commands); } void CSimulation2::Interpolate(float frameLength, float frameOffset) @@ -436,16 +658,12 @@ float CSimulation2::GetLastFrameOffset() const bool CSimulation2::LoadScripts(const VfsPath& path) { - return m->LoadScripts(path); + return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path); } bool CSimulation2::LoadDefaultScripts() { - return ( - m->LoadScripts(L"simulation/components/interfaces/") && - m->LoadScripts(L"simulation/helpers/") && - m->LoadScripts(L"simulation/components/") - ); + return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts); } void CSimulation2::SetStartupScript(const std::wstring& code) @@ -458,6 +676,16 @@ const std::wstring& CSimulation2::GetStartupScript() return m->m_StartupScript; } +void CSimulation2::SetInitAttributes(const CScriptValRooted& attribs) +{ + m->m_InitAttributes = attribs; +} + +CScriptValRooted CSimulation2::GetInitAttributes() +{ + return m->m_InitAttributes; +} + void CSimulation2::SetMapSettings(const std::string& settings) { m->m_MapSettings = m->m_ComponentManager.GetScriptInterface().ParseJSON(settings); @@ -514,7 +742,7 @@ bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick) bool CSimulation2::DumpDebugState(std::ostream& stream) { - return m->m_ComponentManager.DumpDebugState(stream); + return m->m_ComponentManager.DumpDebugState(stream, true); } bool CSimulation2::SerializeState(std::ostream& stream) diff --git a/source/simulation2/Simulation2.h b/source/simulation2/Simulation2.h index eb705a476f..8f98a859c9 100644 --- a/source/simulation2/Simulation2.h +++ b/source/simulation2/Simulation2.h @@ -89,6 +89,17 @@ public: */ const std::wstring& GetStartupScript(); + /** + * Set the attributes identifying the scenario/RMS used to initialise this + * simulation. + */ + void SetInitAttributes(const CScriptValRooted& settings); + + /** + * Get the data passed to SetInitAttributes. + */ + CScriptValRooted GetInitAttributes(); + /** * Set the initial map settings (as a UTF-8-encoded JSON string), * which will be used to set up the simulation state. @@ -141,8 +152,8 @@ public: */ void InitGame(const CScriptVal& data); - bool Update(int turnLength); - bool Update(int turnLength, const std::vector& commands); + void Update(int turnLength); + void Update(int turnLength, const std::vector& commands); void Interpolate(float frameLength, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); diff --git a/source/simulation2/components/tests/test_Pathfinder.h b/source/simulation2/components/tests/test_Pathfinder.h index 8fa47f3038..eb63801244 100644 --- a/source/simulation2/components/tests/test_Pathfinder.h +++ b/source/simulation2/components/tests/test_Pathfinder.h @@ -68,7 +68,8 @@ public: CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself LDR_BeginRegistering(); - mapReader->LoadMap(L"maps/scenarios/Median Oasis.pmp", &terrain, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, -1); + mapReader->LoadMap(L"maps/scenarios/Median Oasis.pmp", &terrain, NULL, NULL, NULL, NULL, NULL, NULL, + &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); diff --git a/source/simulation2/serialization/DebugSerializer.cpp b/source/simulation2/serialization/DebugSerializer.cpp index fb06a9214a..561c58e404 100644 --- a/source/simulation2/serialization/DebugSerializer.cpp +++ b/source/simulation2/serialization/DebugSerializer.cpp @@ -52,8 +52,8 @@ std::string canonfloat(T value, int prec) return r; } -CDebugSerializer::CDebugSerializer(ScriptInterface& scriptInterface, std::ostream& stream) : - m_ScriptInterface(scriptInterface), m_Stream(stream), m_Indent(0) +CDebugSerializer::CDebugSerializer(ScriptInterface& scriptInterface, std::ostream& stream, bool includeDebugInfo) : + m_ScriptInterface(scriptInterface), m_Stream(stream), m_IsDebug(includeDebugInfo), m_Indent(0) { } @@ -170,7 +170,7 @@ void CDebugSerializer::PutRaw(const char* name, const u8* data, size_t len) bool CDebugSerializer::IsDebug() const { - return true; + return m_IsDebug; } std::ostream& CDebugSerializer::GetStream() diff --git a/source/simulation2/serialization/DebugSerializer.h b/source/simulation2/serialization/DebugSerializer.h index 7b67cef491..f8e4506e27 100644 --- a/source/simulation2/serialization/DebugSerializer.h +++ b/source/simulation2/serialization/DebugSerializer.h @@ -30,8 +30,9 @@ public: /** * @param scriptInterface Script interface corresponding to any jsvals passed to ScriptVal() * @param stream Stream to receive UTF-8 encoded output + * @param includeDebugInfo If true then additional non-deterministic data will be included in the output */ - CDebugSerializer(ScriptInterface& scriptInterface, std::ostream& stream); + CDebugSerializer(ScriptInterface& scriptInterface, std::ostream& stream, bool includeDebugInfo = true); void Comment(const std::string& comment); void TextLine(const std::string& text); @@ -59,6 +60,7 @@ protected: private: ScriptInterface& m_ScriptInterface; std::ostream& m_Stream; + bool m_IsDebug; int m_Indent; }; diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index 70ce2671a1..92dbdd09d2 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -208,7 +208,7 @@ public: // Various state serialization functions: bool ComputeStateHash(std::string& outHash, bool quick); - bool DumpDebugState(std::ostream& stream); + bool DumpDebugState(std::ostream& stream, bool includeDebugInfo); // FlushDestroyedComponents must be called before SerializeState (since the destruction queue // won't get serialized) bool SerializeState(std::ostream& stream); diff --git a/source/simulation2/system/ComponentManagerSerialization.cpp b/source/simulation2/system/ComponentManagerSerialization.cpp index 8d65350ec9..fcd673f940 100644 --- a/source/simulation2/system/ComponentManagerSerialization.cpp +++ b/source/simulation2/system/ComponentManagerSerialization.cpp @@ -51,9 +51,9 @@ void DeserializeRNG(const std::string& str, boost::rand48& rng) s >> rng; } -bool CComponentManager::DumpDebugState(std::ostream& stream) +bool CComponentManager::DumpDebugState(std::ostream& stream, bool includeDebugInfo) { - CDebugSerializer serializer(m_ScriptInterface, stream); + CDebugSerializer serializer(m_ScriptInterface, stream, includeDebugInfo); serializer.StringASCII("rng", SerializeRNG(m_RNG), 0, 32); diff --git a/source/simulation2/tests/test_ComponentManager.h b/source/simulation2/tests/test_ComponentManager.h index ea6560d53b..65cfab9720 100644 --- a/source/simulation2/tests/test_ComponentManager.h +++ b/source/simulation2/tests/test_ComponentManager.h @@ -564,7 +564,7 @@ public: TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; - TS_ASSERT(man.DumpDebugState(debugStream)); + TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" @@ -654,7 +654,7 @@ public: TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 8); std::stringstream debugStream; - TS_ASSERT(man.DumpDebugState(debugStream)); + TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" diff --git a/source/simulation2/tests/test_Serializer.h b/source/simulation2/tests/test_Serializer.h index a1002f78de..e8e8cd48a0 100644 --- a/source/simulation2/tests/test_Serializer.h +++ b/source/simulation2/tests/test_Serializer.h @@ -499,7 +499,8 @@ public: CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself LDR_BeginRegistering(); - mapReader->LoadMap(L"maps/scenarios/Latium.pmp", &terrain, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, -1); + mapReader->LoadMap(L"maps/scenarios/Latium.pmp", &terrain, NULL, NULL, NULL, NULL, NULL, NULL, + &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad());