Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 27322) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 27323) @@ -1,424 +1,424 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/external_libraries/libsdl.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" #include "ps/TaskManager.h" #include "ps/scripting/JSInterface_VFS.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/helpers/MapEdgeTiles.h" #include #include // TODO: Maybe this should be optimized depending on the map size. constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024; extern bool IsQuitRequested(); static bool MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : m_ScriptInterface(scriptInterface) { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Cancel or wait for the task to end. m_WorkerThread.CancelOrWait(); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Start generating the map asynchronously. m_WorkerThread = Threading::TaskManager::Instance().PushTask([this]() { PROFILE2("Map Generation"); std::shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); // Enable the script to be aborted JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); // Run map generation scripts if (!Run() || m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(m_WorkerMutex); m_Progress = -1; } SAFE_DELETE(m_ScriptInterface); // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. }); } bool CMapGeneratorWorker::Run() { ScriptRequest rq(m_ScriptInterface); // Parse settings JS::RootedValue settingsVal(rq.cx); if (!Script::ParseJSON(rq, m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!Script::FreezeObject(rq, settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!Script::HasProperty(rq, settingsVal, "Seed") || !Script::GetProperty(rq, settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); InitScriptInterface(seed); RegisterScriptFunctions_MapGenerator(); // Copy settings to global variable JS::RootedValue global(rq.cx, rq.globalValue()); if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } #define REGISTER_MAPGEN_FUNC(func) \ ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, #func); #define REGISTER_MAPGEN_FUNC_NAME(func, name) \ ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, name); void CMapGeneratorWorker::InitScriptInterface(const u32 seed) { m_ScriptInterface->SetCallbackData(static_cast(this)); m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); m_MapGenRNG.seed(seed); // VFS JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // File loading ScriptRequest rq(m_ScriptInterface); REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary"); REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage"); REGISTER_MAPGEN_FUNC(LoadMapTerrain); // Engine constants // Length of one tile of the terrain grid in metres. // Useful to transform footprint sizes to the tilegrid coordinate system. m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); // Number of impassable tiles at the map border m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() { ScriptRequest rq(m_ScriptInterface); // Template functions REGISTER_MAPGEN_FUNC(GetTemplate); REGISTER_MAPGEN_FUNC(TemplateExists); REGISTER_MAPGEN_FUNC(FindTemplates); REGISTER_MAPGEN_FUNC(FindActorTemplates); // Progression and profiling REGISTER_MAPGEN_FUNC(SetProgress); REGISTER_MAPGEN_FUNC(GetMicroseconds); REGISTER_MAPGEN_FUNC(ExportMap); } #undef REGISTER_MAPGEN_FUNC #undef REGISTER_MAPGEN_FUNC_NAME int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } double CMapGeneratorWorker::GetMicroseconds() { return JS_Now(); } Script::StructuredClone CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } void CMapGeneratorWorker::ExportMap(JS::HandleValue data) { // Copy results std::lock_guard lock(m_WorkerMutex); m_MapData = Script::WriteStructuredClone(ScriptRequest(m_ScriptInterface), data); m_Progress = 0; } void CMapGeneratorWorker::SetProgress(int progress) { // Copy data std::lock_guard lock(m_WorkerMutex); if (progress >= m_Progress) m_Progress = progress; else LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", m_Progress, progress); } CParamNode CMapGeneratorWorker::GetTemplate(const std::string& templateName) { - const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); + const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(const std::string& templateName) { return m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories) { return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories) { return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } JS::Value CMapGeneratorWorker::LoadHeightmap(const VfsPath& filename) { std::vector heightmap; if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", filename.string8()); return JS::UndefinedValue(); } ScriptRequest rq(m_ScriptInterface); JS::RootedValue returnValue(rq.cx); Script::ToJSVal(rq, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename) { ScriptRequest rq(m_ScriptInterface); if (!VfsFileExists(filename)) { ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); return JS::UndefinedValue(); } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) { ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str()); return JS::UndefinedValue(); } // unpack size ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; // unpack heightmap std::vector heightmap; heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); // unpack texture names size_t textureCount = unpacker.UnpackSize(); std::vector textureNames; textureNames.reserve(textureCount); for (size_t i = 0; i < textureCount; ++i) { CStr texturename; unpacker.UnpackString(texturename); textureNames.push_back(texturename); } // unpack texture IDs per tile ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; std::vector tiles; tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); // reorder by patches and store and save texture IDs per tile std::vector textureIDs; for (ssize_t x = 0; x < tilesPerSide; ++x) { size_t patchX = x / PATCH_SIZE; size_t offX = x % PATCH_SIZE; for (ssize_t y = 0; y < tilesPerSide; ++y) { size_t patchY = y / PATCH_SIZE; size_t offY = y % PATCH_SIZE; // m_Priority and m_Tex2Index unused textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } JS::RootedValue returnValue(rq.cx); Script::CreateObject( rq, &returnValue, "height", heightmap, "textureNames", textureNames, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } Script::StructuredClone CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/gui/GUIManager.cpp =================================================================== --- ps/trunk/source/gui/GUIManager.cpp (revision 27322) +++ ps/trunk/source/gui/GUIManager.cpp (revision 27323) @@ -1,448 +1,448 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GUIManager.h" #include "gui/CGUI.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/VideoMode.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" namespace { const CStr EVENT_NAME_GAME_LOAD_PROGRESS = "GameLoadProgress"; const CStr EVENT_NAME_WINDOW_RESIZED = "WindowResized"; } // anonymous namespace CGUIManager* g_GUI = nullptr; // General TODOs: // // A lot of the CGUI data could (and should) be shared between // multiple pages, instead of treating them as completely independent, to save // memory and loading time. // called from main loop when (input) events are received. // event is passed to other handlers if false is returned. // trampoline: we don't want to make the HandleEvent implementation static InReaction gui_handler(const SDL_Event_* ev) { if (!g_GUI) return IN_PASS; PROFILE("GUI event handler"); return g_GUI->HandleEvent(ev); } static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } CGUIManager::CGUIManager() { m_ScriptContext = g_ScriptContext; m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptContext)); m_ScriptInterface->SetCallbackData(this); m_ScriptInterface->LoadGlobalScripts(); if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng")) LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'"); if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng")) LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'"); RegisterFileReloadFunc(ReloadChangedFileCB, this); } CGUIManager::~CGUIManager() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } size_t CGUIManager::GetPageCount() const { return m_PageStack.size(); } void CGUIManager::SwitchPage(const CStrW& pageName, const ScriptInterface* srcScriptInterface, JS::HandleValue initData) { // The page stack is cleared (including the script context where initData came from), // therefore we have to clone initData. Script::StructuredClone initDataClone; if (!initData.isUndefined()) { ScriptRequest rq(srcScriptInterface); initDataClone = Script::WriteStructuredClone(rq, initData); } if (!m_PageStack.empty()) { // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); m_PageStack.clear(); } PushPage(pageName, initDataClone, JS::UndefinedHandleValue); } void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction) { // Store the callback handler in the current GUI page before opening the new one if (!m_PageStack.empty() && !callbackFunction.isUndefined()) { m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction); // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); } // Push the page prior to loading its contents, because that may push // another GUI page on init which should be pushed on top of this new page. m_PageStack.emplace_back(pageName, initData); m_PageStack.back().LoadPage(m_ScriptContext); } void CGUIManager::PopPage(Script::StructuredClone args) { if (m_PageStack.size() < 2) { debug_warn(L"Tried to pop GUI page when there's < 2 in the stack"); return; } // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); m_PageStack.pop_back(); m_PageStack.back().PerformCallbackFunction(args); // We return to a page where some object might have been focused. m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS); } CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData) : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction() { } void CGUIManager::SGUIPage::LoadPage(std::shared_ptr scriptContext) { // If we're hotloading then try to grab some data from the previous page Script::StructuredClone hotloadData; if (gui) { std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue hotloadDataVal(rq.cx); ScriptFunction::Call(rq, global, "getHotloadData", &hotloadDataVal); hotloadData = Script::WriteStructuredClone(rq, hotloadDataVal); } g_VideoMode.ResetCursor(); inputs.clear(); gui.reset(new CGUI(scriptContext)); gui->AddObjectTypes(); VfsPath path = VfsPath("gui") / m_Name; inputs.insert(path); CXeromyces xero; if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK) // Fail silently (Xeromyces reported the error) return; int elmt_page = xero.GetElementID("page"); int elmt_include = xero.GetElementID("include"); XMBElement root = xero.GetRoot(); if (root.GetNodeName() != elmt_page) { LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(m_Name)); return; } XERO_ITER_EL(root, node) { if (node.GetNodeName() != elmt_include) { LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(m_Name)); continue; } CStr8 name = node.GetText(); CStrW nameW = node.GetText().FromUTF8(); PROFILE2("load gui xml"); PROFILE2_ATTR("name: %s", name.c_str()); TIMER(nameW.c_str()); if (name.back() == '/') { VfsPath currentDirectory = VfsPath("gui") / nameW; VfsPaths directories; vfs::GetPathnames(g_VFS, currentDirectory, L"*.xml", directories); for (const VfsPath& directory : directories) gui->LoadXmlFile(directory, inputs); } else { VfsPath directory = VfsPath("gui") / nameW; gui->LoadXmlFile(directory, inputs); } } gui->LoadedXmlFiles(); std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initDataVal(rq.cx); JS::RootedValue hotloadDataVal(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); if (initData) Script::ReadStructuredClone(rq, initData, &initDataVal); if (hotloadData) Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal); if (Script::HasProperty(rq, global, "init") && !ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal)) LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name)); } void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) { if (!callbackFunc.isObject()) { LOGERROR("Given callback handler is not an object!"); return; } ScriptRequest rq(scriptInterface); if (!JS_ObjectIsFunction(&callbackFunc.toObject())) { LOGERROR("Given callback handler is not a function!"); return; } callbackFunction = std::make_shared(scriptInterface.GetGeneralJSContext(), callbackFunc); } void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args) { if (!callbackFunction) return; std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedObject globalObj(rq.cx, rq.glob); JS::RootedValue funcVal(rq.cx, *callbackFunction); // Delete the callback function, so that it is not called again callbackFunction.reset(); JS::RootedValue argVal(rq.cx); if (args) Script::ReadStructuredClone(rq, args, &argVal); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(argVal)); JS::RootedValue result(rq.cx); if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result)) ScriptException::CatchPending(rq); } Status CGUIManager::ReloadChangedFile(const VfsPath& path) { for (SGUIPage& p : m_PageStack) if (p.inputs.find(path) != p.inputs.end()) { LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.m_Name)); p.LoadPage(m_ScriptContext); // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators } return INFO::OK; } Status CGUIManager::ReloadAllPages() { // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators for (SGUIPage& p : m_PageStack) p.LoadPage(m_ScriptContext); return INFO::OK; } InReaction CGUIManager::HandleEvent(const SDL_Event_* ev) { // We want scripts to have access to the raw input events, so they can do complex // processing when necessary (e.g. for unit selection and camera movement). // Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the // visible game area), sometimes they'll want to intercepts events before the GUI (e.g. // to capture all mouse events until a mouseup after dragging). // So we call two separate handler functions: bool handled = false; { PROFILE("handleInputBeforeGui"); ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); if (ScriptFunction::Call(rq, global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse())) if (handled) return IN_HANDLED; } { PROFILE("handle event in native GUI"); InReaction r = top()->HandleEvent(ev); if (r != IN_PASS) return r; } { // We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui! ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); PROFILE("handleInputAfterGui"); if (ScriptFunction::Call(rq, global, "handleInputAfterGui", handled, *ev)) if (handled) return IN_HANDLED; } return IN_PASS; } void CGUIManager::SendEventToAll(const CStr& eventName) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName); } void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName, paramData); } void CGUIManager::TickObjects() { PROFILE3("gui tick"); // We share the script context with everything else that runs in the same thread. // This call makes sure we trigger GC regularly even if the simulation is not running. m_ScriptInterface->GetContext()->MaybeIncrementalGC(1.0f); // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->TickObjects(); } void CGUIManager::Draw(CCanvas2D& canvas) const { PROFILE3_GPU("gui"); for (const SGUIPage& p : m_PageStack) p.gui->Draw(canvas); } void CGUIManager::UpdateResolution() { // Save an immutable copy so iterators aren't invalidated by event handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) { p.gui->UpdateResolution(); p.gui->SendEventToAll(EVENT_NAME_WINDOW_RESIZED); } } bool CGUIManager::TemplateExists(const std::string& templateName) const { return m_TemplateLoader.TemplateExists(templateName); } const CParamNode& CGUIManager::GetTemplate(const std::string& templateName) { - const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); + const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } void CGUIManager::DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); Script::ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); SendEventToAll(EVENT_NAME_GAME_LOAD_PROGRESS, paramData); } // This returns a shared_ptr to make sure the CGUI doesn't get deallocated // while we're in the middle of calling a function on it (e.g. if a GUI script // calls SwitchPage) std::shared_ptr CGUIManager::top() const { ENSURE(m_PageStack.size()); return m_PageStack.back().gui; } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 27322) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 27323) @@ -1,1277 +1,1277 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/GameSetup/GameSetup.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/Scripting/JSInterface_GUIManager.h" #include "i18n/L10n.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/input.h" #include "lib/timer.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "network/scripting/JSInterface_Network.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_Game.h" #include "ps/scripting/JSInterface_Main.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/VertexBufferManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/JSON.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "simulation2/scripting/JSInterface_Simulation.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); thread_local std::shared_ptr g_ScriptContext; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); // Mods will be mounted later. // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->Init(); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). in_add_handler(HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN // SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false // breaking the behavior on Windows. // https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710 // https://github.com/libsdl-org/SDL/issues/5033 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.GetSceneRenderer().ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); CConfigDB::Shutdown(); TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); debug_filter_add("FILES"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); // On the first Init (INIT_MODS), check for command-line arguments // or use the default mods from the config and enable those. // On later engine restarts (e.g. the mod selector), we will skip this path, // to avoid overwriting the newly selected mods. if (flags & INIT_MODS) { ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); std::vector mods; if (args.Has("mod")) mods = args.GetMultiple("mod"); else { CStr modsStr; CFG_GET_VAL("mod.enabledmods", modsStr); boost::split(mods, modsStr, boost::algorithm::is_space(), boost::token_compress_on); } if (!g_Mods.EnableMods(mods, flags & INIT_MODS_PUBLIC)) { // In non-visual mode, fail entirely. if (args.Has("autostart-nonvisual")) { LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods.GetIncompatibleMods(), ", ")); return false; } } } // If there are incompatible mods, switch to the mod selector so players can resolve the problem. if (g_Mods.GetIncompatibleMods().empty()) MountMods(Paths(args), g_Mods.GetEnabledMods()); else MountMods(Paths(args), { "mod" }); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if (RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } g_RenderingOptions.ReadConfigAndSetupHooks(); // create renderer new CRenderer; InitInput(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(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 std::shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { Script::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) Script::SetProperty(rq, data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } bool InitNonVisual(const CmdLineArgs& args) { return Autostart(args); } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } // TODO: this essentially duplicates the CGUI logic to load directory or scripts. // NB: this won't make sure to not double-load scripts, unlike the GUI. void AutostartLoadScript(const ScriptInterface& scriptInterface, const VfsPath& path) { if (path.IsDirectory()) { VfsPaths pathnames; vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); for (const VfsPath& file : pathnames) scriptInterface.LoadGlobalScriptFile(file); } else scriptInterface.LoadGlobalScriptFile(path); } // TODO: this essentially duplicates the CGUI function CParamNode GetTemplate(const std::string& templateName) { // This is very cheap to create so let's just do it every time. CTemplateLoader templateLoader; - const CParamNode& templateRoot = templateLoader.GetTemplateFileData(templateName).GetChild("Entity"); + const CParamNode& templateRoot = templateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only). * Use random for a random civ. * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { if (!args.Has("autostart-client") && !args.Has("autostart")) return false; // Get optional playername. CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Create some scriptinterface to store the js values for the settings. ScriptInterface scriptInterface("Engine", "Game Setup", g_ScriptContext); ScriptRequest rq(scriptInterface); // We use the javascript gameSettings to handle options, but that requires running JS. // Since we don't want to use the full Gui manager, we load an entrypoint script // that can run the priviledged "LoadScript" function, and then call the appropriate function. ScriptFunction::Register<&AutostartLoadScript>(rq, "LoadScript"); // Load the entire folder to allow mods to extend the entrypoint without copying the whole file. AutostartLoadScript(scriptInterface, VfsPath(L"autostart/")); // Provide some required functions to the script. if (args.Has("autostart-nonvisual")) ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); else { JSI_GUIManager::RegisterScriptFunctions(rq); // TODO: this loads pregame, which is hardcoded to exist by various code paths. That ought be changed. InitPs(false, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), JS::UndefinedHandleValue); } JSI_Game::RegisterScriptFunctions(rq); JSI_Main::RegisterScriptFunctions(rq); JSI_Simulation::RegisterScriptFunctions(rq); JSI_VFS::RegisterScriptFunctions_ReadWriteAnywhere(rq); JSI_Network::RegisterScriptFunctions(rq); JS::RootedValue sessionInitData(rq.cx); if (args.Has("autostart-client")) { CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; Script::CreateObject( rq, &sessionInitData, "playerName", userName, "ip", ip, "port", PS_DEFAULT_PORT, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartClient", sessionInitData, true)) return false; bool shouldQuit = false; while (!shouldQuit) { g_NetClient->Poll(); ScriptFunction::Call(rq, global, "onTick", shouldQuit); std::this_thread::sleep_for(std::chrono::microseconds(200)); } if (args.Has("autostart-nonvisual")) { LDR_NonprogressiveLoad(); g_Game->ReallyStartGame(); } return true; } CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); Script::CreateObject(rq, &attrs); Script::CreateObject(rq, &settings); Script::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } Script::SetProperty(rq, settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway Script::CreateObject(rq, &player, "Civ", "athen"); Script::SetPropertyInt(rq, playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios") mapType = "scenario"; else if (mapDirectory == L"skirmishes") mapType = "skirmish"; else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } Script::SetProperty(rq, attrs, "mapType", mapType); Script::SetProperty(rq, attrs, "map", "maps/" + autoStartName); Script::SetProperty(rq, settings, "mapType", mapType); Script::SetProperty(rq, settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (Script::GetPropertyInt(rq, playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; Script::SetProperty(rq, currentPlayer, "Team", teamID); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); Script::SetProperty(rq, settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "AI", aiArgs[i].AfterFirst(":")); Script::SetProperty(rq, currentPlayer, "AIDiff", 3); Script::SetProperty(rq, currentPlayer, "AIBehavior", "balanced"); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "Civ", civArgs[i].AfterFirst(":")); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (Script::HasProperty(rq, settings, "TriggerScripts")) { Script::GetProperty(rq, settings, "TriggerScripts", &triggerScripts); Script::FromJSVal(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } Script::ToJSVal(rq, &triggerScripts, triggerScriptsVector); Script::SetProperty(rq, settings, "TriggerScripts", triggerScripts); std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); Script::SetProperty(rq, settings, "VictoryConditions", victoryConditions); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); Script::SetProperty(rq, settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); Script::SetProperty(rq, settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); Script::SetProperty(rq, settings, "RelicCount", relicCount); // Add player data to map settings. Script::SetProperty(rq, settings, "PlayerData", playerData); // Add map settings to game attributes. Script::SetProperty(rq, attrs, "settings", settings); if (args.Has("autostart-host")) { int maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerName", userName, "port", PS_DEFAULT_PORT, "maxPlayers", maxPlayers, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartHost", sessionInitData, true)) return false; // In MP host mode, we need to wait until clients have loaded. bool shouldQuit = false; while (!shouldQuit) { g_NetClient->Poll(); ScriptFunction::Call(rq, global, "onTick", shouldQuit); std::this_thread::sleep_for(std::chrono::microseconds(200)); } } else { JS::RootedValue localPlayer(rq.cx); Script::CreateObject( rq, &localPlayer, "player", args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1, "name", userName); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); Script::SetProperty(rq, playerAssignments, "local", localPlayer); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartHost", sessionInitData, false)) return false; } if (args.Has("autostart-nonvisual")) { LDR_NonprogressiveLoad(); g_Game->ReallyStartGame(); } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); JS::RootedValue localPlayer(rq.cx); Script::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); Script::SetProperty(rq, playerAssignments, "local", localPlayer); JS::RootedValue sessionInitData(rq.cx); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); return true; } void CancelLoad(const CStrW& message) { std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && Script::HasProperty(rq, global, "cancelOnLoadGameError")) ScriptFunction::CallVoid(rq, global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 27322) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 27323) @@ -1,1120 +1,1120 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TemplateLoader.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/StructuredClone.h" #include "scriptinterface/JSON.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/SerializedTypes.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" extern void QuitEngine(); /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * The original idea was to run CAIWorker in a separate thread to prevent * slow AIs from impacting framerate. However, copying the game-state every turn * proved difficult and rather slow itself (and isn't threadable, obviously). * For these reasons, the design was changed to a single-thread, same-compartment, different-realm design. * The AI can therefore directly use the simulation data via the 'Sim' & 'SimEngine' globals. * As a result, a lof of the code is still designed to be "thread-ready", but this no longer matters. * * TODO: despite the above, it would still be useful to allow the AI to run tasks asynchronously (and off-thread). * This could be implemented by having a separate JS runtime in a different thread, * that runs tasks and returns after a distinct # of simulation turns (to maintain determinism). * * Note also that the RL Interface, by default, uses the 'AI representation'. * This representation, alimented by the JS AIInterface/AIProxy tandem, is likely to grow smaller over time * as the AI uses more sim data directly. */ /** * AI computation orchestator for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, std::shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetGeneralJSContext()) { } bool Initialise() { // LoadScripts will only load each script once even though we call it for each player if (!m_Worker.LoadScripts(m_AIName)) return false; ScriptRequest rq(m_ScriptInterface); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(rq.cx); m_Worker.LoadMetadata(path, &metadata); if (metadata.isUndefined()) { LOGERROR("Failed to create AI player: can't find %s", path.string8()); return false; } // Get the constructor name from the metadata std::string moduleName; std::string constructor; JS::RootedValue objectWithConstructor(rq.cx); // object that should contain the constructor function JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!Script::HasProperty(rq, metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } Script::GetProperty(rq, metadata, "moduleName", moduleName); if (!Script::GetProperty(rq, global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName); return false; } if (!Script::GetProperty(rq, metadata, "constructor", constructor)) { LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8()); return false; } // Get the constructor function from the loaded scripts if (!Script::GetProperty(rq, objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } Script::GetProperty(rq, metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); Script::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); Script::SetProperty(rq, settings, "templates", m_Worker.m_EntityTemplates, false); } JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings.get())); m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj); if (m_Obj.get().isNull()) { LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor); return false; } return true; } void Run(JS::HandleValue state, int playerID) { m_Commands.clear(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, m_Obj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI) { m_Commands.clear(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, m_Obj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. std::shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector commands; }; CAIWorker() : m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false) { } ~CAIWorker() { // Init will always be called. JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); } void Init(const ScriptInterface& simInterface) { // Create the script interface in the same compartment as the simulation interface. // This will allow us to directly share data from the sim to the AI (and vice versa, should the need arise). m_ScriptInterface = std::make_shared("Engine", "AI", simInterface); ScriptRequest rq(m_ScriptInterface); m_EntityTemplates.init(rq.cx); m_SharedAIObj.init(rq.cx); m_PassabilityMapVal.init(rq.cx); m_TerritoryMapVal.init(rq.cx); m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); { ScriptRequest simrq(simInterface); // Register the sim globals for easy & explicit access. Mark it replaceable for hotloading. JS::RootedValue global(rq.cx, simrq.globalValue()); m_ScriptInterface->SetGlobal("Sim", global, true); JS::RootedValue scope(rq.cx, JS::ObjectValue(*simrq.nativeScope.get())); m_ScriptInterface->SetGlobal("SimEngine", scope, true); } #define REGISTER_FUNC_NAME(func, name) \ ScriptFunction::Register<&CAIWorker::func, ScriptInterface::ObjectFromCBData>(rq, name); REGISTER_FUNC_NAME(PostCommand, "PostCommand"); REGISTER_FUNC_NAME(LoadScripts, "IncludeModule"); ScriptFunction::Register(rq, "Exit"); REGISTER_FUNC_NAME(ComputePathScript, "ComputePath"); REGISTER_FUNC_NAME(DumpImage, "DumpImage"); REGISTER_FUNC_NAME(GetTemplate, "GetTemplate"); #undef REGISTER_FUNC_NAME JSI_VFS::RegisterScriptFunctions_ReadOnlySimulation(rq); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } bool LoadScripts(const std::wstring& moduleName) { // Ignore modules that are already loaded if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedModules.insert(moduleName); // Load and execute *.js VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0) { LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName)); return false; } for (const VfsPath& path : pathnames) { if (!m_ScriptInterface->LoadGlobalScriptFile(path)) { LOGERROR("Failed to load script %s", path.string8()); return false; } } return true; } void PostCommand(int playerid, JS::HandleValue cmd) { ScriptRequest rq(m_ScriptInterface); for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(Script::WriteStructuredClone(rq, cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } JS::Value ComputePathScript(JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { ScriptRequest rq(m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); Script::FromJSVal(rq, position, pos); Script::FromJSVal(rq, goal, goalPos); ComputePath(pos, goalPos, passClass, waypoints); Script::ToJSVal(rq, &retVal, waypoints); return retVal; } void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints) { WaypointPath ret; PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y }; m_LongPathfinder.ComputePath(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret); for (Waypoint& wp : ret.m_Waypoints) waypoints.emplace_back(wp.x, wp.z); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); - return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); + return m_TemplateLoader.GetTemplateFileData(name).GetOnlyChild(); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ void DumpImage(const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); std::shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } void SetRNGSeed(u32 seed) { m_RNG.seed(seed); } bool TryLoadSharedComponent() { ScriptRequest rq(m_ScriptInterface); // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; if (LoadScripts(L"common-api")) m_HasSharedComponent = true; else return false; // mainly here for the error messages OsPath path = L"simulation/ai/common-api/"; // Constructor name is SharedScript, it's in the module API3 // TODO: Hardcoding this is bad, we need a smarter way. JS::RootedValue AIModule(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!Script::GetProperty(rq, global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!Script::GetProperty(rq, AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue playersID(rq.cx); Script::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, m_Players[i]->m_Player); Script::SetPropertyInt(rq, playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); Script::CreateObject( rq, &settings, "players", playersID, "templates", m_EntityTemplates); JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings)); m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj); if (m_SharedAIObj.get().isNull()) { LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior) { std::shared_ptr ai = std::make_shared(*this, aiName, player, difficulty, behavior, m_ScriptInterface); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const Script::StructuredClone& gameState, const Grid& passabilityMap, const Grid& territoryMap, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { // this will be run last by InitGame.js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); Script::ReadStructuredClone(rq, gameState, &state); Script::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); Script::ToJSVal(rq, &m_TerritoryMapVal, territoryMap); m_PassabilityMap = passabilityMap; m_NonPathfindingPassClasses = nonPathfindingPassClassMasks; m_PathfindingPassClasses = pathfindingPassClassMasks; m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); if (m_HasSharedComponent) { Script::SetProperty(rq, state, "passabilityMap", m_PassabilityMapVal, true); Script::SetProperty(rq, state, "territoryMap", m_TerritoryMapVal, true); ScriptFunction::CallVoid(rq, m_SharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void UpdateGameState(const Script::StructuredClone& gameState) { ENSURE(m_CommandsComputed); m_GameState = gameState; } void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { ENSURE(m_CommandsComputed); bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H; m_PassabilityMap = passabilityMap; if (globallyDirty) { m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } else { m_LongPathfinder.Update(&m_PassabilityMap); m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid); } ScriptRequest rq(m_ScriptInterface); if (dimensionChange || justDeserialized) Script::ToJSVal(rq, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, nogc), m_PassabilityMap.m_Data, nbytes); } } void UpdateTerritoryMap(const Grid& territoryMap) { ENSURE(m_CommandsComputed); bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H; m_TerritoryMap = territoryMap; ScriptRequest rq(m_ScriptInterface); if (dimensionChange) Script::ToJSVal(rq, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, nogc), m_TerritoryMap.m_Data, nbytes); } } void StartComputation() { m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void LoadEntityTemplates(const std::vector >& templates) { ScriptRequest rq(m_ScriptInterface); m_HasLoadedEntityTemplates = true; Script::CreateObject(rq, &m_EntityTemplates); JS::RootedValue val(rq.cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(rq, false, &val); Script::SetProperty(rq, m_EntityTemplates, templates[i].first.c_str(), val, true); } } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { if (m_Players.empty()) return; ScriptRequest rq(m_ScriptInterface); std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) serializer.ScriptVal("sharedData", &m_SharedAIObj); for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { JS::RootedValue val(rq.cx); Script::ReadStructuredClone(rq, m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } serializer.ScriptVal("data", &m_Players[i]->m_Obj); } // AI pathfinder Serializer(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(serializer, "pathfinding pass classes", m_PathfindingPassClasses); serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W); serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H); serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data, m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData)); } void Deserialize(std::istream& stream, u32 numAis) { m_PlayerMetadata.clear(); m_Players.clear(); if (numAis == 0) return; ScriptRequest rq(m_ScriptInterface); ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad CStdDeserializer deserializer(*m_ScriptInterface, stream); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); deserializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { TryLoadSharedComponent(); deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; u8 difficulty; std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); deserializer.String("behavior", behavior, 1, 256); if (!AddPlayer(name, player, difficulty, behavior)) throw PSERROR_Deserialize_ScriptError(); u32 numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { JS::RootedValue val(rq.cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(Script::WriteStructuredClone(rq, val)); } deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj); } // AI pathfinder Serializer(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(deserializer, "pathfinding pass classes", m_PathfindingPassClasses); u16 mapW, mapH; deserializer.NumberU16_Unbounded("pathfinder grid w", mapW); deserializer.NumberU16_Unbounded("pathfinder grid h", mapH); m_PassabilityMap = Grid(mapW, mapH); deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData)); m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses); } int getPlayerSize() { return m_Players.size(); } private: static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc) { for (std::pair>& metadata : m_PlayerMetadata) JS::TraceEdge(trc, &metadata.second, "CAIWorker::m_PlayerMetadata"); } void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata Script::ReadJSONFile(ScriptRequest(m_ScriptInterface), path, out); m_PlayerMetadata[path] = JS::Heap(out); return; } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); Script::ReadStructuredClone(rq, m_GameState, &state); Script::SetProperty(rq, state, "passabilityMap", m_PassabilityMapVal, true); Script::SetProperty(rq, state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // Script::FreezeObject(rq, state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); ScriptFunction::CallVoid(rq, m_SharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } std::shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; JS::PersistentRootedValue m_EntityTemplates; bool m_HasLoadedEntityTemplates; std::map> m_PlayerMetadata; std::vector> m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; JS::PersistentRootedValue m_SharedAIObj; std::vector m_Commands; std::set m_LoadedModules; Script::StructuredClone m_GameState; Grid m_PassabilityMap; JS::PersistentRootedValue m_PassabilityMapVal; Grid m_TerritoryMap; JS::PersistentRootedValue m_TerritoryMapVal; std::map m_NonPathfindingPassClasses; std::map m_PathfindingPassClasses; HierarchicalPathfinder m_HierarchicalPathfinder; LongPathfinder m_LongPathfinder; bool m_CommandsComputed; CTemplateLoader m_TemplateLoader; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager final : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } void Init(const CParamNode& UNUSED(paramNode)) override { m_Worker.Init(GetSimContext().GetScriptInterface()); m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } void Deinit() override { } void Serialize(ISerializer& serialize) override { serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize()); // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override { Init(paramNode); u32 numAis; deserialize.NumberU32_Unbounded("num ais", numAis); if (numAis > 0) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) override { LoadUsedEntityTemplates(); m_Worker.AddPlayer(id, player, difficulty, behavior); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } void SetRNGSeed(u32 seed) override { m_Worker.SetRNGSeed(seed); } void TryLoadSharedComponent() override { m_Worker.TryLoadSharedComponent(); } void RunGamestateInit() override { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. JS::RootedValue state(rq.cx); cmpAIInterface->GetFullRepresentation(&state, true); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID)) territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); LoadPathfinderClasses(state); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; if (cmpPathfinder) cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.RunGamestateInit(Script::WriteStructuredClone(rq, state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } void StartComputation() override { PROFILE("AI setup"); const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(rq.cx); if (m_JustDeserialized) cmpAIInterface->GetFullRepresentation(&state, false); else cmpAIInterface->GetRepresentation(&state); LoadPathfinderClasses(state); // add the pathfinding classes to it // Update the game state m_Worker.UpdateGameState(Script::WriteStructuredClone(rq, state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized)) { const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid(); m_Worker.UpdateTerritoryMap(territoryMap); } m_Worker.StartComputation(); m_JustDeserialized = false; } void PushCommands() override { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue clonedCommandVal(rq.cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { Script::ReadStructuredClone(rq, commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal); } } } private: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue classesVal(rq.cx); Script::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) Script::SetProperty(rq, classesVal, it->first.c_str(), it->second, true); Script::SetProperty(rq, state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 27322) +++ ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 27323) @@ -1,257 +1,257 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedTypes.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/TemplateLoader.h" #include "ps/XML/RelaxNG.h" class CCmpTemplateManager final : public ICmpTemplateManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(TemplateManager) static std::string GetSchema() { return ""; } void Init(const CParamNode& UNUSED(paramNode)) override { m_DisableValidation = false; m_Validator.LoadGrammar(GetSimContext().GetComponentManager().GenerateSchema()); // TODO: handle errors loading the grammar here? // TODO: support hotloading changes to the grammar } void Deinit() override { } void Serialize(ISerializer& serialize) override { std::map> templateMap; for (const std::pair& templateEnt : m_LatestTemplates) if (!ENTITY_IS_LOCAL(templateEnt.first)) templateMap[templateEnt.second].push_back(templateEnt.first); Serializer(serialize, "templates", templateMap); } void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override { Init(paramNode); std::map> templateMap; Serializer(deserialize, "templates", templateMap); for (const std::pair>& mapEl : templateMap) for (entity_id_t id : mapEl.second) m_LatestTemplates[id] = mapEl.first; } void HandleMessage(const CMessage& msg, bool UNUSED(global)) override { switch (msg.GetType()) { case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); // Clean up m_LatestTemplates so it doesn't record any data for destroyed entities m_LatestTemplates.erase(msgData.entity); break; } } } void DisableValidation() override { m_DisableValidation = true; } const CParamNode* LoadTemplate(entity_id_t ent, const std::string& templateName) override; const CParamNode* GetTemplate(const std::string& templateName) override; const CParamNode* GetTemplateWithoutValidation(const std::string& templateName) override; bool TemplateExists(const std::string& templateName) const override; const CParamNode* LoadLatestTemplate(entity_id_t ent) override; std::string GetCurrentTemplateName(entity_id_t ent) const override; std::vector FindAllTemplates(bool includeActors) const override; std::vector> GetCivData() override; std::vector FindUsedTemplates() const override; std::vector GetEntitiesUsingTemplate(const std::string& templateName) const override; private: // Template loader CTemplateLoader m_templateLoader; // Entity template XML validator RelaxNGValidator m_Validator; // Disable validation, for test cases bool m_DisableValidation; // Map from template name to schema validation status. // (Some files, e.g. inherited parent templates, may not be valid themselves but we still need to load // them and use them; we only reject invalid templates that were requested directly by GetTemplate/etc) std::map m_TemplateSchemaValidity; // Remember the template used by each entity, so we can return them // again for deserialization. std::map m_LatestTemplates; }; REGISTER_COMPONENT_TYPE(TemplateManager) const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std::string& templateName) { m_LatestTemplates[ent] = templateName; return GetTemplate(templateName); } const CParamNode* CCmpTemplateManager::GetTemplate(const std::string& templateName) { const CParamNode& fileData = m_templateLoader.GetTemplateFileData(templateName); if (!fileData.IsOk()) return NULL; if (!m_DisableValidation) { // Compute validity, if it's not computed before if (m_TemplateSchemaValidity.find(templateName) == m_TemplateSchemaValidity.end()) { m_TemplateSchemaValidity[templateName] = m_Validator.Validate(templateName, fileData.ToXMLString()); // Show error on the first failure to validate the template if (!m_TemplateSchemaValidity[templateName]) LOGERROR("Failed to validate entity template '%s'", templateName.c_str()); } // Refuse to return invalid templates if (!m_TemplateSchemaValidity[templateName]) return NULL; } - const CParamNode& templateRoot = fileData.GetChild("Entity"); + const CParamNode& templateRoot = fileData.GetOnlyChild(); if (!templateRoot.IsOk()) { // The validator should never let this happen LOGERROR("Invalid root element in entity template '%s'", templateName.c_str()); return NULL; } return &templateRoot; } const CParamNode* CCmpTemplateManager::GetTemplateWithoutValidation(const std::string& templateName) { - const CParamNode& templateRoot = m_templateLoader.GetTemplateFileData(templateName).GetChild("Entity"); + const CParamNode& templateRoot = m_templateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) return NULL; return &templateRoot; } bool CCmpTemplateManager::TemplateExists(const std::string& templateName) const { return m_templateLoader.TemplateExists(templateName); } const CParamNode* CCmpTemplateManager::LoadLatestTemplate(entity_id_t ent) { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return NULL; return LoadTemplate(ent, it->second); } std::string CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent) const { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return ""; return it->second; } std::vector CCmpTemplateManager::FindAllTemplates(bool includeActors) const { ETemplatesType templatesType = includeActors ? ALL_TEMPLATES : SIMULATION_TEMPLATES; return m_templateLoader.FindTemplates("", true, templatesType); } std::vector> CCmpTemplateManager::GetCivData() { std::vector> data; std::vector names = m_templateLoader.FindTemplatesUnrestricted("special/players/", false); data.reserve(names.size()); for (const std::string& name : names) { const CParamNode& identity = GetTemplate(name)->GetChild("Identity"); data.push_back(std::vector { identity.GetChild("Civ").ToWString(), identity.GetChild("GenericName").ToWString() }); } return data; } std::vector CCmpTemplateManager::FindUsedTemplates() const { std::vector usedTemplates; for (const std::pair& p : m_LatestTemplates) if (std::find(usedTemplates.begin(), usedTemplates.end(), p.second) == usedTemplates.end()) usedTemplates.push_back(p.second); return usedTemplates; } /** * Get the list of entities using the specified template */ std::vector CCmpTemplateManager::GetEntitiesUsingTemplate(const std::string& templateName) const { std::vector entities; for (const std::pair& p : m_LatestTemplates) if (p.second == templateName) entities.push_back(p.first); return entities; } Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 27322) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 27323) @@ -1,1162 +1,1163 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ComponentManager.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "scriptinterface/FunctionWrapper.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/DynamicSubscription.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include /** * Used for script-only message types. */ class CMessageScripted final : public CMessage { public: virtual int GetType() const { return mtid; } virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); } virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); } virtual JS::Value ToJSVal(const ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); } CMessageScripted(const ScriptInterface& scriptInterface, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(scriptInterface.GetGeneralJSContext(), msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; CComponentManager::CComponentManager(CSimContext& context, std::shared_ptr cx, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", cx), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); m_ScriptInterface.SetCallbackData(static_cast (this)); m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); // For component script tests, the test system sets up its own scripted implementation of // these functions, so we skip registering them here in those cases if (!skipScriptFunctions) { JSI_VFS::RegisterScriptFunctions_ReadOnlySimulation(m_ScriptInterface); ScriptRequest rq(m_ScriptInterface); constexpr ScriptFunction::ObjectGetter Getter = &ScriptInterface::ObjectFromCBData; ScriptFunction::Register<&CComponentManager::Script_RegisterComponentType, Getter>(rq, "RegisterComponentType"); ScriptFunction::Register<&CComponentManager::Script_RegisterSystemComponentType, Getter>(rq, "RegisterSystemComponentType"); ScriptFunction::Register<&CComponentManager::Script_ReRegisterComponentType, Getter>(rq, "ReRegisterComponentType"); ScriptFunction::Register<&CComponentManager::Script_RegisterInterface, Getter>(rq, "RegisterInterface"); ScriptFunction::Register<&CComponentManager::Script_RegisterMessageType, Getter>(rq, "RegisterMessageType"); ScriptFunction::Register<&CComponentManager::Script_RegisterGlobal, Getter>(rq, "RegisterGlobal"); ScriptFunction::Register<&CComponentManager::Script_GetEntitiesWithInterface, Getter>(rq, "GetEntitiesWithInterface"); ScriptFunction::Register<&CComponentManager::Script_GetComponentsWithInterface, Getter>(rq, "GetComponentsWithInterface"); ScriptFunction::Register<&CComponentManager::Script_PostMessage, Getter>(rq, "PostMessage"); ScriptFunction::Register<&CComponentManager::Script_BroadcastMessage, Getter>(rq, "BroadcastMessage"); ScriptFunction::Register<&CComponentManager::Script_AddEntity, Getter>(rq, "AddEntity"); ScriptFunction::Register<&CComponentManager::Script_AddLocalEntity, Getter>(rq, "AddLocalEntity"); ScriptFunction::Register<&CComponentManager::QueryInterface, Getter>(rq, "QueryInterface"); ScriptFunction::Register<&CComponentManager::DestroyComponentsSoon, Getter>(rq, "DestroyEntity"); ScriptFunction::Register<&CComponentManager::FlushDestroyedComponents, Getter>(rq, "FlushDestroyedEntities"); ScriptFunction::Register<&CComponentManager::Script_GetTemplate, Getter>(rq, "GetTemplate"); } // Globalscripts may use VFS script functions m_ScriptInterface.LoadGlobalScripts(); // Define MT_*, IID_* as script globals, and store their names #define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name); #define INTERFACE(name) \ m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \ m_InterfaceIdsByName[#name] = IID_##name; #define COMPONENT(name) #include "simulation2/TypeList.h" #undef MESSAGE #undef INTERFACE #undef COMPONENT m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY); m_ScriptInterface.SetGlobal("INVALID_PLAYER", (int)INVALID_PLAYER); m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); m_ComponentsByInterface.resize(IID__LastNative); ResetState(); } CComponentManager::~CComponentManager() { ResetState(); } void CComponentManager::LoadComponentTypes() { #define MESSAGE(name) \ RegisterMessageType(MT_##name, #name); #define INTERFACE(name) \ extern void RegisterComponentInterface_##name(ScriptInterface&); \ RegisterComponentInterface_##name(m_ScriptInterface); #define COMPONENT(name) \ extern void RegisterComponentType_##name(CComponentManager&); \ m_CurrentComponent = CID_##name; \ RegisterComponentType_##name(*this); #include "simulation2/TypeList.h" m_CurrentComponent = CID__Invalid; #undef MESSAGE #undef INTERFACE #undef COMPONENT } bool CComponentManager::LoadScript(const VfsPath& filename, bool hotload) { m_CurrentlyHotloading = hotload; CVFSFile file; PSRETURN loadOk = file.Load(g_VFS, filename); if (loadOk != PSRETURN_OK) // VFS will log the failed file and the reason return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); m_CurrentlyHotloading = false; return ok; } void CComponentManager::Script_RegisterComponentType_Common(int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { ScriptRequest rq(m_ScriptInterface); // Find the C++ component that wraps the interface int cidWrapper = GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { ScriptException::Raise(rq, "Invalid interface id"); return; } const ComponentType& ctWrapper = m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading ComponentTypeId cid = LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { ScriptException::Raise(rq, "ReRegistering component type that was not registered before '%s'", cname.c_str()); return; } // Allocate a new cid number cid = m_NextScriptComponentTypeId++; m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: if (!m_CurrentlyHotloading && !reRegister) { ScriptException::Raise(rq, "Registering component type with already-registered name '%s'", cname.c_str()); return; } const ComponentType& ctPrevious = m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { ScriptException::Raise(rq, "Loading script component type with same name '%s' as native component", cname.c_str()); return; } // We don't support changing the IID of a component type (it would require fiddling // around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity) if (ctPrevious.iid != iid) { // ...though it only matters if any components exist with this type if (!m_ComponentsByTypeId[cid].empty()) { ScriptException::Raise(rq, "Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; for (it = m_LocalMessageSubscriptions.begin(); it != m_LocalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } for (it = m_GlobalMessageSubscriptions.begin(); it != m_GlobalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } mustReloadComponents = true; } JS::RootedValue protoVal(rq.cx); if (!Script::GetProperty(rq, ctor, "prototype", &protoVal)) { ScriptException::Raise(rq, "Failed to get property 'prototype'"); return; } if (!protoVal.isObject()) { ScriptException::Raise(rq, "Component has no constructor"); return; } std::string schema = ""; if (Script::HasProperty(rq, protoVal, "Schema")) Script::GetProperty(rq, protoVal, "Schema", schema); // Construct a new ComponentType, using the wrapper's alloc functions ComponentType ct{ CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, std::make_unique(rq.cx, ctor) }; m_ComponentTypesById[cid] = std::move(ct); m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: std::vector methods; if (!Script::EnumeratePropertyNames(rq, protoVal, false, methods)) { ScriptException::Raise(rq, "Failed to enumerate component properties."); return; } for (const std::string& method : methods) { if (std::string_view{method}.substr(0, 2) != "On") continue; std::string_view name{std::string_view{method}.substr(2)}; // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (std::string_view{name}.substr(0, 6) == "Global") { isGlobal = true; name.remove_prefix(6); } auto mit = m_MessageTypeIdsByName.find(std::string{name}); if (mit == m_MessageTypeIdsByName.end()) { ScriptException::Raise(rq, "Registered component has unrecognized '%s' message handler method", method.c_str()); return; } if (isGlobal) SubscribeGloballyToMessageType(mit->second); else SubscribeToMessageType(mit->second); } m_CurrentComponent = CID__Invalid; if (mustReloadComponents) { // For every script component with this cid, we need to switch its // prototype from the old constructor's prototype property to the new one's const std::map& comps = m_ComponentsByTypeId[cid]; std::map::const_iterator eit = comps.begin(); for (; eit != comps.end(); ++eit) { JS::RootedValue instance(rq.cx, eit->second->GetJSInstance()); if (!instance.isNull()) m_ScriptInterface.SetPrototype(instance, protoVal); } } } void CComponentManager::Script_RegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, false, false); m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, false, true); m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, true, false); } void CComponentManager::Script_RegisterInterface(const std::string& name) { std::map::iterator it = m_InterfaceIdsByName.find(name); if (it != m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!m_CurrentlyHotloading) { ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering interface with already-registered name '%s'", name.c_str()); } return; } // IIDs start at 1, so size+1 is the next unused one size_t id = m_InterfaceIdsByName.size() + 1; m_InterfaceIdsByName[name] = (InterfaceId)id; m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterMessageType(const std::string& name) { std::map::iterator it = m_MessageTypeIdsByName.find(name); if (it != m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!m_CurrentlyHotloading) { ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering message type with already-registered name '%s'", name.c_str()); } return; } // MTIDs start at 1, so size+1 is the next unused one size_t id = m_MessageTypeIdsByName.size() + 1; RegisterMessageType((MessageTypeId)id, name.c_str()); m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterGlobal(const std::string& name, JS::HandleValue value) { m_ScriptInterface.SetGlobal(name.c_str(), value, m_CurrentlyHotloading); } const CParamNode& CComponentManager::Script_GetTemplate(const std::string& templateName) { static CParamNode nullNode(false); ICmpTemplateManager* cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { LOGERROR("Template manager is not loaded"); return nullNode; } const CParamNode* tmpl = cmpTemplateManager->GetTemplate(templateName); if (!tmpl) return nullNode; return *tmpl; } std::vector CComponentManager::Script_GetEntitiesWithInterface(int iid) { std::vector ret; const InterfaceListUnordered& ents = GetEntitiesWithInterfaceUnordered(iid); for (InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) if (!ENTITY_IS_LOCAL(it->first)) ret.push_back(it->first); std::sort(ret.begin(), ret.end()); return ret; } std::vector CComponentManager::Script_GetComponentsWithInterface(int iid) { std::vector ret; InterfaceList ents = GetEntitiesWithInterface(iid); for (InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } CMessage* CComponentManager::ConstructMessage(int mtid, JS::HandleValue data) { if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here) LOGERROR("PostMessage with invalid message type ID '%d'", mtid); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, m_ScriptInterface, data); } else { return new CMessageScripted(m_ScriptInterface, mtid, m_MessageTypeNamesById[mtid], data); } } void CComponentManager::Script_PostMessage(int ent, int mtid, JS::HandleValue data) { CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error PostMessage(ent, *msg); delete msg; } void CComponentManager::Script_BroadcastMessage(int mtid, JS::HandleValue data) { CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error BroadcastMessage(*msg); delete msg; } int CComponentManager::Script_AddEntity(const std::wstring& templateName) { // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files return AddEntity(templateName, AllocateNewEntity()); } int CComponentManager::Script_AddLocalEntity(const std::wstring& templateName) { // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files return AddEntity(templateName, AllocateNewLocalEntity()); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents in reverse order of creation. std::map >::reverse_iterator iit = m_ComponentsByTypeId.rbegin(); for (; iit != m_ComponentsByTypeId.rend(); ++iit) { std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) ifcit->clear(); m_ComponentsByTypeId.clear(); // Delete all SEntityComponentCaches std::unordered_map::iterator ccit = m_ComponentCaches.begin(); for (; ccit != m_ComponentCaches.end(); ++ccit) free(ccit->second); m_ComponentCaches.clear(); m_SystemEntity = CEntityHandle(); m_DestructionQueue.clear(); // Reset IDs m_NextEntityId = SYSTEM_ENTITY + 1; m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::SetRNGSeed(u32 seed) { m_RNG.seed(seed); } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_Native, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_ScriptWrapper, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType } void CComponentManager::MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid) { m_ScriptedSystemComponents.push_back(cid); } void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) { m_MessageTypeIdsByName[name] = mtid; m_MessageTypeNamesById[mtid] = name; } void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_LocalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_GlobalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; for (it = m_DynamicMessageSubscriptionsNonsync.begin(); it != m_DynamicMessageSubscriptionsNonsync.end(); ++it) { it->second.Flatten(); } } void CComponentManager::DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enable) { if (enable) { bool newlyInserted = m_DynamicMessageSubscriptionsNonsyncByComponent[component].insert(mtid).second; if (newlyInserted) m_DynamicMessageSubscriptionsNonsync[mtid].Add(component); } else { size_t numRemoved = m_DynamicMessageSubscriptionsNonsyncByComponent[component].erase(mtid); if (numRemoved) m_DynamicMessageSubscriptionsNonsync[mtid].Remove(component); } } void CComponentManager::RemoveComponentDynamicSubscriptions(IComponent* component) { std::map >::iterator it = m_DynamicMessageSubscriptionsNonsyncByComponent.find(component); if (it == m_DynamicMessageSubscriptionsNonsyncByComponent.end()) return; std::set::iterator mtit; for (mtit = it->second.begin(); mtit != it->second.end(); ++mtit) { m_DynamicMessageSubscriptionsNonsync[*mtit].Remove(component); // Need to flatten the subscription lists immediately to avoid dangling IComponent* references m_DynamicMessageSubscriptionsNonsync[*mtit].Flatten(); } m_DynamicMessageSubscriptionsNonsyncByComponent.erase(it); } CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const { std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname); if (it == m_ComponentTypeIdsByName.end()) return CID__Invalid; return it->second; } std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const { std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) return ""; return it->second.name; } CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid) { if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1 return CID_UnknownScript; std::map::const_iterator it = m_ComponentTypesById.begin(); for (; it != m_ComponentTypesById.end(); ++it) if (it->second.iid == iid && it->second.type == CT_ScriptWrapper) return it->first; std::map::const_iterator iiit = m_InterfaceIdsByName.begin(); for (; iiit != m_InterfaceIdsByName.end(); ++iiit) if (iiit->second == iid) { LOGERROR("No script wrapper found for interface id %d '%s'", iid, iiit->first.c_str()); return CID__Invalid; } LOGERROR("No script wrapper found for interface id %d", iid); return CID__Invalid; } entity_id_t CComponentManager::AllocateNewEntity() { entity_id_t id = m_NextEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewLocalEntity() { entity_id_t id = m_NextLocalEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) { // TODO: ensure this ID hasn't been allocated before // (this might occur with broken map files) // Trying to actually add two entities with the same id will fail in AddEntitiy entity_id_t id = preferredId; // Ensure this ID won't be allocated again if (id >= m_NextEntityId) m_NextEntityId = id+1; // TODO: check for overflow return id; } bool CComponentManager::AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); if (!component) return false; component->Init(paramNode); return true; } void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool skipAI) { CParamNode noParam; AddComponent(m_SystemEntity, CID_TemplateManager, noParam); AddComponent(m_SystemEntity, CID_CinemaManager, noParam); AddComponent(m_SystemEntity, CID_CommandQueue, noParam); AddComponent(m_SystemEntity, CID_ObstructionManager, noParam); AddComponent(m_SystemEntity, CID_ParticleManager, noParam); AddComponent(m_SystemEntity, CID_Pathfinder, noParam); AddComponent(m_SystemEntity, CID_ProjectileManager, noParam); AddComponent(m_SystemEntity, CID_RangeManager, noParam); AddComponent(m_SystemEntity, CID_SoundManager, noParam); AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitMotionManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { for (uint32_t i = 0; i < m_ScriptedSystemComponents.size(); ++i) AddComponent(m_SystemEntity, m_ScriptedSystemComponents[i], noParam); if (!skipAI) AddComponent(m_SystemEntity, CID_AIManager, noParam); } } IComponent* CComponentManager::ConstructComponent(CEntityHandle ent, ComponentTypeId cid) { ScriptRequest rq(m_ScriptInterface); std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) { LOGERROR("Invalid component id %d", cid); return NULL; } const ComponentType& ct = it->second; ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); std::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; if (emap1.find(ent.GetId()) != emap1.end()) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; } std::map& emap2 = m_ComponentsByTypeId[cid]; // If this is a scripted component, construct the appropriate JS object first JS::RootedValue obj(rq.cx); if (ct.type == CT_Script) { m_ScriptInterface.CallConstructor(*ct.ctor, JS::HandleValueArray::empty(), &obj); if (obj.isNull()) { LOGERROR("Script component constructor failed"); return NULL; } } // Construct the new component // NB: The unit motion manager relies on components not moving in memory once constructed. IComponent* component = ct.alloc(m_ScriptInterface, obj); ENSURE(component); component->SetEntityHandle(ent); component->SetSimContext(m_SimContext); // Store a reference to the new component emap1.insert(std::make_pair(ent.GetId(), component)); emap2.insert(std::make_pair(ent.GetId(), component)); // TODO: We need to more careful about this - if an entity is constructed by a component // while we're iterating over all components, this will invalidate the iterators and everything // will break. // We probably need some kind of delayed addition, so they get pushed onto a queue and then // inserted into the world later on. (Be careful about immediation deletion in that case, too.) SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); cache->interfaces[ct.iid] = component; return component; } void CComponentManager::AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component) { // Just add it into the by-interface map, not the by-component-type map, // so it won't be considered for messages or deletion etc std::unordered_map& emap1 = m_ComponentsByInterface.at(iid); if (emap1.find(ent.GetId()) != emap1.end()) debug_warn(L"Multiple components for interface"); emap1.insert(std::make_pair(ent.GetId(), &component)); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && iid < (int)cache->numInterfaces && cache->interfaces[iid] == NULL); cache->interfaces[iid] = &component; } CEntityHandle CComponentManager::AllocateEntityHandle(entity_id_t ent) { ENSURE(!EntityExists(ent)); // Interface IDs start at 1, and SEntityComponentCache is defined with a 1-sized array, // so we need space for an extra m_InterfaceIdsByName.size() items SEntityComponentCache* cache = (SEntityComponentCache*)calloc(1, sizeof(SEntityComponentCache) + sizeof(IComponent*) * m_InterfaceIdsByName.size()); ENSURE(cache != NULL); cache->numInterfaces = m_InterfaceIdsByName.size() + 1; m_ComponentCaches[ent] = cache; return CEntityHandle(ent, cache); } CEntityHandle CComponentManager::LookupEntityHandle(entity_id_t ent, bool allowCreate) { std::unordered_map::iterator it; it = m_ComponentCaches.find(ent); if (it == m_ComponentCaches.end()) { if (allowCreate) return AllocateEntityHandle(ent); else return CEntityHandle(ent, NULL); } else return CEntityHandle(ent, it->second); } void CComponentManager::InitSystemEntity() { ENSURE(m_SystemEntity.GetId() == INVALID_ENTITY); m_SystemEntity = AllocateEntityHandle(SYSTEM_ENTITY); m_SimContext.SetSystemEntity(m_SystemEntity); } entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent) { ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { debug_warn(L"No ICmpTemplateManager loaded"); return INVALID_ENTITY; } const CParamNode* tmpl = cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); if (!tmpl) return INVALID_ENTITY; // LoadTemplate will have reported the error // This also ensures that ent does not exist CEntityHandle handle = AllocateEntityHandle(ent); // Construct a component for each child of the root element const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element if (it->first.length() && it->first[0] == '@') continue; CComponentManager::ComponentTypeId cid = LookupCID(it->first); if (cid == CID__Invalid) { LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } if (!AddComponent(handle, cid, it->second)) { LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? } CMessageCreate msg(ent); PostMessage(ent, msg); return ent; } bool CComponentManager::EntityExists(entity_id_t ent) const { return m_ComponentCaches.find(ent) != m_ComponentCaches.end(); } void CComponentManager::DestroyComponentsSoon(entity_id_t ent) { m_DestructionQueue.push_back(ent); } void CComponentManager::FlushDestroyedComponents() { PROFILE2("Flush Destroyed Components"); while (!m_DestructionQueue.empty()) { // Make a copy of the destruction queue, so that the iterators won't be invalidated if the // CMessageDestroy handlers try to destroy more entities themselves std::vector queue; queue.swap(m_DestructionQueue); for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it) { entity_id_t ent = *it; // Do nothing if invalid, destroyed, etc. if (!EntityExists(ent)) continue; CEntityHandle handle = LookupEntityHandle(ent); CMessageDestroy msg(ent); PostMessage(ent, msg); // Flatten all the dynamic subscriptions to ensure there are no dangling // references in the 'removed' lists to components we're going to delete // Some components may have dynamically unsubscribed following the Destroy message FlattenDynamicSubscriptions(); // Destroy the components, and remove from m_ComponentsByTypeId: std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { eit->second->Deinit(); RemoveComponentDynamicSubscriptions(eit->second); m_ComponentTypesById[iit->first].dealloc(eit->second); iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } } free(handle.GetComponentCache()); m_ComponentCaches.erase(ent); // Remove from m_ComponentsByInterface std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) { ifcit->erase(ent); } } } } IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return NULL; } std::unordered_map::const_iterator eit = m_ComponentsByInterface[iid].find(ent); if (eit == m_ComponentsByInterface[iid].end()) { // This entity doesn't implement this interface return NULL; } return eit->second; } CComponentManager::InterfaceList CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { std::vector > ret; if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return ret; } ret.reserve(m_ComponentsByInterface[iid].size()); std::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID return ret; } static CComponentManager::InterfaceListUnordered g_EmptyEntityMap; const CComponentManager::InterfaceListUnordered& CComponentManager::GetEntitiesWithInterfaceUnordered(InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return g_EmptyEntityMap; } return m_ComponentsByInterface[iid]; } void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("Post Message", 0.0005); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // Send the message to components of ent, that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); if (eit != emap->second.end()) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(ent, msg); } void CComponentManager::BroadcastMessage(const CMessage& msg) { // Send the message to components of all entities that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(INVALID_ENTITY, msg); } void CComponentManager::SendGlobalMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("SendGlobalMessage", 0.001); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // (Common functionality for PostMessage and BroadcastMessage) // Send the message to components of all entities that subscribed globally to this message std::map >::const_iterator it; it = m_GlobalMessageSubscriptions.find(msg.GetType()); if (it != m_GlobalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Special case: Messages for local entities shouldn't be sent to script // components that subscribed globally, so that we don't have to worry about // them accidentally picking up non-network-synchronised data. if (ENTITY_IS_LOCAL(ent)) { std::map::const_iterator cit = m_ComponentTypesById.find(*ctit); if (cit != m_ComponentTypesById.end() && cit->second.type == CT_Script) continue; } // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, true); } } // Send the message to component instances that dynamically subscribed to this message std::map::iterator dit = m_DynamicMessageSubscriptionsNonsync.find(msg.GetType()); if (dit != m_DynamicMessageSubscriptionsNonsync.end()) { dit->second.Flatten(); const std::vector& dynamic = dit->second.GetComponents(); for (size_t i = 0; i < dynamic.size(); i++) dynamic[i]->HandleMessage(msg, false); } } std::string CComponentManager::GenerateSchema() const { std::string schema = "" "" "" "" "" "0" "" "" "0" "" "" "" "" "" "" "" "" "" "" "" "" ""; std::map > interfaceComponentTypes; std::vector componentTypes; for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) { schema += "" "" "" + it->second.schema + "" "" ""; interfaceComponentTypes[it->second.iid].push_back(it->second.name); componentTypes.push_back(it->second.name); } // Declare the implementation of each interface, for documentation for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) { schema += ""; std::vector& cts = interfaceComponentTypes[it->second]; for (size_t i = 0; i < cts.size(); ++i) schema += ""; schema += ""; } // List all the component types, in alphabetical order (to match the reordering performed by CParamNode). // (We do it this way, rather than ing all the interface definitions (which would additionally perform // a check that we don't use multiple component types of the same interface in one file), because libxml2 gives // useless error messages in the latter case; this way lets it report the real error.) std::sort(componentTypes.begin(), componentTypes.end()); schema += "" - "" + "" + "" ""; for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it) schema += ""; schema += "" ""; schema += ""; return schema; } Index: ps/trunk/source/simulation2/system/ParamNode.cpp =================================================================== --- ps/trunk/source/simulation2/system/ParamNode.cpp (revision 27322) +++ ps/trunk/source/simulation2/system/ParamNode.cpp (revision 27323) @@ -1,447 +1,456 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ParamNode.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/CStrIntern.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptRequest.h" #include #include #include static CParamNode g_NullNode(false); CParamNode::CParamNode(bool isOk) : m_IsOk(isOk) { } void CParamNode::LoadXML(CParamNode& ret, const XMBData& xmb, const wchar_t* sourceIdentifier /*= NULL*/) { ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier); } void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName) { CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path, validatorName); if (ok != PSRETURN_OK) return; // (Xeromyces already logged an error) LoadXML(ret, xero, path.string().c_str()); } PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/) { CXeromyces xero; PSRETURN ok = xero.LoadString(xml); if (ok != PSRETURN_OK) return ok; ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier); return PSRETURN_OK; } void CParamNode::ApplyLayer(const XMBData& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/) { ResetScriptVal(); std::string name = xmb.GetElementString(element.GetNodeName()); CStr value = element.GetText(); bool hasSetValue = false; // Look for special attributes int at_disable = xmb.GetAttributeID("disable"); int at_replace = xmb.GetAttributeID("replace"); int at_filtered = xmb.GetAttributeID("filtered"); int at_merge = xmb.GetAttributeID("merge"); int at_op = xmb.GetAttributeID("op"); int at_datatype = xmb.GetAttributeID("datatype"); enum op { INVALID, ADD, MUL, MUL_ROUND } op = INVALID; bool replacing = false; bool filtering = false; bool merging = false; { XERO_ITER_ATTR(element, attr) { if (attr.Name == at_disable) { m_Childs.erase(name); return; } else if (attr.Name == at_replace) { m_Childs.erase(name); replacing = true; } else if (attr.Name == at_filtered) { filtering = true; } else if (attr.Name == at_merge) { if (m_Childs.find(name) == m_Childs.end()) return; merging = true; } else if (attr.Name == at_op) { if (attr.Value == "add") op = ADD; else if (attr.Value == "mul") op = MUL; else if (attr.Value == "mul_round") op = MUL_ROUND; else LOGWARNING("Invalid op '%ls'", attr.Value); } } } { XERO_ITER_ATTR(element, attr) { if (attr.Name == at_datatype && attr.Value == "tokens") { CParamNode& node = m_Childs[name]; // Split into tokens std::vector oldTokens; std::vector newTokens; if (!replacing && !node.m_Value.empty()) // ignore the old tokens if replace="" was given boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); if (!value.empty()) boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); // Merge the two lists std::vector tokens = oldTokens; for (const std::string& newToken : newTokens) { if (newToken[0] == '-') { std::vector::iterator tokenIt = std::find(tokens.begin(), tokens.end(), std::string_view{newToken}.substr(1)); if (tokenIt != tokens.end()) tokens.erase(tokenIt); else { const std::string identifier{ sourceIdentifier ? (" in '" + utf8_from_wstring(sourceIdentifier) + "'") : ""}; LOGWARNING("[ParamNode] Could not remove token " "'%s' from node '%s'%s; not present in " "list nor inherited (possible typo?)", std::string_view{newToken}.substr(1), name, identifier); } } else { if (std::find(oldTokens.begin(), oldTokens.end(), newToken) == oldTokens.end()) tokens.push_back(newToken); } } node.m_Value = boost::algorithm::join(tokens, " "); hasSetValue = true; break; } } } // Add this element as a child node CParamNode& node = m_Childs[name]; if (op != INVALID) { // TODO: Support parsing of data types other than fixed; log warnings in other cases fixed oldval = node.ToFixed(); fixed mod = fixed::FromString(value); switch (op) { case ADD: node.m_Value = (oldval + mod).ToString(); break; case MUL: node.m_Value = oldval.Multiply(mod).ToString(); break; case MUL_ROUND: node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString(); break; default: break; } hasSetValue = true; } if (!hasSetValue && !merging) node.m_Value = value; // We also need to reset node's script val, even if it has no children // or if the attributes change. node.ResetScriptVal(); // For the filtered case ChildrenMap childs; // Recurse through the element's children XERO_ITER_EL(element, child) { node.ApplyLayer(xmb, child, sourceIdentifier); if (filtering) { std::string childname = xmb.GetElementString(child.GetNodeName()); if (node.m_Childs.find(childname) != node.m_Childs.end()) childs[childname] = std::move(node.m_Childs[childname]); } } if (filtering) node.m_Childs.swap(childs); // Add the element's attributes, prefixing names with "@" XERO_ITER_ATTR(element, attr) { // Skip special attributes if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered) continue; // Add any others const char* attrName(xmb.GetAttributeString(attr.Name)); node.m_Childs[CStr("@") + attrName].m_Value = attr.Value; } } +const CParamNode& CParamNode::GetOnlyChild() const +{ + if (m_Childs.empty()) + return g_NullNode; + + ENSURE(m_Childs.size() == 1); + return m_Childs.begin()->second; +} + const CParamNode& CParamNode::GetChild(const char* name) const { ChildrenMap::const_iterator it = m_Childs.find(name); if (it == m_Childs.end()) return g_NullNode; return it->second; } bool CParamNode::IsOk() const { return m_IsOk; } const std::wstring CParamNode::ToWString() const { return wstring_from_utf8(m_Value); } const std::string& CParamNode::ToString() const { return m_Value; } const CStrIntern CParamNode::ToUTF8Intern() const { return CStrIntern(m_Value); } int CParamNode::ToInt() const { return std::strtol(m_Value.c_str(), nullptr, 10); } fixed CParamNode::ToFixed() const { return fixed::FromString(m_Value); } float CParamNode::ToFloat() const { return std::strtof(m_Value.c_str(), nullptr); } bool CParamNode::ToBool() const { if (m_Value == "true") return true; else return false; } const CParamNode::ChildrenMap& CParamNode::GetChildren() const { return m_Childs; } std::string CParamNode::EscapeXMLString(const std::string& str) { std::string ret; ret.reserve(str.size()); // TODO: would be nice to check actual v1.0 XML codepoints, // but our UTF8 validation routines are lacking. for (size_t i = 0; i < str.size(); ++i) { char c = str[i]; switch (c) { case '<': ret += "<"; break; case '>': ret += ">"; break; case '&': ret += "&"; break; case '"': ret += """; break; case '\t': ret += " "; break; case '\n': ret += " "; break; case '\r': ret += " "; break; default: ret += c; } } return ret; } std::string CParamNode::ToXMLString() const { std::stringstream strm; ToXMLString(strm); return strm.str(); } void CParamNode::ToXMLString(std::ostream& strm) const { strm << m_Value; ChildrenMap::const_iterator it = m_Childs.begin(); for (; it != m_Childs.end(); ++it) { // Skip attributes here (they were handled when the caller output the tag) if (it->first.length() && it->first[0] == '@') continue; strm << "<" << it->first; // Output the child's attributes first ChildrenMap::const_iterator cit = it->second.m_Childs.begin(); for (; cit != it->second.m_Childs.end(); ++cit) { if (cit->first.length() && cit->first[0] == '@') { std::string attrname (cit->first.begin()+1, cit->first.end()); strm << " " << attrname << "=\"" << EscapeXMLString(cit->second.m_Value) << "\""; } } strm << ">"; it->second.ToXMLString(strm); strm << "first << ">"; } } void CParamNode::ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const { if (cacheValue && m_ScriptVal != NULL) { ret.set(*m_ScriptVal); return; } ConstructJSVal(rq, ret); if (cacheValue) m_ScriptVal.reset(new JS::PersistentRootedValue(rq.cx, ret)); } void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const { if (m_Childs.empty()) { // Empty node - map to undefined if (m_Value.empty()) { ret.setUndefined(); return; } // Just a string JS::RootedString str(rq.cx, JS_NewStringCopyUTF8Z(rq.cx, JS::ConstUTF8CharsZ(m_Value.data(), m_Value.size()))); str.set(JS_AtomizeAndPinJSString(rq.cx, str)); if (str) { ret.setString(str); return; } // TODO: report error ret.setUndefined(); return; } // Got child nodes - convert this node into a hash-table-style object: JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); if (!obj) { ret.setUndefined(); return; // TODO: report error } JS::RootedValue childVal(rq.cx); for (std::map::const_iterator it = m_Childs.begin(); it != m_Childs.end(); ++it) { it->second.ConstructJSVal(rq, &childVal); if (!JS_SetProperty(rq.cx, obj, it->first.c_str(), childVal)) { ret.setUndefined(); return; // TODO: report error } } // If the node has a string too, add that as an extra property if (!m_Value.empty()) { std::u16string text(m_Value.begin(), m_Value.end()); JS::RootedString str(rq.cx, JS_AtomizeAndPinUCStringN(rq.cx, text.c_str(), text.length())); if (!str) { ret.setUndefined(); return; // TODO: report error } JS::RootedValue subChildVal(rq.cx, JS::StringValue(str)); if (!JS_SetProperty(rq.cx, obj, "_string", subChildVal)) { ret.setUndefined(); return; // TODO: report error } } ret.setObject(*obj); } void CParamNode::ResetScriptVal() { m_ScriptVal = NULL; } Index: ps/trunk/source/simulation2/system/ParamNode.h =================================================================== --- ps/trunk/source/simulation2/system/ParamNode.h (revision 27322) +++ ps/trunk/source/simulation2/system/ParamNode.h (revision 27323) @@ -1,295 +1,301 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_PARAMNODE #define INCLUDED_PARAMNODE #include "lib/file/vfs/vfs_path.h" #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptTypes.h" #include #include class XMBData; class XMBElement; class CStrIntern; class ScriptRequest; /** * An entity initialisation parameter node. * Each node has a text value, plus a number of named child nodes (in a tree structure). * Child nodes are unordered, and there cannot be more than one with the same name. * Nodes are immutable. * * Nodes can be initialised from XML files. Child elements are mapped onto child nodes. * Attributes are mapped onto child nodes with names prefixed by "@" * (e.g. the XML <a b="c"><d/></a> is loaded as a node with two * child nodes, one called "@b" and one called "d"). * * They can also be initialised from @em multiple XML files, * which is used by ICmpTemplateManager for entity template inheritance. * Loading one XML file like: * @code * * * text * * * * * * * * * one two three * * * * * test * * * example * * * * @endcode * then a second like: * @code * * * example * new * * * * new * * * four * -two * * * * example * * * * text * * * * @endcode * is equivalent to loading a single file like: * @code * * * example * new * * * new * * * one three four * * * * test * example * * * text * * * * @endcode * * Parameter nodes can be translated to JavaScript objects. The previous example will become the object: * @code * { "Entity": { * "Example1": { * "A": { "@attr": "value", "_string": "example" }, * "D": "new" * }, * "Example3": { * "D": "new" * }, * "Example4": { "@datatype": "tokens", "_string": "one three four" }, * "Example5": { * "F": { * "I": "test", * "K": "example" * }, * "H": { * "J": "text" * } * } * } * } * @endcode * (Note the special @c _string for the hopefully-rare cases where a node contains both child nodes and text.) */ class CParamNode { public: typedef std::map ChildrenMap; /** * Constructs a new, empty node. */ CParamNode(bool isOk = true); /** * Loads the XML data specified by @a file into the node @a ret. * Any existing data in @a ret will be overwritten or else kept, so this * can be called multiple times to build up a node from multiple inputs. * * @param sourceIdentifier Optional; string you can pass along to indicate the source of * the data getting loaded. Used for output to log messages if an error occurs. */ static void LoadXML(CParamNode& ret, const XMBData& xmb, const wchar_t* sourceIdentifier = NULL); /** * Loads the XML data specified by @a path into the node @a ret. * Any existing data in @a ret will be overwritten or else kept, so this * can be called multiple times to build up a node from multiple inputs. */ static void LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName); /** * See LoadXML, but parses the XML string @a xml. * @return error code if parsing failed, else @c PSRETURN_OK * * @param sourceIdentifier Optional; string you can pass along to indicate the source of * the data getting loaded. Used for output to log messages if an error occurs. */ static PSRETURN LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier = NULL); /** * Returns the (unique) child node with the given name, or a node with IsOk() == false if there is none. */ const CParamNode& GetChild(const char* name) const; // (Children are returned as const in order to allow future optimisations, where we assume // a node is always modified explicitly and not indirectly via its children, e.g. to cache JS::Values) /** + * Returns the only child node, or a node with IsOk() == false if there is none. + * This is mainly useful for the root node. + */ + const CParamNode& GetOnlyChild() const; + + /** * Returns true if this is a valid CParamNode, false if it represents a non-existent node */ bool IsOk() const; /** * Returns the content of this node as a wstring */ const std::wstring ToWString() const; /** * Returns the content of this node as an UTF8 string */ const std::string& ToString() const; /** * Returns the content of this node as an internalized 8-bit string. Should only be used for * predictably small and frequently-used strings. */ const CStrIntern ToUTF8Intern() const; /** * Parses the content of this node as an integer */ int ToInt() const; /** * Parses the content of this node as a fixed-point number */ fixed ToFixed() const; /** * Parses the content of this node as a floating-point number */ float ToFloat() const; /** * Parses the content of this node as a boolean ("true" == true, anything else == false) */ bool ToBool() const; /** * Returns the content of this node and its children as an XML string */ std::string ToXMLString() const; /** * Write the content of this node and its children as an XML string, to the stream */ void ToXMLString(std::ostream& strm) const; /** * Returns a JS::Value representation of this node and its children. * If @p cacheValue is true, then the same JS::Value will be returned each time * this is called (regardless of whether you passed the same @p cx - be careful * to only use the cache in one context). * When caching, the lifetime of @p cx must be longer than the lifetime of this node. * The cache will be reset if *this* node is modified (e.g. by LoadXML), * but *not* if any child nodes are modified (so don't do that). */ void ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const; /** * Returns the names/nodes of the children of this node, ordered by name */ const ChildrenMap& GetChildren() const; /** * Escapes a string so that it is well-formed XML content/attribute text. * (Replaces "&" with "&" etc) */ static std::string EscapeXMLString(const std::string& str); std::string m_Name; u32 m_Index; private: /** * Overlays the specified data onto this node. See class documentation for the concept and examples. * * @param xmb Representation of the XMB file containing an element with the data to apply. * @param element Element inside the specified @p xmb file containing the data to apply. * @param sourceIdentifier Optional; string you can pass along to indicate the source of * the data getting applied. Used for output to log messages if an error occurs. */ void ApplyLayer(const XMBData& xmb, const XMBElement& element, const wchar_t* sourceIdentifier = NULL); void ResetScriptVal(); void ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const; std::string m_Value; ChildrenMap m_Childs; bool m_IsOk; /** * Caches the ToJSVal script representation of this node. */ mutable std::shared_ptr m_ScriptVal; }; #endif // INCLUDED_PARAMNODE Index: ps/trunk/source/simulation2/tests/test_ParamNode.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ParamNode.h (revision 27322) +++ ps/trunk/source/simulation2/tests/test_ParamNode.h (revision 27323) @@ -1,204 +1,205 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/system/ParamNode.h" #include "ps/CLogger.h" #include "ps/XML/Xeromyces.h" class TestParamNode : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } void test_basic() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 1 234"), PSRETURN_OK); TS_ASSERT(node.GetChild("test").IsOk()); TS_ASSERT(!node.GetChild("Test").IsOk()); TS_ASSERT_STR_EQUALS(node.GetChild("test").ToString(), ""); TS_ASSERT(node.GetChild("test").GetChild("Foo").IsOk()); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("Foo").ToInt(), 1); TS_ASSERT_STR_EQUALS(node.GetChild("test").GetChild("Foo").ToString(), "1"); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("Bar").ToInt(), 24); TS_ASSERT_STR_EQUALS(node.GetChild("test").GetChild("Bar").ToString(), "24"); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("Bar").GetChild("Baz").ToInt(), 3); TS_ASSERT(node.GetChild("test").GetChild("Qux").IsOk()); TS_ASSERT(!node.GetChild("test").GetChild("Qux").GetChild("Baz").IsOk()); + TS_ASSERT_STR_EQUALS(node.GetChild("test").ToXMLString(), node.GetOnlyChild().ToXMLString()); CParamNode nullOne(false); CParamNode nullTwo = nullOne; CParamNode nullThree(nullOne); TS_ASSERT(!nullOne.IsOk()); TS_ASSERT(!nullTwo.IsOk()); TS_ASSERT(!nullThree.IsOk()); TS_ASSERT_STR_EQUALS(nullOne.ToString(), ""); TS_ASSERT(nullOne.ToInt() == 0); TS_ASSERT(nullOne.ToFixed().ToDouble() == 0); } void test_attrs() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 3 "), PSRETURN_OK); TS_ASSERT(node.GetChild("test").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("@x").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("@y").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("z").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("w").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("w").GetChild("@a").IsOk()); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("@x").ToInt(), 1); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("@y").ToInt(), 2); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("z").ToInt(), 3); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("w").GetChild("@a").ToInt(), 4); } void test_ToXMLString() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 3 "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "3"); } void test_overlay_basic() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 3 4 "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 7 8 "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "378"); } void test_overlay_disable() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 1 2 "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "2"); } void test_overlay_replace() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 2 "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), ""); } void test_overlay_tokens() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " x ya b\nc\tdm n"), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " -y z wn o"), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "x z wa b c dn o"); } void test_overlay_remove_nonexistent_token() { // regression test; this used to cause a crash because of a failure to check whether the token being removed was present TestLogger logger; CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " -nonexistenttoken X"), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "X"); } void test_overlay_remove_empty_token() { TestLogger logger; CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " Y - X "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "Y X"); } void test_overlay_filtered() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " toberemoved "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), ""); CParamNode node2; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node2, " bcde "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node2, " c2 "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node2.ToXMLString(), "bc2"); } void test_overlay_merge() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " foobar foo "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " testbaz willnotbeincluded textmore text "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "testbarbazmore texttextfoo"); } void test_overlay_merge_empty() { // 'merge' nodes don't change the original value. CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "foobar"), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "skippedreplaced"), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "foobar"); } void test_overlay_filtered_merge() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 1200 "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " bar 1 "), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "11200bar"); } void test_overlay_ops() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "555"), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "530.55"), PSRETURN_OK); TS_ASSERT_STR_EQUALS(node.ToXMLString(), "10153"); } void test_types() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "+010.75true"), PSRETURN_OK); TS_ASSERT(node.GetChild("test").IsOk()); TS_ASSERT(node.GetChild("test").GetChild("n").IsOk()); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("n").ToString(), "+010.75"); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("n").ToInt(), 10); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("n").ToFixed().ToDouble(), 10.75); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("n").ToBool(), false); TS_ASSERT_EQUALS(node.GetChild("test").GetChild("t").ToBool(), true); } void test_escape() { TS_ASSERT_STR_EQUALS(CParamNode::EscapeXMLString("test"), "test"); TS_ASSERT_STR_EQUALS(CParamNode::EscapeXMLString("x < y << z"), "x < y << z"); TS_ASSERT_STR_EQUALS(CParamNode::EscapeXMLString("x < y \"&' y > z ]]> "), "x < y "&' y > z ]]> "); TS_ASSERT_STR_EQUALS(CParamNode::EscapeXMLString(" \r\n\t "), " "); } };