Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 25441) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 25442) @@ -1,429 +1,429 @@ /* Copyright (C) 2021 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/Threading.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() { // Wait for thread to end if (m_WorkerThread.joinable()) m_WorkerThread.join(); } 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; // Launch the worker thread m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); } void CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); // Enable the script to be aborted JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } SAFE_DELETE(self->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, ScriptFunction::ObjectFromCBData>(rq, #func); + ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData>(rq, #func); #define REGISTER_MAPGEN_FUNC_NAME(func, name) \ - ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptFunction::ObjectFromCBData>(rq, 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_Maps(*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"); 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 25441) +++ ps/trunk/source/gui/GUIManager.cpp (revision 25442) @@ -1,410 +1,410 @@ /* Copyright (C) 2021 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/XML/Xeromyces.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" CGUIManager* g_GUI = nullptr; const CStr CGUIManager::EventNameWindowResized = "WindowResized"; // 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, ScriptInterface* srcScriptInterface, JS::HandleValue initData) +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); } 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); // 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; } m_PageStack.pop_back(); m_PageStack.back().PerformCallbackFunction(args); } CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData) : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction() { } void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptContext) { // If we're hotloading then try to grab some data from the previous page Script::StructuredClone hotloadData; if (gui) { 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_CursorName = g_DefaultCursor; 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(); 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; 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() const { PROFILE3_GPU("gui"); for (const SGUIPage& p : m_PageStack) p.gui->Draw(); } 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(EventNameWindowResized); } } 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"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } // 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) shared_ptr CGUIManager::top() const { ENSURE(m_PageStack.size()); return m_PageStack.back().gui; } Index: ps/trunk/source/gui/GUIManager.h =================================================================== --- ps/trunk/source/gui/GUIManager.h (revision 25441) +++ ps/trunk/source/gui/GUIManager.h (revision 25442) @@ -1,180 +1,180 @@ /* Copyright (C) 2021 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_GUIMANAGER #define INCLUDED_GUIMANAGER #include "lib/file/vfs/vfs_path.h" #include "lib/input.h" #include "ps/CStr.h" #include "ps/TemplateLoader.h" #include "scriptinterface/StructuredClone.h" #include #include class CGUI; /** * External interface to the GUI system. * * The GUI consists of a set of pages. Each page is constructed from a * series of XML files, and is independent from any other page. * Only one page is active at a time. All events and render requests etc * will go to the active page. This lets the GUI switch between pre-game menu * and in-game UI. */ class CGUIManager { NONCOPYABLE(CGUIManager); public: CGUIManager(); ~CGUIManager(); shared_ptr GetScriptInterface() { return m_ScriptInterface; } shared_ptr GetContext() { return m_ScriptContext; } shared_ptr GetActiveGUI() { return top(); } /** * Returns the number of currently open GUI pages. */ size_t GetPageCount() const; /** * Load a new GUI page and make it active. All current pages will be destroyed. */ - void SwitchPage(const CStrW& name, ScriptInterface* srcScriptInterface, JS::HandleValue initData); + void SwitchPage(const CStrW& name, const ScriptInterface* srcScriptInterface, JS::HandleValue initData); /** * Load a new GUI page and make it active. All current pages will be retained, * and will still be drawn and receive tick events, but will not receive * user inputs. * If given, the callbackHandler function will be executed once this page is closed. */ void PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunc); /** * Unload the currently active GUI page, and make the previous page active. * (There must be at least two pages when you call this.) */ void PopPage(Script::StructuredClone args); /** * Called when a file has been modified, to hotload changes. */ Status ReloadChangedFile(const VfsPath& path); /** * Called when we should reload all pages (e.g. translation hotloading update). */ Status ReloadAllPages(); /** * Pass input events to the currently active GUI page. */ InReaction HandleEvent(const SDL_Event_* ev); /** * See CGUI::SendEventToAll; applies to the currently active page. */ void SendEventToAll(const CStr& eventName) const; void SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const; /** * See CGUI::TickObjects; applies to @em all loaded pages. */ void TickObjects(); /** * See CGUI::Draw; applies to @em all loaded pages. */ void Draw() const; /** * See CGUI::UpdateResolution; applies to @em all loaded pages. */ void UpdateResolution(); /** * Check if a template with this name exists */ bool TemplateExists(const std::string& templateName) const; /** * Retrieve the requested template, used for displaying faction specificities. */ const CParamNode& GetTemplate(const std::string& templateName); private: struct SGUIPage { // COPYABLE, because event handlers may invalidate page stack iterators by open or close pages, // and event handlers need to be called for the entire stack. /** * Initializes the data that will be used to create the CGUI page one or multiple times (hotloading). */ SGUIPage(const CStrW& pageName, const Script::StructuredClone initData); /** * Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed. */ void LoadPage(shared_ptr scriptContext); /** * Sets the callback handler when a new page is opened that will be performed when the page is closed. */ void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc); /** * Execute the stored callback function with the given arguments. */ void PerformCallbackFunction(Script::StructuredClone args); CStrW m_Name; std::unordered_set inputs; // for hotloading Script::StructuredClone initData; // data to be passed to the init() function shared_ptr gui; // the actual GUI page /** * Function executed by this parent GUI page when the child GUI page it pushed is popped. * Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset. */ shared_ptr callbackFunction; }; const static CStr EventNameWindowResized; shared_ptr top() const; shared_ptr m_ScriptContext; shared_ptr m_ScriptInterface; using PageStackType = std::vector; PageStackType m_PageStack; CTemplateLoader m_TemplateLoader; }; extern CGUIManager* g_GUI; extern InReaction gui_handler(const SDL_Event_* ev); #endif // INCLUDED_GUIMANAGER Index: ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp (revision 25441) +++ ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp (revision 25442) @@ -1,91 +1,92 @@ /* Copyright (C) 2021 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 "JSInterface_GUIManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/ObjectBases/IGUIObject.h" #include "ps/GameSetup/Config.h" #include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" namespace JSI_GUIManager { // Note that the initData argument may only contain clonable data. // Functions aren't supported for example! void PushGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction) { g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData), callbackFunction); } -void SwitchGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData) +void SwitchGuiPage(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue initData) { - g_GUI->SwitchPage(name, pCmptPrivate->pScriptInterface, initData); + g_GUI->SwitchPage(name, &scriptInterface, initData); } void PopGuiPage(const ScriptRequest& rq, JS::HandleValue args) { if (g_GUI->GetPageCount() < 2) { ScriptException::Raise(rq, "Can't pop GUI pages when less than two pages are opened!"); return; } g_GUI->PopPage(Script::WriteStructuredClone(rq, args)); } std::wstring SetCursor(const std::wstring& name) { std::wstring old = g_CursorName; g_CursorName = name; return old; } void ResetCursor() { g_CursorName = g_DefaultCursor; } bool TemplateExists(const std::string& templateName) { return g_GUI->TemplateExists(templateName); } CParamNode GetTemplate(const std::string& templateName) { return g_GUI->GetTemplate(templateName); } void RegisterScriptFunctions(const ScriptRequest& rq) { ScriptFunction::Register<&PushGuiPage>(rq, "PushGuiPage"); ScriptFunction::Register<&SwitchGuiPage>(rq, "SwitchGuiPage"); ScriptFunction::Register<&PopGuiPage>(rq, "PopGuiPage"); ScriptFunction::Register<&SetCursor>(rq, "SetCursor"); ScriptFunction::Register<&ResetCursor>(rq, "ResetCursor"); ScriptFunction::Register<&TemplateExists>(rq, "TemplateExists"); ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); - ScriptFunction::Register<&CGUI::FindObjectByName, &ScriptFunction::ObjectFromCBData>(rq, "GetGUIObjectByName"); - ScriptFunction::Register<&CGUI::SetGlobalHotkey, &ScriptFunction::ObjectFromCBData>(rq, "SetGlobalHotkey"); - ScriptFunction::Register<&CGUI::UnsetGlobalHotkey, &ScriptFunction::ObjectFromCBData>(rq, "UnsetGlobalHotkey"); + ScriptFunction::Register<&CGUI::FindObjectByName, &ScriptInterface::ObjectFromCBData>(rq, "GetGUIObjectByName"); + ScriptFunction::Register<&CGUI::SetGlobalHotkey, &ScriptInterface::ObjectFromCBData>(rq, "SetGlobalHotkey"); + ScriptFunction::Register<&CGUI::UnsetGlobalHotkey, &ScriptInterface::ObjectFromCBData>(rq, "UnsetGlobalHotkey"); } } Index: ps/trunk/source/gui/Scripting/JSInterface_GUIProxy_impl.h =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUIProxy_impl.h (revision 25441) +++ ps/trunk/source/gui/Scripting/JSInterface_GUIProxy_impl.h (revision 25442) @@ -1,320 +1,319 @@ /* Copyright (C) 2021 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 . */ // This file is included directly into actual implementation files. #include "JSInterface_GUIProxy.h" #include "gui/CGUI.h" #include "gui/CGUISetting.h" #include "gui/ObjectBases/IGUIObject.h" #include "ps/CLogger.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptExtraHeaders.h" -#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRequest.h" #include template JSI_GUIProxy& JSI_GUIProxy::Singleton() { static JSI_GUIProxy s; return s; } // Call this for every specialised type. You will need to override IGUIObject::CreateJSObject() in your class interface. #define DECLARE_GUIPROXY(Type) \ void Type::CreateJSObject() \ { \ ScriptRequest rq(m_pGUI.GetScriptInterface()); \ using ProxyHandler = JSI_GUIProxy>; \ m_JSObject = ProxyHandler::CreateJSObject(rq, this, GetGUI().GetProxyData(&ProxyHandler::Singleton())); \ } \ template class JSI_GUIProxy; // Use a common namespace to avoid duplicating the symbols un-necessarily. namespace JSInterface_GUIProxy { // All proxy objects share a class definition. JSClass& ClassDefinition() { static JSClass c = PROXY_CLASS_DEF("GUIObjectProxy", JSCLASS_HAS_CACHED_PROTO(JSProto_Proxy) | JSCLASS_HAS_RESERVED_SLOTS(1)); return c; } // Default implementation of the cache via unordered_map class MapCache : public GUIProxyProps { public: virtual ~MapCache() {}; virtual bool has(const std::string& name) const override { return m_Functions.find(name) != m_Functions.end(); } virtual JSObject* get(const std::string& name) const override { return m_Functions.at(name).get(); } virtual bool setFunction(const ScriptRequest& rq, const std::string& name, JSFunction* function) override { m_Functions[name].init(rq.cx, JS_GetFunctionObject(function)); return true; } protected: std::unordered_map m_Functions; }; } template<> IGUIObject* IGUIProxyObject::FromPrivateSlot(JSObject* obj) { if (!obj) return nullptr; if (JS_GetClass(obj) != &JSInterface_GUIProxy::ClassDefinition()) return nullptr; return UnsafeFromPrivateSlot(obj); } // The default propcache is a MapCache. template struct JSI_GUIProxy::PropCache { using type = JSInterface_GUIProxy::MapCache; }; template T* JSI_GUIProxy::FromPrivateSlot(const ScriptRequest&, JS::CallArgs& args) { // Call the unsafe version - this is only ever called from actual proxy objects. return IGUIProxyObject::UnsafeFromPrivateSlot(args.thisv().toObjectOrNull()); } template bool JSI_GUIProxy::PropGetter(JS::HandleObject proxy, const std::string& propName, JS::MutableHandleValue vp) const { using PropertyCache = typename PropCache::type; // Since we know at compile time what the type actually is, avoid the virtual call. const PropertyCache* data = static_cast(static_cast(js::GetProxyReservedSlot(proxy, 0).toPrivate())); if (data->has(propName)) { vp.setObjectOrNull(data->get(propName)); return true; } return false; } template std::pair JSI_GUIProxy::CreateData(ScriptInterface& scriptInterface) { using PropertyCache = typename PropCache::type; PropertyCache* data = new PropertyCache(); ScriptRequest rq(scriptInterface); // Functions common to all children of IGUIObject. JSI_GUIProxy::CreateFunctions(rq, data); // Let derived classes register their own interface. if constexpr (!std::is_same_v) CreateFunctions(rq, data); return { &Singleton(), data }; } template template void JSI_GUIProxy::CreateFunction(const ScriptRequest& rq, GUIProxyProps* cache, const std::string& name) { cache->setFunction(rq, name, ScriptFunction::Create(rq, name.c_str())); } template std::unique_ptr JSI_GUIProxy::CreateJSObject(const ScriptRequest& rq, T* ptr, GUIProxyProps* dataPtr) { js::ProxyOptions options; options.setClass(&JSInterface_GUIProxy::ClassDefinition()); auto ret = std::make_unique(); ret->m_Ptr = static_cast(ptr); JS::RootedValue cppObj(rq.cx), data(rq.cx); cppObj.get().setPrivate(ret->m_Ptr); data.get().setPrivate(static_cast(dataPtr)); ret->m_Object.init(rq.cx, js::NewProxyObject(rq.cx, &Singleton(), cppObj, nullptr, options)); js::SetProxyReservedSlot(ret->m_Object, 0, data); return ret; } template bool JSI_GUIProxy::get(JSContext* cx, JS::HandleObject proxy, JS::HandleValue UNUSED(receiver), JS::HandleId id, JS::MutableHandleValue vp) const { - ScriptInterface* pScriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; - ScriptRequest rq(*pScriptInterface); + ScriptRequest rq(cx); T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) return false; JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return false; std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return false; // Return function properties. Specializable. if (PropGetter(proxy, propName, vp)) return true; // Use onWhatever to access event handlers if (propName.substr(0, 2) == "on") { CStr eventName(propName.substr(2)); std::map>::iterator it = e->m_ScriptHandlers.find(eventName); if (it == e->m_ScriptHandlers.end()) vp.setNull(); else vp.setObject(*it->second.get()); return true; } if (propName == "parent") { IGUIObject* parent = e->GetParent(); if (parent) vp.set(JS::ObjectValue(*parent->GetJSObject())); else vp.set(JS::NullValue()); return true; } else if (propName == "children") { Script::CreateArray(rq, vp); for (size_t i = 0; i < e->m_Children.size(); ++i) Script::SetPropertyInt(rq, vp, i, e->m_Children[i]); return true; } else if (propName == "name") { Script::ToJSVal(rq, vp, e->GetName()); return true; } else if (e->SettingExists(propName)) { e->m_Settings[propName]->ToJSVal(rq, vp); return true; } LOGERROR("Property '%s' does not exist!", propName.c_str()); return false; } template bool JSI_GUIProxy::set(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::HandleValue vp, JS::HandleValue UNUSED(receiver), JS::ObjectOpResult& result) const { T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) { LOGERROR("C++ GUI Object could not be found"); return result.fail(JSMSG_OBJECT_REQUIRED); } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); + ScriptRequest rq(cx); JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return result.fail(JSMSG_BAD_PROP_ID); std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return result.fail(JSMSG_BAD_PROP_ID); if (propName == "name") { std::string value; if (!Script::FromJSVal(rq, vp, value)) return result.fail(JSMSG_BAD_PROP_ID); e->SetName(value); return result.succeed(); } JS::RootedObject vpObj(cx); if (vp.isObject()) vpObj = &vp.toObject(); // Use onWhatever to set event handlers if (propName.substr(0, 2) == "on") { if (vp.isPrimitive() || vp.isNull() || !JS_ObjectIsFunction(&vp.toObject())) { LOGERROR("on- event-handlers must be functions"); return result.fail(JSMSG_NOT_FUNCTION); } CStr eventName(propName.substr(2)); e->SetScriptHandler(eventName, vpObj); return result.succeed(); } if (e->SettingExists(propName)) return e->m_Settings[propName]->FromJSVal(rq, vp, true) ? result.succeed() : result.fail(JSMSG_USER_DEFINED_ERROR); LOGERROR("Property '%s' does not exist!", propName.c_str()); return result.fail(JSMSG_BAD_PROP_ID); } template bool JSI_GUIProxy::delete_(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult& result) const { T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) { LOGERROR("C++ GUI Object could not be found"); return result.fail(JSMSG_OBJECT_REQUIRED); } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); + ScriptRequest rq(cx); JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return result.fail(JSMSG_BAD_PROP_ID); std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return result.fail(JSMSG_BAD_PROP_ID); // event handlers if (propName.substr(0, 2) == "on") { CStr eventName(propName.substr(2)); e->UnsetScriptHandler(eventName); return result.succeed(); } LOGERROR("Only event handlers can be deleted from GUI objects!"); return result.fail(JSMSG_BAD_PROP_ID); } Index: ps/trunk/source/gui/Scripting/JSInterface_GUISize.cpp =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUISize.cpp (revision 25441) +++ ps/trunk/source/gui/Scripting/JSInterface_GUISize.cpp (revision 25442) @@ -1,130 +1,129 @@ /* Copyright (C) 2021 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 "JSInterface_GUISize.h" #include "ps/CStr.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/Object.h" JSClass JSI_GUISize::JSI_class = { "GUISize", 0, &JSI_GUISize::JSI_classops }; JSClassOps JSI_GUISize::JSI_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JSI_GUISize::construct, nullptr }; JSFunctionSpec JSI_GUISize::JSI_methods[] = { JS_FN("toString", JSI_GUISize::toString, 0, 0), JS_FS_END }; void JSI_GUISize::RegisterScriptClass(ScriptInterface& scriptInterface) { scriptInterface.DefineCustomObjectType(&JSI_GUISize::JSI_class, JSI_GUISize::construct, 0, nullptr, JSI_GUISize::JSI_methods, nullptr, nullptr); } bool JSI_GUISize::construct(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptInterface* pScriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; - ScriptRequest rq(*pScriptInterface); + ScriptRequest rq(cx); + const ScriptInterface& scriptInterface = rq.GetScriptInterface(); - JS::RootedObject obj(rq.cx, pScriptInterface->CreateCustomObject("GUISize")); + JS::RootedObject obj(rq.cx, scriptInterface.CreateCustomObject("GUISize")); if (args.length() == 8) { JS_SetProperty(rq.cx, obj, "left", args[0]); JS_SetProperty(rq.cx, obj, "top", args[1]); JS_SetProperty(rq.cx, obj, "right", args[2]); JS_SetProperty(rq.cx, obj, "bottom", args[3]); JS_SetProperty(rq.cx, obj, "rleft", args[4]); JS_SetProperty(rq.cx, obj, "rtop", args[5]); JS_SetProperty(rq.cx, obj, "rright", args[6]); JS_SetProperty(rq.cx, obj, "rbottom", args[7]); } else if (args.length() == 4) { JS::RootedValue zero(rq.cx, JS::NumberValue(0)); JS_SetProperty(rq.cx, obj, "left", args[0]); JS_SetProperty(rq.cx, obj, "top", args[1]); JS_SetProperty(rq.cx, obj, "right", args[2]); JS_SetProperty(rq.cx, obj, "bottom", args[3]); JS_SetProperty(rq.cx, obj, "rleft", zero); JS_SetProperty(rq.cx, obj, "rtop", zero); JS_SetProperty(rq.cx, obj, "rright", zero); JS_SetProperty(rq.cx, obj, "rbottom", zero); } else { JS::RootedValue zero(rq.cx, JS::NumberValue(0)); JS_SetProperty(rq.cx, obj, "left", zero); JS_SetProperty(rq.cx, obj, "top", zero); JS_SetProperty(rq.cx, obj, "right", zero); JS_SetProperty(rq.cx, obj, "bottom", zero); JS_SetProperty(rq.cx, obj, "rleft", zero); JS_SetProperty(rq.cx, obj, "rtop", zero); JS_SetProperty(rq.cx, obj, "rright", zero); JS_SetProperty(rq.cx, obj, "rbottom", zero); } args.rval().setObject(*obj); return true; } // Produces "10", "-10", "50%", "50%-10", "50%+10", etc CStr JSI_GUISize::ToPercentString(double pix, double per) { if (per == 0) return CStr::FromDouble(pix); return CStr::FromDouble(per)+"%"+(pix == 0.0 ? CStr() : pix > 0.0 ? CStr("+")+CStr::FromDouble(pix) : CStr::FromDouble(pix)); } bool JSI_GUISize::toString(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); CStr buffer; - ScriptInterface* pScriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; - ScriptRequest rq(*pScriptInterface); + ScriptRequest rq(cx); double val, valr; #define SIDE(side) \ Script::GetProperty(rq, args.thisv(), #side, val); \ Script::GetProperty(rq, args.thisv(), "r"#side, valr); \ buffer += ToPercentString(val, valr); SIDE(left); buffer += " "; SIDE(top); buffer += " "; SIDE(right); buffer += " "; SIDE(bottom); #undef SIDE Script::ToJSVal(rq, args.rval(), buffer); return true; } Index: ps/trunk/source/gui/SettingTypes/CGUISize.cpp =================================================================== --- ps/trunk/source/gui/SettingTypes/CGUISize.cpp (revision 25441) +++ ps/trunk/source/gui/SettingTypes/CGUISize.cpp (revision 25442) @@ -1,230 +1,230 @@ /* Copyright (C) 2021 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 "CGUISize.h" #include "gui/Scripting/JSInterface_GUISize.h" #include "ps/CLogger.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptInterface.h" CGUISize::CGUISize() : pixel(), percent() { } CGUISize::CGUISize(const CRect& pixel, const CRect& percent) : pixel(pixel), percent(percent) { } CGUISize CGUISize::Full() { return CGUISize(CRect(0, 0, 0, 0), CRect(0, 0, 100, 100)); } CRect CGUISize::GetSize(const CRect& parent) const { // If it's a 0 0 100% 100% we need no calculations if (percent == CRect(0.f, 0.f, 100.f, 100.f) && pixel == CRect()) return parent; CRect client; // This should probably be cached and not calculated all the time for every object. client.left = parent.left + (parent.right-parent.left)*percent.left/100.f + pixel.left; client.top = parent.top + (parent.bottom-parent.top)*percent.top/100.f + pixel.top; client.right = parent.left + (parent.right-parent.left)*percent.right/100.f + pixel.right; client.bottom = parent.top + (parent.bottom-parent.top)*percent.bottom/100.f + pixel.bottom; return client; } bool CGUISize::FromString(const CStr& Value) { /* * GUISizes contain a left, top, right, and bottom * for example: "50%-150 10%+9 50%+150 10%+25" means * the left edge is at 50% minus 150 pixels, the top * edge is at 10% plus 9 pixels, the right edge is at * 50% plus 150 pixels, and the bottom edge is at 10% * plus 25 pixels. * All four coordinates are required and can be * defined only in pixels, only in percents, or some * combination of both. */ // Check the input is only numeric const char* input = Value.c_str(); CStr buffer = ""; unsigned int coord = 0; float pixels[4] = {0, 0, 0, 0}; float percents[4] = {0, 0, 0, 0}; for (unsigned int i = 0; i < Value.length(); ++i) { switch (input[i]) { case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': buffer.push_back(input[i]); break; case '+': pixels[coord] += buffer.ToFloat(); buffer = "+"; break; case '-': pixels[coord] += buffer.ToFloat(); buffer = "-"; break; case '%': percents[coord] += buffer.ToFloat(); buffer = ""; break; case ' ': pixels[coord] += buffer.ToFloat(); buffer = ""; ++coord; break; default: LOGERROR("CGUISize definition may only include numbers. Your input: '%s'", Value.c_str()); return false; } if (coord > 3) { LOGERROR("Too many CGUISize parameters (4 max). Your input: '%s'", Value.c_str()); return false; } } if (coord < 3) { LOGERROR("Too few CGUISize parameters (4 min). Your input: '%s'", Value.c_str()); return false; } // Now that we're at the end of the string, flush the remaining buffer. pixels[coord] += buffer.ToFloat(); // Now store the coords in the right place pixel.left = pixels[0]; pixel.top = pixels[1]; pixel.right = pixels[2]; pixel.bottom = pixels[3]; percent.left = percents[0]; percent.top = percents[1]; percent.right = percents[2]; percent.bottom = percents[3]; return true; } void CGUISize::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const { - ScriptInterface* pScriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(rq.cx)->pScriptInterface; - ret.setObjectOrNull(pScriptInterface->CreateCustomObject("GUISize")); + const ScriptInterface& scriptInterface = rq.GetScriptInterface(); + ret.setObjectOrNull(scriptInterface.CreateCustomObject("GUISize")); if (!ret.isObject()) { ScriptException::Raise(rq, "CGUISize value is not an Object"); return; } JS::RootedObject obj(rq.cx, &ret.toObject()); if (!JS_InstanceOf(rq.cx, obj, &JSI_GUISize::JSI_class, nullptr)) { ScriptException::Raise(rq, "CGUISize value is not a CGUISize class instance"); return; } #define P(x, y, z)\ if (!Script::SetProperty(rq, ret, #z, x.y)) \ { \ ScriptException::Raise(rq, "Could not SetProperty '%s'", #z); \ return; \ } P(pixel, left, left); P(pixel, top, top); P(pixel, right, right); P(pixel, bottom, bottom); P(percent, left, rleft); P(percent, top, rtop); P(percent, right, rright); P(percent, bottom, rbottom); #undef P } bool CGUISize::FromJSVal(const ScriptRequest& rq, JS::HandleValue v) { if (v.isString()) { CStrW str; if (!Script::FromJSVal(rq, v, str)) { LOGERROR("CGUISize could not read JS string"); return false; } if (!FromString(str.ToUTF8())) { LOGERROR("CGUISize could not parse JS string"); return false; } return true; } if (!v.isObject()) { LOGERROR("CGUISize value is not an String, nor Object"); return false; } JS::RootedObject obj(rq.cx, &v.toObject()); if (!JS_InstanceOf(rq.cx, obj, &JSI_GUISize::JSI_class, nullptr)) { LOGERROR("CGUISize value is not a CGUISize class instance"); return false; } #define P(x, y, z) \ if (!Script::GetProperty(rq, v, #z, x.y))\ {\ LOGERROR("CGUISize could not get object property '%s'", #z);\ return false;\ } P(pixel, left, left); P(pixel, top, top); P(pixel, right, right); P(pixel, bottom, bottom); P(percent, left, rleft); P(percent, top, rtop); P(percent, right, rright); P(percent, bottom, rbottom); #undef P return true; } Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 25441) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 25442) @@ -1,232 +1,232 @@ /* Copyright (C) 2021 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 "JSInterface_Lobby.h" #include "gui/GUIManager.h" #include "lib/utf8.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "third_party/encryption/pkcs5_pbkdf2.h" #include namespace JSI_Lobby { bool HasXmppClient() { return g_XmppClient; } void SetRankedGame(bool isRanked) { g_rankedGame = isRanked; } #if CONFIG2_LOBBY void StartXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize) { if (g_XmppClient) { ScriptException::Raise(rq, "Cannot call StartXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), utf8_from_wstring(room), utf8_from_wstring(nick), historyRequestSize); g_rankedGame = true; } void StartRegisterXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password) { if (g_XmppClient) { ScriptException::Raise(rq, "Cannot call StartRegisterXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), std::string(), std::string(), 0, true); } void StopXmppClient(const ScriptRequest& rq) { if (!g_XmppClient) { ScriptException::Raise(rq, "Cannot call StopXmppClient without an initialized XmppClient!"); return; } SAFE_DELETE(g_XmppClient); g_rankedGame = false; } //////////////////////////////////////////////// //////////////////////////////////////////////// IXmppClient* XmppGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_XmppClient) { LOGERROR("Cannot use XMPPClient functions without an initialized XmppClient!"); return nullptr; } return g_XmppClient; } -void SendRegisterGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) +void SendRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) { if (!g_XmppClient) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); ScriptException::Raise(rq, "Cannot call SendRegisterGame without an initialized XmppClient!"); return; } // Prevent JS mods to register matches in the lobby that were started with lobby authentication disabled if (!g_NetServer || !g_NetServer->UseLobbyAuth()) { LOGERROR("Registering games in the lobby requires lobby authentication to be enabled!"); return; } - g_XmppClient->SendIqRegisterGame(*(pCmptPrivate->pScriptInterface), data); + g_XmppClient->SendIqRegisterGame(scriptInterface, data); } // Unlike other functions, this one just returns Undefined if XmppClient isn't initialised. JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) { if (!g_XmppClient) return JS::UndefinedValue(); return g_XmppClient->GuiPollNewMessages(scriptInterface); } // Non-public secure PBKDF2 hash function with salting and 1,337 iterations // // TODO: We should use libsodium's crypto_pwhash instead of this. The first reason is that // libsodium doesn't propose a bare PBKDF2 hash in its API and it's too bad to rely on custom // code when we have a fully-fledged library available; the second reason is that Argon2 (the // default algorithm for crypto_pwhash) is better than what we use (and it's the default one // in the lib for a reason). // However changing the hashing method should be planned carefully, by trying to login with a // password hashed the old way, and, if successful, updating the password in the database using // the new hashing method. Dropping the old hashing code can only be done either by giving users // a way to reset their password, or by keeping track of successful password updates and dropping // old unused accounts after some time. std::string EncryptPassword(const std::string& password, const std::string& username) { ENSURE(sodium_init() >= 0); const int DIGESTSIZE = crypto_hash_sha256_BYTES; const int ITERATIONS = 1337; cassert(DIGESTSIZE == 32); static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 133, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = {0}; crypto_hash_sha256_state state; crypto_hash_sha256_init(&state); crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); crypto_hash_sha256_update(&state, (unsigned char*)username.c_str(), username.length()); crypto_hash_sha256_final(&state, salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); } #endif void RegisterScriptFunctions(const ScriptRequest& rq) { // Lobby functions ScriptFunction::Register<&HasXmppClient>(rq, "HasXmppClient"); ScriptFunction::Register<&SetRankedGame>(rq, "SetRankedGame"); #if CONFIG2_LOBBY // Allow the lobby to be disabled ScriptFunction::Register<&StartXmppClient>(rq, "StartXmppClient"); ScriptFunction::Register<&StartRegisterXmppClient>(rq, "StartRegisterXmppClient"); ScriptFunction::Register<&StopXmppClient>(rq, "StopXmppClient"); #define REGISTER_XMPP(func, name) \ ScriptFunction::Register<&IXmppClient::func, &XmppGetter>(rq, name) REGISTER_XMPP(connect, "ConnectXmppClient"); REGISTER_XMPP(disconnect, "DisconnectXmppClient"); REGISTER_XMPP(isConnected, "IsXmppClientConnected"); REGISTER_XMPP(SendIqGetBoardList, "SendGetBoardList"); REGISTER_XMPP(SendIqGetProfile, "SendGetProfile"); REGISTER_XMPP(SendIqGameReport, "SendGameReport"); ScriptFunction::Register<&SendRegisterGame>(rq, "SendRegisterGame"); REGISTER_XMPP(SendIqUnregisterGame, "SendUnregisterGame"); REGISTER_XMPP(SendIqChangeStateGame, "SendChangeStateGame"); REGISTER_XMPP(GUIGetPlayerList, "GetPlayerList"); REGISTER_XMPP(GUIGetGameList, "GetGameList"); REGISTER_XMPP(GUIGetBoardList, "GetBoardList"); REGISTER_XMPP(GUIGetProfile, "GetProfile"); ScriptFunction::Register<&GuiPollNewMessages>(rq, "LobbyGuiPollNewMessages"); REGISTER_XMPP(GuiPollHistoricMessages, "LobbyGuiPollHistoricMessages"); REGISTER_XMPP(GuiPollHasPlayerListUpdate, "LobbyGuiPollHasPlayerListUpdate"); REGISTER_XMPP(SendMUCMessage, "LobbySendMessage"); REGISTER_XMPP(SetPresence, "LobbySetPlayerPresence"); REGISTER_XMPP(SetNick, "LobbySetNick"); REGISTER_XMPP(GetNick, "LobbyGetNick"); REGISTER_XMPP(GetJID, "LobbyGetJID"); REGISTER_XMPP(kick, "LobbyKick"); REGISTER_XMPP(ban, "LobbyBan"); REGISTER_XMPP(GetPresence, "LobbyGetPlayerPresence"); REGISTER_XMPP(GetRole, "LobbyGetPlayerRole"); REGISTER_XMPP(GetRating, "LobbyGetPlayerRating"); REGISTER_XMPP(GetSubject, "LobbyGetRoomSubject"); #undef REGISTER_XMPP ScriptFunction::Register<&EncryptPassword>(rq, "EncryptPassword"); #endif // CONFIG2_LOBBY } } Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 25441) +++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 25442) @@ -1,661 +1,662 @@ /* Copyright (C) 2021 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 "lib/ogl.h" #include "lib/svn_revision.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/libsdl.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/posix/posix_utsname.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/gfx.h" #include "lib/sysdep/numa.h" #include "lib/sysdep/os_cpu.h" #if ARCH_X86_X64 # include "lib/sysdep/arch/x86_x64/topology.h" #endif #if CONFIG2_AUDIO #include "soundmanager/SoundManager.h" #endif #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Debug.h" #include "ps/UserReport.h" #include "ps/VideoMode.h" #include "scriptinterface/FunctionWrapper.h" -#include "scriptinterface/Object.h" #include "scriptinterface/JSON.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/ScriptInterface.h" #if OS_LINUX #include #endif // TODO: Support OpenGL platforms which don't use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #include #include // Define the GLX_MESA_query_renderer macros if built with // an old Mesa (<10.0) that doesn't provide them #ifndef GLX_MESA_query_renderer #define GLX_MESA_query_renderer 1 #define GLX_RENDERER_VENDOR_ID_MESA 0x8183 #define GLX_RENDERER_DEVICE_ID_MESA 0x8184 #define GLX_RENDERER_VERSION_MESA 0x8185 #define GLX_RENDERER_ACCELERATED_MESA 0x8186 #define GLX_RENDERER_VIDEO_MEMORY_MESA 0x8187 #define GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA 0x8188 #define GLX_RENDERER_PREFERRED_PROFILE_MESA 0x8189 #define GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA 0x818A #define GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA 0x818B #define GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA 0x818C #define GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA 0x818D #define GLX_RENDERER_ID_MESA 0x818E #endif /* GLX_MESA_query_renderer */ #endif static void ReportSDL(const ScriptRequest& rq, JS::HandleValue settings); static void ReportGLLimits(const ScriptRequest& rq, JS::HandleValue settings); void SetDisableAudio(bool disabled) { g_DisableAudio = disabled; } void RunHardwareDetection() { TIMER(L"RunHardwareDetection"); ScriptInterface scriptInterface("Engine", "HWDetect", g_ScriptContext); ScriptRequest rq(scriptInterface); JSI_Debug::RegisterScriptFunctions(scriptInterface); // Engine.DisplayErrorDialog JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); ScriptFunction::Register(rq, "SetDisableAudio"); // Load the detection script: const wchar_t* scriptName = L"hwdetect/hwdetect.js"; CVFSFile file; if (file.Load(g_VFS, scriptName) != PSRETURN_OK) { LOGERROR("Failed to load hardware detection script"); return; } std::string code = file.DecodeUTF8(); // assume it's UTF-8 scriptInterface.LoadScript(scriptName, code); // Collect all the settings we'll pass to the script: // (We'll use this same data for the opt-in online reporting system, so it // includes some fields that aren't directly useful for the hwdetect script) JS::RootedValue settings(rq.cx); Script::CreateObject(rq, &settings); Script::SetProperty(rq, settings, "os_unix", OS_UNIX); Script::SetProperty(rq, settings, "os_bsd", OS_BSD); Script::SetProperty(rq, settings, "os_linux", OS_LINUX); Script::SetProperty(rq, settings, "os_android", OS_ANDROID); Script::SetProperty(rq, settings, "os_macosx", OS_MACOSX); Script::SetProperty(rq, settings, "os_win", OS_WIN); Script::SetProperty(rq, settings, "arch_ia32", ARCH_IA32); Script::SetProperty(rq, settings, "arch_amd64", ARCH_AMD64); Script::SetProperty(rq, settings, "arch_arm", ARCH_ARM); Script::SetProperty(rq, settings, "arch_aarch64", ARCH_AARCH64); Script::SetProperty(rq, settings, "arch_e2k", ARCH_E2K); Script::SetProperty(rq, settings, "arch_ppc64", ARCH_PPC64); #ifdef NDEBUG Script::SetProperty(rq, settings, "build_debug", 0); #else Script::SetProperty(rq, settings, "build_debug", 1); #endif Script::SetProperty(rq, settings, "build_opengles", CONFIG2_GLES); Script::SetProperty(rq, settings, "build_datetime", std::string(__DATE__ " " __TIME__)); Script::SetProperty(rq, settings, "build_revision", std::wstring(svn_revision)); Script::SetProperty(rq, settings, "build_msc", (int)MSC_VERSION); Script::SetProperty(rq, settings, "build_icc", (int)ICC_VERSION); Script::SetProperty(rq, settings, "build_gcc", (int)GCC_VERSION); Script::SetProperty(rq, settings, "build_clang", (int)CLANG_VERSION); Script::SetProperty(rq, settings, "gfx_card", gfx::CardName()); Script::SetProperty(rq, settings, "gfx_drv_ver", gfx::DriverInfo()); #if CONFIG2_AUDIO if (g_SoundManager) { Script::SetProperty(rq, settings, "snd_card", g_SoundManager->GetSoundCardNames()); Script::SetProperty(rq, settings, "snd_drv_ver", g_SoundManager->GetOpenALVersion()); } #endif ReportSDL(scriptInterface, settings); ReportGLLimits(scriptInterface, settings); Script::SetProperty(rq, settings, "video_desktop_xres", g_VideoMode.GetDesktopXRes()); Script::SetProperty(rq, settings, "video_desktop_yres", g_VideoMode.GetDesktopYRes()); Script::SetProperty(rq, settings, "video_desktop_bpp", g_VideoMode.GetDesktopBPP()); Script::SetProperty(rq, settings, "video_desktop_freq", g_VideoMode.GetDesktopFreq()); struct utsname un; uname(&un); Script::SetProperty(rq, settings, "uname_sysname", std::string(un.sysname)); Script::SetProperty(rq, settings, "uname_release", std::string(un.release)); Script::SetProperty(rq, settings, "uname_version", std::string(un.version)); Script::SetProperty(rq, settings, "uname_machine", std::string(un.machine)); #if OS_LINUX { std::ifstream ifs("/etc/lsb-release"); if (ifs.good()) { std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); Script::SetProperty(rq, settings, "linux_release", str); } } #endif Script::SetProperty(rq, settings, "cpu_identifier", std::string(cpu_IdentifierString())); Script::SetProperty(rq, settings, "cpu_frequency", os_cpu_ClockFrequency()); Script::SetProperty(rq, settings, "cpu_pagesize", (u32)os_cpu_PageSize()); Script::SetProperty(rq, settings, "cpu_largepagesize", (u32)os_cpu_LargePageSize()); Script::SetProperty(rq, settings, "cpu_numprocs", (u32)os_cpu_NumProcessors()); #if ARCH_X86_X64 Script::SetProperty(rq, settings, "cpu_numpackages", (u32)topology::NumPackages()); Script::SetProperty(rq, settings, "cpu_coresperpackage", (u32)topology::CoresPerPackage()); Script::SetProperty(rq, settings, "cpu_logicalpercore", (u32)topology::LogicalPerCore()); #endif Script::SetProperty(rq, settings, "numa_numnodes", (u32)numa_NumNodes()); Script::SetProperty(rq, settings, "numa_factor", numa_Factor()); Script::SetProperty(rq, settings, "numa_interleaved", numa_IsMemoryInterleaved()); Script::SetProperty(rq, settings, "ram_total", (u32)os_cpu_MemorySize()); Script::SetProperty(rq, settings, "ram_total_os", (u32)os_cpu_QueryMemorySize()); #if ARCH_X86_X64 Script::SetProperty(rq, settings, "x86_vendor", (u32)x86_x64::Vendor()); Script::SetProperty(rq, settings, "x86_model", (u32)x86_x64::Model()); Script::SetProperty(rq, settings, "x86_family", (u32)x86_x64::Family()); u32 caps0, caps1, caps2, caps3; x86_x64::GetCapBits(&caps0, &caps1, &caps2, &caps3); Script::SetProperty(rq, settings, "x86_caps[0]", caps0); Script::SetProperty(rq, settings, "x86_caps[1]", caps1); Script::SetProperty(rq, settings, "x86_caps[2]", caps2); Script::SetProperty(rq, settings, "x86_caps[3]", caps3); #endif Script::SetProperty(rq, settings, "timer_resolution", timer_Resolution()); // The version should be increased for every meaningful change. const int reportVersion = 14; // Send the same data to the reporting system g_UserReporter.SubmitReport( "hwdetect", reportVersion, Script::StringifyJSON(rq, &settings, false), Script::StringifyJSON(rq, &settings, true)); // Run the detection script: JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "RunHardwareDetection", settings); } static void ReportSDL(const ScriptRequest& rq, JS::HandleValue settings) { SDL_version build, runtime; SDL_VERSION(&build); char version[16]; snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", build.major, build.minor, build.patch); Script::SetProperty(rq, settings, "sdl_build_version", version); SDL_GetVersion(&runtime); snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", runtime.major, runtime.minor, runtime.patch); Script::SetProperty(rq, settings, "sdl_runtime_version", version); // This is null in atlas (and further the call triggers an assertion). const char* backend = g_VideoMode.GetWindow() ? GetSDLSubsystem(g_VideoMode.GetWindow()) : "none"; Script::SetProperty(rq, settings, "sdl_video_backend", backend ? backend : "unknown"); } static void ReportGLLimits(const ScriptRequest& rq, JS::HandleValue settings) { const char* errstr = "(error)"; #define INTEGER(id) do { \ GLint i = -1; \ glGetIntegerv(GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ Script::SetProperty(rq, settings, "GL_" #id, errstr); \ else \ Script::SetProperty(rq, settings, "GL_" #id, i); \ } while (false) #define INTEGER2(id) do { \ GLint i[2] = { -1, -1 }; \ glGetIntegerv(GL_##id, i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ Script::SetProperty(rq, settings, "GL_" #id "[0]", errstr); \ Script::SetProperty(rq, settings, "GL_" #id "[1]", errstr); \ } else { \ Script::SetProperty(rq, settings, "GL_" #id "[0]", i[0]); \ Script::SetProperty(rq, settings, "GL_" #id "[1]", i[1]); \ } \ } while (false) #define FLOAT(id) do { \ GLfloat f = std::numeric_limits::quiet_NaN(); \ glGetFloatv(GL_##id, &f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ Script::SetProperty(rq, settings, "GL_" #id, errstr); \ else \ Script::SetProperty(rq, settings, "GL_" #id, f); \ } while (false) #define FLOAT2(id) do { \ GLfloat f[2] = { std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; \ glGetFloatv(GL_##id, f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ Script::SetProperty(rq, settings, "GL_" #id "[0]", errstr); \ Script::SetProperty(rq, settings, "GL_" #id "[1]", errstr); \ } else { \ Script::SetProperty(rq, settings, "GL_" #id "[0]", f[0]); \ Script::SetProperty(rq, settings, "GL_" #id "[1]", f[1]); \ } \ } while (false) #define STRING(id) do { \ const char* c = (const char*)glGetString(GL_##id); \ if (!c) c = ""; \ if (ogl_SquelchError(GL_INVALID_ENUM)) c = errstr; \ Script::SetProperty(rq, settings, "GL_" #id, std::string(c)); \ } while (false) #define QUERY(target, pname) do { \ GLint i = -1; \ pglGetQueryivARB(GL_##target, GL_##pname, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ Script::SetProperty(rq, settings, "GL_" #target ".GL_" #pname, errstr); \ else \ Script::SetProperty(rq, settings, "GL_" #target ".GL_" #pname, i); \ } while (false) #define VERTEXPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ Script::SetProperty(rq, settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, errstr); \ else \ Script::SetProperty(rq, settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define FRAGMENTPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ Script::SetProperty(rq, settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, errstr); \ else \ Script::SetProperty(rq, settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define BOOL(id) INTEGER(id) ogl_WarnIfError(); // Core OpenGL 1.3: // (We don't bother checking extension strings for anything older than 1.3; // it'll just produce harmless warnings) STRING(VERSION); STRING(VENDOR); STRING(RENDERER); STRING(EXTENSIONS); #if !CONFIG2_GLES INTEGER(MAX_LIGHTS); INTEGER(MAX_CLIP_PLANES); // Skip MAX_COLOR_MATRIX_STACK_DEPTH (only in imaging subset) INTEGER(MAX_MODELVIEW_STACK_DEPTH); INTEGER(MAX_PROJECTION_STACK_DEPTH); INTEGER(MAX_TEXTURE_STACK_DEPTH); #endif INTEGER(SUBPIXEL_BITS); #if !CONFIG2_GLES INTEGER(MAX_3D_TEXTURE_SIZE); #endif INTEGER(MAX_TEXTURE_SIZE); INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE); #if !CONFIG2_GLES INTEGER(MAX_PIXEL_MAP_TABLE); INTEGER(MAX_NAME_STACK_DEPTH); INTEGER(MAX_LIST_NESTING); INTEGER(MAX_EVAL_ORDER); #endif INTEGER2(MAX_VIEWPORT_DIMS); #if !CONFIG2_GLES INTEGER(MAX_ATTRIB_STACK_DEPTH); INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH); INTEGER(AUX_BUFFERS); BOOL(RGBA_MODE); BOOL(INDEX_MODE); BOOL(DOUBLEBUFFER); BOOL(STEREO); #endif FLOAT2(ALIASED_POINT_SIZE_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_POINT_SIZE_RANGE); FLOAT(SMOOTH_POINT_SIZE_GRANULARITY); #endif FLOAT2(ALIASED_LINE_WIDTH_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_LINE_WIDTH_RANGE); FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY); // Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT (only in imaging subset) INTEGER(MAX_ELEMENTS_INDICES); INTEGER(MAX_ELEMENTS_VERTICES); INTEGER(MAX_TEXTURE_UNITS); #endif INTEGER(SAMPLE_BUFFERS); INTEGER(SAMPLES); // TODO: compressed texture formats INTEGER(RED_BITS); INTEGER(GREEN_BITS); INTEGER(BLUE_BITS); INTEGER(ALPHA_BITS); #if !CONFIG2_GLES INTEGER(INDEX_BITS); #endif INTEGER(DEPTH_BITS); INTEGER(STENCIL_BITS); #if !CONFIG2_GLES INTEGER(ACCUM_RED_BITS); INTEGER(ACCUM_GREEN_BITS); INTEGER(ACCUM_BLUE_BITS); INTEGER(ACCUM_ALPHA_BITS); #endif #if !CONFIG2_GLES // Core OpenGL 2.0 (treated as extensions): if (ogl_HaveExtension("GL_EXT_texture_lod_bias")) { FLOAT(MAX_TEXTURE_LOD_BIAS_EXT); } if (ogl_HaveExtension("GL_ARB_occlusion_query")) { QUERY(SAMPLES_PASSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_shading_language_100")) { STRING(SHADING_LANGUAGE_VERSION_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader")) { INTEGER(MAX_VERTEX_ATTRIBS_ARB); INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_VARYING_FLOATS_ARB); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB); } if (ogl_HaveExtension("GL_ARB_fragment_shader")) { INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader") || ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_TEXTURE_COORDS_ARB); } if (ogl_HaveExtension("GL_ARB_draw_buffers")) { INTEGER(MAX_DRAW_BUFFERS_ARB); } // Core OpenGL 3.0: if (ogl_HaveExtension("GL_EXT_gpu_shader4")) { INTEGER(MIN_PROGRAM_TEXEL_OFFSET); // no _EXT version of these in glext.h INTEGER(MAX_PROGRAM_TEXEL_OFFSET); } if (ogl_HaveExtension("GL_EXT_framebuffer_object")) { INTEGER(MAX_COLOR_ATTACHMENTS_EXT); INTEGER(MAX_RENDERBUFFER_SIZE_EXT); } if (ogl_HaveExtension("GL_EXT_framebuffer_multisample")) { INTEGER(MAX_SAMPLES_EXT); } if (ogl_HaveExtension("GL_EXT_texture_array")) { INTEGER(MAX_ARRAY_TEXTURE_LAYERS_EXT); } if (ogl_HaveExtension("GL_EXT_transform_feedback")) { INTEGER(MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT); } // Other interesting extensions: if (ogl_HaveExtension("GL_EXT_timer_query") || ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIME_ELAPSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIMESTAMP, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic")) { FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT); } if (ogl_HaveExtension("GL_ARB_texture_rectangle")) { INTEGER(MAX_RECTANGLE_TEXTURE_SIZE_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_PROGRAM_MATRICES_ARB); INTEGER(MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program")) { VERTEXPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); if (ogl_HaveExtension("GL_ARB_fragment_program")) { // The spec seems to say these should be supported, but // Mesa complains about them so let's not bother /* VERTEXPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_fragment_program")) { FRAGMENTPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); if (ogl_HaveExtension("GL_ARB_vertex_program")) { // The spec seems to say these should be supported, but // Intel drivers on Windows complain about them so let's not bother /* FRAGMENTPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_geometry_shader4")) { INTEGER(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_GEOMETRY_OUTPUT_VERTICES_ARB); INTEGER(MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_VARYING_COMPONENTS_ARB); INTEGER(MAX_VERTEX_VARYING_COMPONENTS_ARB); } #else // CONFIG2_GLES // Core OpenGL ES 2.0: STRING(SHADING_LANGUAGE_VERSION); INTEGER(MAX_VERTEX_ATTRIBS); INTEGER(MAX_VERTEX_UNIFORM_VECTORS); INTEGER(MAX_VARYING_VECTORS); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_FRAGMENT_UNIFORM_VECTORS); INTEGER(MAX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_RENDERBUFFER_SIZE); #endif // CONFIG2_GLES // TODO: Support OpenGL platforms which don't use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #define GLXQCR_INTEGER(id) do { \ unsigned int i = UINT_MAX; \ if (pglXQueryCurrentRendererIntegerMESA(id, &i)) \ Script::SetProperty(rq, settings, #id, i); \ } while (false) #define GLXQCR_INTEGER2(id) do { \ unsigned int i[2] = { UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ Script::SetProperty(rq, settings, #id "[0]", i[0]); \ Script::SetProperty(rq, settings, #id "[1]", i[1]); \ } \ } while (false) #define GLXQCR_INTEGER3(id) do { \ unsigned int i[3] = { UINT_MAX, UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ Script::SetProperty(rq, settings, #id "[0]", i[0]); \ Script::SetProperty(rq, settings, #id "[1]", i[1]); \ Script::SetProperty(rq, settings, #id "[2]", i[2]); \ } \ } while (false) #define GLXQCR_STRING(id) do { \ const char* str = pglXQueryCurrentRendererStringMESA(id); \ if (str) \ Script::SetProperty(rq, settings, #id ".string", str); \ } while (false) SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); const int ret = SDL_GetWindowWMInfo(g_VideoMode.GetWindow(), &wminfo); if (ret && wminfo.subsystem == SDL_SYSWM_X11) { Display* dpy = wminfo.info.x11.display; int scrnum = DefaultScreen(dpy); const char* glxexts = glXQueryExtensionsString(dpy, scrnum); Script::SetProperty(rq, settings, "glx_extensions", glxexts); if (strstr(glxexts, "GLX_MESA_query_renderer") && pglXQueryCurrentRendererIntegerMESA && pglXQueryCurrentRendererStringMESA) { GLXQCR_INTEGER(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_INTEGER(GLX_RENDERER_DEVICE_ID_MESA); GLXQCR_INTEGER3(GLX_RENDERER_VERSION_MESA); GLXQCR_INTEGER(GLX_RENDERER_ACCELERATED_MESA); GLXQCR_INTEGER(GLX_RENDERER_VIDEO_MEMORY_MESA); GLXQCR_INTEGER(GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA); GLXQCR_INTEGER(GLX_RENDERER_PREFERRED_PROFILE_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA); GLXQCR_STRING(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_STRING(GLX_RENDERER_DEVICE_ID_MESA); } } #endif // SDL_VIDEO_DRIVER_X11 } Index: ps/trunk/source/ps/scripting/JSInterface_Mod.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 25441) +++ ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 25442) @@ -1,53 +1,53 @@ /* Copyright (C) 2021 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 "JSInterface_Mod.h" #include "ps/Mod.h" #include "scriptinterface/FunctionWrapper.h" extern void RestartEngine(); -namespace +namespace JSI_Mod { -bool SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods) +bool SetModsAndRestartEngine(const ScriptInterface& scriptInterface, const std::vector& mods) { Mod::ClearIncompatibleMods(); - if (!Mod::CheckAndEnableMods(*(pCmptPrivate->pScriptInterface), mods)) + if (!Mod::CheckAndEnableMods(scriptInterface, mods)) return false; RestartEngine(); return true; } -} -bool HasFailedMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool HasFailedMods() { return Mod::GetFailedMods().size() > 0; } -void JSI_Mod::RegisterScriptFunctions(const ScriptRequest& rq) +void RegisterScriptFunctions(const ScriptRequest& rq) { ScriptFunction::Register<&Mod::GetEngineInfo>(rq, "GetEngineInfo"); ScriptFunction::Register<&Mod::GetAvailableMods>(rq, "GetAvailableMods"); ScriptFunction::Register<&Mod::GetEnabledMods>(rq, "GetEnabledMods"); ScriptFunction::Register (rq, "HasFailedMods"); ScriptFunction::Register<&Mod::GetFailedMods>(rq, "GetFailedMods"); ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine"); } +} Index: ps/trunk/source/scriptinterface/FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 25441) +++ ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 25442) @@ -1,426 +1,406 @@ /* Copyright (C) 2021 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_FUNCTIONWRAPPER #define INCLUDED_FUNCTIONWRAPPER #include "Object.h" #include "ScriptConversions.h" #include "ScriptExceptions.h" -#include "ScriptInterface.h" #include "ScriptRequest.h" +#include +#include + +class ScriptInterface; + /** * This class introduces templates to conveniently wrap C++ functions in JSNative functions. * This _is_ rather template heavy, so compilation times beware. * The C++ code can have arbitrary arguments and arbitrary return types, so long * as they can be converted to/from JS using Script::ToJSVal (FromJSVal respectively), * and they are default-constructible (TODO: that can probably changed). * (This could be a namespace, but I like being able to specify public/private). */ class ScriptFunction { private: ScriptFunction() = delete; ScriptFunction(const ScriptFunction&) = delete; ScriptFunction(ScriptFunction&&) = delete; /** * In JS->C++ calls, types are converted using FromJSVal, * and this requires them to be default-constructible (as that function takes an out parameter) * thus constref needs to be removed when defining the tuple. * Exceptions are: * - const ScriptRequest& (as the first argument only, for implementation simplicity). * - const ScriptInterface& (as the first argument only, for implementation simplicity). * - JS::HandleValue */ template using type_transform = std::conditional_t< std::is_same_v || std::is_same_v, T, std::remove_const_t> >; /** * Convenient struct to get info on a [class] [const] function pointer. * TODO VS19: I ran into a really weird bug with an auto specialisation on this taking function pointers. * It'd be good to add it back once we upgrade. */ template struct args_info; template struct args_info { static constexpr const size_t nb_args = sizeof...(Types); using return_type = R; using object_type = void; using arg_types = std::tuple...>; }; template struct args_info : public args_info { using object_type = C; }; template struct args_info : public args_info {}; /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * DoConvertFromJS takes a type, a JS argument, and converts. * The type T must be default constructible (except for HandleValue, which is handled specially). * (possible) TODO: this could probably be changed if FromJSVal had a different signature. * @param went_ok - true if the conversion succeeded and went_ok was true before, false otherwise. */ template static std::tuple DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok) { // No need to convert JS values. if constexpr (std::is_same_v) { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return std::forward_as_tuple(JS::UndefinedHandleValue); else { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(args[idx]); // This passes the null handle value if idx is beyond the length of args. } } else { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return std::forward_as_tuple(T{}); else { T ret; went_ok &= Script::FromJSVal(rq, args[idx], ret); return std::forward_as_tuple(ret); } } } /** * Recursive wrapper: calls DoConvertFromJS for type T and recurses. */ template static std::tuple DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok) { return std::tuple_cat(DoConvertFromJS(rq, args, went_ok), DoConvertFromJS(rq, args, went_ok)); } /** * ConvertFromJS is a wrapper around DoConvertFromJS, and serves to: * - unwrap the tuple types as a parameter pack - * - handle specific cases for the first argument (cmptPrivate, ScriptRequest). + * - handle specific cases for the first argument (ScriptRequest, ...). * * Trick: to unpack the types of the tuple as a parameter pack, we deduce them from the function signature. * To do that, we want the tuple in the arguments, but we don't want to actually have to default-instantiate, * so we'll pass a nullptr that's static_cast to what we want. */ template - static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate*, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) + static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return {}; } else return DoConvertFromJS<0, Types...>(rq, args, went_ok); } - // Overloads for CmptPrivate* first argument. - template - static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate* cmptPrivate, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) - { - if constexpr (sizeof...(Types) == 0) - { - // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. - UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); - return std::forward_as_tuple(cmptPrivate); - } - else - return std::tuple_cat(std::forward_as_tuple(cmptPrivate), DoConvertFromJS<0, Types...>(rq, args, went_ok)); - } - // Overloads for ScriptRequest& first argument. template - static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate*, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) + static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(rq); } else return std::tuple_cat(std::forward_as_tuple(rq), DoConvertFromJS<0, Types...>(rq, args, went_ok)); } // Overloads for ScriptInterface& first argument. template - static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate* cmptPrivate, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) + static std::tuple ConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); - return std::forward_as_tuple(*cmptPrivate->pScriptInterface); + return std::forward_as_tuple(rq.GetScriptInterface()); } else - return std::tuple_cat(std::forward_as_tuple(*cmptPrivate->pScriptInterface), DoConvertFromJS<0, Types...>(rq, args, went_ok)); + return std::tuple_cat(std::forward_as_tuple(rq.GetScriptInterface()), DoConvertFromJS<0, Types...>(rq, args, went_ok)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * Wrap std::apply for the case where we have an object method or a regular function. */ template static typename args_info::return_type call(T* object, tuple& args) { if constexpr(std::is_same_v) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(object); return std::apply(callable, args); } else return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// struct IgnoreResult_t {}; static inline IgnoreResult_t IgnoreResult; /** * Recursive helper to call AssignOrToJSVal */ template static void AssignOrToJSValHelper(const ScriptRequest& rq, JS::MutableHandleValueVector argv, const T& a, const Ts&... params) { Script::ToJSVal(rq, argv[i], a); AssignOrToJSValHelper(rq, argv, params...); } template static void AssignOrToJSValHelper(const ScriptRequest& UNUSED(rq), JS::MutableHandleValueVector UNUSED(argv)) { static_assert(sizeof...(Ts) == 0); // Nop, for terminating the template recursion. } /** * Wrapper around calling a JS function from C++. * Arguments are const& to avoid lvalue/rvalue issues, and so can't be used as out-parameters. * In particular, the problem is that Rooted are deduced as Rooted, not Handle, and so can't be copied. * This could be worked around with more templates, but it doesn't seem particularly worth doing. */ template static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) { JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; JS::RootedValueVector argv(rq.cx); ignore_result(argv.resize(sizeof...(Args))); AssignOrToJSValHelper<0>(rq, &argv, args...); bool success; if constexpr (std::is_same_v) success = JS_CallFunctionName(rq.cx, obj, name, argv, ret); else { JS::RootedValue jsRet(rq.cx); success = JS_CallFunctionName(rq.cx, obj, name, argv, &jsRet); if constexpr (!std::is_same_v) { if (success) Script::FromJSVal(rq, jsRet, ret); } else UNUSED2(ret); // VS2017 complains. } // Even if everything succeeded, there could be pending exceptions return !ScriptException::CatchPending(rq) && success; } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// public: template using ObjectGetter = T*(*)(const ScriptRequest&, JS::CallArgs&); // TODO: the fact that this takes class and not auto is to work around an odd VS17 bug. // It can be removed with VS19. template using GetterFor = ObjectGetter::object_type>; /** * The meat of this file. This wraps a C++ function into a JSNative, * so that it can be called from JS and manipulated in Spidermonkey. * Most C++ functions can be directly wrapped, so long as their arguments are * convertible from JS::Value and their return value is convertible to JS::Value (or void) - * The C++ function may optionally take const ScriptRequest& or CmptPrivate* as its first argument. + * The C++ function may optionally take const ScriptRequest& or ScriptInterface& as its first argument. * The function may be an object method, in which case you need to pass an appropriate getter * * Optimisation note: the ScriptRequest object is created even without arguments, * as it's necessary for IsExceptionPending. * * @param thisGetter to get the object, if necessary. */ template thisGetter = nullptr> static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp) { using ObjType = typename args_info::object_type; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptInterface* scriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; - ScriptRequest rq(*scriptInterface); + ScriptRequest rq(cx); // If the callable is an object method, we must specify how to fetch the object. static_assert(std::is_same_v::object_type, void> || thisGetter != nullptr, "ScriptFunction::Register - No getter specified for object method"); // GCC 7 triggers spurious warnings #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Waddress" #endif ObjType* obj = nullptr; if constexpr (thisGetter != nullptr) { obj = thisGetter(rq, args); if (!obj) return false; } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif bool went_ok = true; - typename args_info::arg_types outs = ConvertFromJS(ScriptInterface::GetScriptInterfaceAndCBData(cx), rq, args, went_ok, static_cast::arg_types*>(nullptr)); + typename args_info::arg_types outs = ConvertFromJS(rq, args, went_ok, static_cast::arg_types*>(nullptr)); if (!went_ok) return false; /** * TODO: error handling isn't standard, and since this can call any C++ function, * there's no simple obvious way to deal with it. * For now we check for pending JS exceptions, but it would probably be nicer * to standardise on something, or perhaps provide an "errorHandler" here. */ if constexpr (std::is_same_v::return_type>) call(obj, outs); else if constexpr (std::is_same_v::return_type>) args.rval().set(call(obj, outs)); else Script::ToJSVal(rq, args.rval(), call(obj, outs)); return !ScriptException::IsPending(rq); } /** * Call a JS function @a name, property of object @a val, with the arguments @a args. * @a ret will be updated with the return value, if any. * @return the success (or failure) thereof. */ template static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) { return Call_(rq, val, name, ret, std::forward(args)...); } // Specialisation for MutableHandleValue return. template static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args) { return Call_(rq, val, name, ret, std::forward(args)...); } /** * Call a JS function @a name, property of object @a val, with the arguments @a args. * @return the success (or failure) thereof. */ template static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args) { return Call(rq, val, name, IgnoreResult, std::forward(args)...); } /** * Return a function spec from a C++ function. */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static JSFunctionSpec Wrap(const char* name) { return JS_FN(name, (&ToJSNative), args_info::nb_args, flags); } /** * Return a JSFunction from a C++ function. */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static JSFunction* Create(const ScriptRequest& rq, const char* name) { return JS_NewFunction(rq.cx, &ToJSNative, args_info::nb_args, flags, name); } /** * Register a function on the native scope (usually 'Engine'). */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static void Register(const ScriptRequest& rq, const char* name) { JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative, args_info::nb_args, flags); } /** * Register a function on @param scope. * Prefer the version taking ScriptRequest unless you have a good reason not to. * @see Register */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static void Register(JSContext* cx, JS::HandleObject scope, const char* name) { JS_DefineFunction(cx, scope, name, &ToJSNative, args_info::nb_args, flags); } - - /** - * Convert the CmptPrivate callback data to T* - */ - template - static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&) - { - return static_cast(ScriptInterface::GetScriptInterfaceAndCBData(rq.cx)->pCBData); - } }; #endif // INCLUDED_FUNCTIONWRAPPER Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 25441) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 25442) @@ -1,684 +1,706 @@ /* Copyright (C) 2021 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 "FunctionWrapper.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "StructuredClone.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ - struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; /** * Constructor for ScriptRequest - here because it needs access into ScriptInterface_impl. */ ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : - cx(scriptInterface.m->m_cx), glob(scriptInterface.m->m_glob), nativeScope(scriptInterface.m->m_nativeScope) + cx(scriptInterface.m->m_cx), + glob(scriptInterface.m->m_glob), + nativeScope(scriptInterface.m->m_nativeScope), + m_ScriptInterface(scriptInterface) { - m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); + m_FormerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); } ScriptRequest::~ScriptRequest() { - JS::LeaveRealm(cx, m_formerRealm); + JS::LeaveRealm(cx, m_FormerRealm); +} + +ScriptRequest::ScriptRequest(JSContext* cx) : ScriptRequest(ScriptInterface::CmptPrivate::GetScriptInterface(cx)) +{ } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } +const ScriptInterface& ScriptRequest::GetScriptInterface() const +{ + return m_ScriptInterface; +} namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + ScriptRequest rq(cx); for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!Script::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } JS::Value deepcopy(const ScriptRequest& rq, JS::HandleValue val) { if (val.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy requires one argument."); return JS::UndefinedValue(); } JS::RootedValue ret(rq.cx, Script::DeepCopy(rq, val)); if (ret.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy StructureClone copy failed."); return JS::UndefinedValue(); } return ret; } JS::Value deepfreeze(const ScriptInterface& scriptInterface, JS::HandleValue val) { ScriptRequest rq(scriptInterface); if (!val.isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return JS::UndefinedValue(); } Script::FreezeObject(rq, val, true); return val; } void ProfileStart(const std::string& regionName) { const char* name = "(ProfileStart)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!regionName.empty()) name = StringFlyweight(regionName).get().c_str(); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); } void ProfileStop() { if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); } void ProfileAttribute(const std::string& attr) { const char* name = "(ProfileAttribute)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!attr.empty()) name = StringFlyweight(attr).get().c_str(); g_Profiler2.RecordAttribute("%s", name); } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } -bool Math_random(JSContext* cx, uint argc, JS::Value* vp) +} // anonymous namespace + +bool ScriptInterface::MathRandom(double& nbr) const { - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - double r; - if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) + if (m->m_rng == nullptr) return false; - - args.rval().setNumber(r); + nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } -} // anonymous namespace - -bool ScriptInterface::MathRandom(double& nbr) +bool ScriptInterface::Math_random(JSContext* cx, uint argc, JS::Value* vp) { - if (m->m_rng == NULL) + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + double r; + if (!ScriptInterface::CmptPrivate::GetScriptInterface(cx).MathRandom(r)) return false; - nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); + + args.rval().setNumber(r); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); // These first 4 actually use CallArgs & thus don't use ScriptFunction JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register(m_cx, m_glob, "clone"); ScriptFunction::Register(m_cx, m_glob, "deepfreeze"); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register<&ProfileStart>(m_cx, m_nativeScope, "ProfileStart"); ScriptFunction::Register<&ProfileStop>(m_cx, m_nativeScope, "ProfileStop"); ScriptFunction::Register<&ProfileAttribute>(m_cx, m_nativeScope, "ProfileAttribute"); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(std::make_unique(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } -void ScriptInterface::SetCallbackData(void* pCBData) +const ScriptInterface& ScriptInterface::CmptPrivate::GetScriptInterface(JSContext *cx) { - m_CmptPrivate.pCBData = pCBData; + CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); + ENSURE(pCmptPrivate); + return *pCmptPrivate->pScriptInterface; } -ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) +void* ScriptInterface::CmptPrivate::GetCBData(JSContext *cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); - return pCmptPrivate; + return pCmptPrivate ? pCmptPrivate->pCBData : nullptr; } +void ScriptInterface::SetCallbackData(void* pCBData) +{ + m_CmptPrivate.pCBData = pCBData; +} + +template <> +void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq) +{ + return ScriptInterface::CmptPrivate::GetCBData(rq.cx); +} bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 25441) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 25442) @@ -1,259 +1,292 @@ /* Copyright (C) 2021 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_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "ps/Errors.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptRequest.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local std::shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } class Path; using VfsPath = Path; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; + public: /** * Constructor. * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context); ~ScriptInterface(); struct CmptPrivate { + friend class ScriptInterface; + public: + static const ScriptInterface& GetScriptInterface(JSContext* cx); + static void* GetCBData(JSContext* cx); + protected: ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions - } m_CmptPrivate; + }; void SetCallbackData(void* pCBData); - static CmptPrivate* GetScriptInterfaceAndCBData(JSContext* cx); + + /** + * Convert the CmptPrivate callback data to T* + */ + template + static T* ObjectFromCBData(const ScriptRequest& rq) + { + static_assert(!std::is_same_v); + ScriptInterface::CmptPrivate::GetCBData(rq.cx); + return static_cast(ObjectFromCBData(rq)); + } + + /** + * Variant for the function wrapper. + */ + template + static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&) + { + return ObjectFromCBData(rq); + } /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; shared_ptr GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number generator with a seeded, network-synced one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Get an object from the global scope or any lexical scope. * This can return globally accessible objects even if they are not properties * of the global object (e.g. ES6 class definitions). * @param name - Name of the property. * @param out The object or null. */ static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** - * MathRandom (this function) calls the random number generator assigned to this ScriptInterface instance and - * returns the generated number. - * Math_random (with underscore, not this function) is a global function, but different random number generators can be - * stored per ScriptInterface. It calls MathRandom of the current ScriptInterface instance. + * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number. */ - bool MathRandom(double& nbr); + bool MathRandom(double& nbr) const; + + /** + * JSNative wrapper of the above. + */ + static bool Math_random(JSContext* cx, uint argc, JS::Value* vp); /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } private: bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; + CmptPrivate m_CmptPrivate; + // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; std::map m_CustomObjectTypes; }; +// Explicitly instantiate void* as that is used for the generic template, +// and we want to define it in the .cpp file. +template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq); + template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return Script::FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/scriptinterface/ScriptRequest.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptRequest.h (revision 25441) +++ ps/trunk/source/scriptinterface/ScriptRequest.h (revision 25442) @@ -1,83 +1,101 @@ /* Copyright (C) 2021 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_SCRIPTREQUEST #define INCLUDED_SCRIPTREQUEST #include "scriptinterface/ScriptForward.h" // Ignore warnings in SM headers. #if GCC_VERSION || CLANG_VERSION # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-parameter" # pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #elif MSC_VERSION # pragma warning(push, 1) #endif #include "js/RootingAPI.h" #if GCC_VERSION || CLANG_VERSION # pragma GCC diagnostic pop #elif MSC_VERSION # pragma warning(pop) #endif #include class ScriptInterface; /** * Spidermonkey maintains some 'local' state via the JSContext* object. * This object is an argument to most JSAPI functions. * Furthermore, this state is Realm (~ global) dependent. For many reasons, including GC safety, * The JSContext* Realm must be set up correctly when accessing it. * 'Entering' and 'Leaving' realms must be done in a LIFO manner. * SM recommends using JSAutoRealm, which provides an RAII option. * * ScriptRequest combines both of the above in a single convenient package, * providing safe access to the JSContext*, the global object, and ensuring that the proper realm has been entered. * Most scriptinterface/ functions will take a ScriptRequest, to ensure proper rooting. You may sometimes * have to create one from a ScriptInterface. * * Be particularly careful when manipulating several script interfaces. */ class ScriptRequest { ScriptRequest() = delete; ScriptRequest(const ScriptRequest& rq) = delete; ScriptRequest& operator=(const ScriptRequest& rq) = delete; public: /** * NB: the definitions are in scriptinterface.cpp, because these access members of the PImpled * implementation of ScriptInterface, and that seemed more convenient. */ ScriptRequest(const ScriptInterface& scriptInterface); ScriptRequest(const ScriptInterface* scriptInterface) : ScriptRequest(*scriptInterface) {} ScriptRequest(std::shared_ptr scriptInterface) : ScriptRequest(*scriptInterface) {} ~ScriptRequest(); + /** + * Create a script request from a JSContext. + * This can be used to get the script interface in a JSNative function. + * In general, you shouldn't have to rely on this otherwise. + */ + ScriptRequest(JSContext* cx); + + /** + * Return the scriptInterface active when creating this ScriptRequest. + * Note that this is multi-request safe: even if another ScriptRequest is created, + * it will point to the original scriptInterface, and thus can be used to re-enter the realm. + */ + const ScriptInterface& GetScriptInterface() const; + JS::Value globalValue() const; + + // Note that JSContext actually changes behind the scenes when creating another ScriptRequest for another realm, + // so be _very_ careful when juggling between different realms. JSContext* cx; JS::HandleObject glob; JS::HandleObject nativeScope; private: - JS::Realm* m_formerRealm; + const ScriptInterface& m_ScriptInterface; + JS::Realm* m_FormerRealm; }; #endif // INCLUDED_SCRIPTREQUEST Index: ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h (revision 25441) +++ ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h (revision 25442) @@ -1,111 +1,111 @@ /* Copyright (C) 2021 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 "scriptinterface/FunctionWrapper.h" class TestFunctionWrapper : public CxxTest::TestSuite { public: // TODO C++20: use lambda functions directly, names are 'N params, void/returns'. static void _1p_v(int) {}; static void _3p_v(int, bool, std::string) {}; static int _3p_r(int a, bool, std::string) { return a; }; static void _0p_v() {}; static int _0p_r() { return 1; }; void test_simple_wrappers() { static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); } static void _handle(JS::HandleValue) {}; static void _handle_2(int, JS::HandleValue, bool) {}; - static void _cmpt_private(ScriptInterface::CmptPrivate*) {}; - static int _cmpt_private_2(ScriptInterface::CmptPrivate*, int a, bool) { return a; }; + static void _script_interface(const ScriptInterface&) {}; + static int _script_interface_2(const ScriptInterface&, int a, bool) { return a; }; static void _script_request(const ScriptRequest&) {}; static int _script_request_2(const ScriptRequest&, int a, bool) { return a; }; void test_special_wrappers() { static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); - static_assert(std::is_same_v), JSNative>); - static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); static_assert(std::is_same_v), JSNative>); } class test_method { public: void method_1() {}; int method_2(int, const int&) { return 4; }; void const_method_1() const {}; int const_method_2(int, const int&) const { return 4; }; }; void test_method_wrappers() { static_assert(std::is_same_v>), JSNative>); + &ScriptInterface::ObjectFromCBData>), JSNative>); static_assert(std::is_same_v>), JSNative>); + &ScriptInterface::ObjectFromCBData>), JSNative>); static_assert(std::is_same_v>), JSNative>); + &ScriptInterface::ObjectFromCBData>), JSNative>); static_assert(std::is_same_v>), JSNative>); + &ScriptInterface::ObjectFromCBData>), JSNative>); } void test_calling() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); ScriptFunction::Register<&TestFunctionWrapper::_1p_v>(script, "_1p_v"); { std::string input = "Test._1p_v(0);"; JS::RootedValue val(rq.cx); TS_ASSERT(script.Eval(input.c_str(), &val)); } ScriptFunction::Register<&TestFunctionWrapper::_3p_r>(script, "_3p_r"); { std::string input = "Test._3p_r(4, false, 'test');"; int ret = 0; TS_ASSERT(script.Eval(input.c_str(), ret)); TS_ASSERT_EQUALS(ret, 4); } - ScriptFunction::Register<&TestFunctionWrapper::_cmpt_private_2>(script, "_cmpt_private_2"); + ScriptFunction::Register<&TestFunctionWrapper::_script_interface_2>(script, "_cmpt_private_2"); { std::string input = "Test._cmpt_private_2(4);"; int ret = 0; TS_ASSERT(script.Eval(input.c_str(), ret)); TS_ASSERT_EQUALS(ret, 4); } } }; Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 25441) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 25442) @@ -1,1105 +1,1105 @@ /* Copyright (C) 2021 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. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS::Values are passed between the game and AI threads using Script::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread 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, 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. shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector commands; }; CAIWorker() : m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false), m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()), m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()), m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()), m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext()) { m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); ScriptRequest rq(m_ScriptInterface); #define REGISTER_FUNC_NAME(func, name) \ - ScriptFunction::Register<&CAIWorker::func, ScriptFunction::ObjectFromCBData>(rq, 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_Simulation(rq); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); } 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"); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ - void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) + 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); 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) { 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); } } // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptContext; 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 : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { 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()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 numAis; deserialize.NumberU32_Unbounded("num ais", numAis); if (numAis > 0) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { 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); } virtual void SetRNGSeed(u32 seed) { m_Worker.SetRNGSeed(seed); } virtual void TryLoadSharedComponent() { m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { 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); } virtual void StartComputation() { 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; } virtual void PushCommands() { 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/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 25441) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 25442) @@ -1,134 +1,135 @@ /* Copyright (C) 2021 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 "simulation2/system/ComponentTest.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } - static void Script_LoadComponentScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) + static void Script_LoadComponentScript(const ScriptInterface& scriptInterface, const VfsPath& pathname) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); + ScriptRequest rq(scriptInterface); + CComponentManager* componentManager = scriptInterface.ObjectFromCBData(rq); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } - static void Script_LoadHelperScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) + static void Script_LoadHelperScript(const ScriptInterface& scriptInterface, const VfsPath& pathname) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); + ScriptRequest rq(scriptInterface); + CComponentManager* componentManager = scriptInterface.ObjectFromCBData(rq); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } - static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) + static JS::Value Script_SerializationRoundTrip(const ScriptInterface& scriptInterface, JS::HandleValue value) { - ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); val = value; std::stringstream stream; CStdSerializer serializer(scriptInterface, stream); serializer.ScriptVal("", &val); CStdDeserializer deserializer(scriptInterface, stream); deserializer.ScriptVal("", &val); return val; } void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "SerializationRoundTrip"); load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "LoadComponentScript"); ScriptFunction::Register(rq, "LoadHelperScript"); ScriptFunction::Register(rq, "SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 25441) +++ ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 25442) @@ -1,332 +1,332 @@ /* Copyright (C) 2021 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 "scriptinterface/Object.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/Color.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "maths/Rect.h" #include "ps/CLogger.h" #include "ps/utf16string.h" #include "simulation2/helpers/CinemaPath.h" #include "simulation2/helpers/Grid.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #define FAIL(msg) STMT(LOGERROR(msg); return false) #define FAIL_VOID(msg) STMT(ScriptException::Raise(rq, msg); return) template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, IComponent* const& val) { if (val == NULL) { ret.setNull(); return; } // If this is a scripted component, just return the JS object directly JS::RootedValue instance(rq.cx, val->GetJSInstance()); if (!instance.isNull()) { ret.set(instance); return; } // Otherwise we need to construct a wrapper object // (TODO: cache wrapper objects?) JS::RootedObject obj(rq.cx); - if (!val->NewJSObject(*ScriptInterface::GetScriptInterfaceAndCBData(rq.cx)->pScriptInterface, &obj)) + if (!val->NewJSObject(rq.GetScriptInterface(), &obj)) { // Report as an error, since scripts really shouldn't try to use unscriptable interfaces LOGERROR("IComponent does not have a scriptable interface"); ret.setUndefined(); return; } JS_SetPrivate(obj, static_cast(val)); ret.setObject(*obj); } template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, CParamNode const& val) { val.ToJSVal(rq, true, ret); // Prevent modifications to the object, so that it's safe to share between // components and to reconstruct on deserialization if (ret.isObject()) Script::FreezeObject(rq, ret, true); } template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CParamNode* const& val) { if (val) ToJSVal(rq, ret, *val); else ret.setUndefined(); } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CColor& out) { if (!v.isObject()) FAIL("CColor has to be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue r(rq.cx); JS::RootedValue g(rq.cx); JS::RootedValue b(rq.cx); JS::RootedValue a(rq.cx); if (!JS_GetProperty(rq.cx, obj, "r", &r) || !FromJSVal(rq, r, out.r)) FAIL("Failed to get property CColor.r"); if (!JS_GetProperty(rq.cx, obj, "g", &g) || !FromJSVal(rq, g, out.g)) FAIL("Failed to get property CColor.g"); if (!JS_GetProperty(rq.cx, obj, "b", &b) || !FromJSVal(rq, b, out.b)) FAIL("Failed to get property CColor.b"); if (!JS_GetProperty(rq.cx, obj, "a", &a) || !FromJSVal(rq, a, out.a)) FAIL("Failed to get property CColor.a"); return true; } template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, CColor const& val) { Script::CreateObject( rq, ret, "r", val.r, "g", val.g, "b", val.b, "a", val.a); } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, fixed& out) { double ret; if (!JS::ToNumber(rq.cx, v, &ret)) return false; out = fixed::FromDouble(ret); // double can precisely represent the full range of fixed, so this is a non-lossy conversion return true; } template<> void Script::ToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue ret, const fixed& val) { ret.set(JS::NumberValue(val.ToDouble())); } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CFixedVector3D& out) { if (!v.isObject()) return false; // TODO: report type error JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue p(rq.cx); if (!JS_GetProperty(rq.cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(rq, p, out.X)) return false; if (!JS_GetProperty(rq.cx, obj, "y", &p)) return false; if (!FromJSVal(rq, p, out.Y)) return false; if (!JS_GetProperty(rq.cx, obj, "z", &p)) return false; if (!FromJSVal(rq, p, out.Z)) return false; return true; } template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CFixedVector3D& val) { JS::RootedObject global(rq.cx, rq.glob); JS::RootedValue valueVector3D(rq.cx); if (!ScriptInterface::GetGlobalProperty(rq, "Vector3D", &valueVector3D)) FAIL_VOID("Failed to get Vector3D constructor"); JS::RootedValueArray<3> args(rq.cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); args[2].setNumber(val.Z.ToDouble()); JS::RootedObject objVec(rq.cx); if (!JS::Construct(rq.cx, valueVector3D, args, &objVec)) FAIL_VOID("Failed to construct Vector3D object"); ret.setObject(*objVec); } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CFixedVector2D& out) { if (!v.isObject()) return false; // TODO: report type error JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue p(rq.cx); if (!JS_GetProperty(rq.cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(rq, p, out.X)) return false; if (!JS_GetProperty(rq.cx, obj, "y", &p)) return false; if (!FromJSVal(rq, p, out.Y)) return false; return true; } template<> void Script::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CFixedVector2D& val) { JS::RootedObject global(rq.cx, rq.glob); JS::RootedValue valueVector2D(rq.cx); if (!ScriptInterface::GetGlobalProperty(rq, "Vector2D", &valueVector2D)) FAIL_VOID("Failed to get Vector2D constructor"); JS::RootedValueArray<2> args(rq.cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); JS::RootedObject objVec(rq.cx); if (!JS::Construct(rq.cx, valueVector2D, args, &objVec)) FAIL_VOID("Failed to construct Vector2D object"); ret.setObject(*objVec); } template<> void Script::ToJSVal >(const ScriptRequest& rq, JS::MutableHandleValue ret, const Grid& val) { u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u8)); JS::RootedObject objArr(rq.cx, JS_NewUint8Array(rq.cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint8ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(rq.cx, JS::ObjectValue(*objArr)); Script::CreateObject( rq, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> void Script::ToJSVal >(const ScriptRequest& rq, JS::MutableHandleValue ret, const Grid& val) { u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u16)); JS::RootedObject objArr(rq.cx, JS_NewUint16Array(rq.cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint16ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(rq.cx, JS::ObjectValue(*objArr)); Script::CreateObject( rq, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, TNSpline& out) { if (!v.isObject()) FAIL("Argument must be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); bool isArray; if (!JS::IsArrayObject(rq.cx, obj, &isArray) || !isArray) FAIL("Argument must be an array"); u32 numberOfNodes = 0; if (!JS::GetArrayLength(rq.cx, obj, &numberOfNodes)) FAIL("Failed to get array length"); for (u32 i = 0; i < numberOfNodes; ++i) { JS::RootedValue node(rq.cx); if (!JS_GetElement(rq.cx, obj, i, &node)) FAIL("Failed to read array element"); fixed deltaTime; if (!FromJSProperty(rq, node, "deltaTime", deltaTime)) FAIL("Failed to read Spline.deltaTime property"); CFixedVector3D position; if (!FromJSProperty(rq, node, "position", position)) FAIL("Failed to read Spline.position property"); out.AddNode(position, CFixedVector3D(), deltaTime); } if (out.GetAllNodes().empty()) FAIL("Spline must contain at least one node"); return true; } template<> bool Script::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CCinemaPath& out) { if (!v.isObject()) FAIL("Argument must be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); CCinemaData pathData; TNSpline positionSpline, targetSpline; if (!FromJSProperty(rq, v, "name", pathData.m_Name)) FAIL("Failed to get CCinemaPath.name property"); if (!FromJSProperty(rq, v, "orientation", pathData.m_Orientation)) FAIL("Failed to get CCinemaPath.orientation property"); if (!FromJSProperty(rq, v, "positionNodes", positionSpline)) FAIL("Failed to get CCinemaPath.positionNodes property"); if (pathData.m_Orientation == L"target" && !FromJSProperty(rq, v, "targetNodes", targetSpline)) FAIL("Failed to get CCinemaPath.targetNodes property"); // Other properties are not necessary to be defined if (!FromJSProperty(rq, v, "timescale", pathData.m_Timescale)) pathData.m_Timescale = fixed::FromInt(1); if (!FromJSProperty(rq, v, "mode", pathData.m_Mode)) pathData.m_Mode = L"ease_inout"; if (!FromJSProperty(rq, v, "style", pathData.m_Style)) pathData.m_Style = L"default"; out = CCinemaPath(pathData, positionSpline, targetSpline); return true; } // define vectors JSVAL_VECTOR(CFixedVector2D) #undef FAIL #undef FAIL_VOID Index: ps/trunk/source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 25441) +++ ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 25442) @@ -1,495 +1,496 @@ /* Copyright (C) 2021 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 "StdDeserializer.h" #include "lib/byte_order.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptExtraHeaders.h" // For typed arrays and ArrayBuffer +#include "scriptinterface/ScriptInterface.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/serialization/SerializedScriptTypes.h" #include "simulation2/serialization/StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) : m_ScriptInterface(scriptInterface), m_Stream(stream) { - JS_AddExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); + JS_AddExtraGCRootsTracer(ScriptRequest(scriptInterface).cx, CStdDeserializer::Trace, this); // Insert a dummy object in front, as valid tags start at 1. m_ScriptBackrefs.emplace_back(nullptr); } CStdDeserializer::~CStdDeserializer() { - JS_RemoveExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); + JS_RemoveExtraGCRootsTracer(ScriptRequest(m_ScriptInterface).cx, CStdDeserializer::Trace, this); } void CStdDeserializer::Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void CStdDeserializer::TraceMember(JSTracer *trc) { for (JS::Heap& backref : m_ScriptBackrefs) JS::TraceEdge(trc, &backref, "StdDeserializer::m_ScriptBackrefs"); } void CStdDeserializer::Get(const char* name, u8* data, size_t len) { #if DEBUG_SERIALIZER_ANNOTATE std::string strName; char c = m_Stream.get(); ENSURE(c == '<'); while (1) { c = m_Stream.get(); if (c == '>') break; else strName += c; } ENSURE(strName == name); #else UNUSED2(name); #endif m_Stream.read((char*)data, (std::streamsize)len); if (!m_Stream.good()) { // hit eof before len, or other errors // NOTE: older libc++ versions incorrectly set eofbit on the last char; test gcount as a workaround // see https://llvm.org/bugs/show_bug.cgi?id=9335 if (m_Stream.bad() || m_Stream.fail() || (m_Stream.eof() && m_Stream.gcount() != (std::streamsize)len)) throw PSERROR_Deserialize_ReadFailed(); } } std::istream& CStdDeserializer::GetStream() { return m_Stream; } void CStdDeserializer::RequireBytesInStream(size_t numBytes) { // It would be nice to do: // if (numBytes > (size_t)m_Stream.rdbuf()->in_avail()) // throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); // but that doesn't work (at least on MSVC) since in_avail isn't // guaranteed to return the actual number of bytes available; see e.g. // http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/13009a88-933f-4be7-bf3d-150e425e66a6#70ea562d-8605-4742-8851-1bae431ce6ce // Instead we'll just verify that it's not an extremely large number: if (numBytes > 64*MiB) throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); } void CStdDeserializer::AddScriptBackref(JS::HandleObject obj) { m_ScriptBackrefs.push_back(JS::Heap(obj)); } void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret) { ENSURE(m_ScriptBackrefs.size() > tag); ret.set(m_ScriptBackrefs[tag]); } //////////////////////////////////////////////////////////////// JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject preexistingObject) { ScriptRequest rq(m_ScriptInterface); uint8_t type; NumberU8_Unbounded("type", type); switch (type) { case SCRIPT_TYPE_VOID: return JS::UndefinedValue(); case SCRIPT_TYPE_NULL: return JS::NullValue(); case SCRIPT_TYPE_ARRAY: case SCRIPT_TYPE_OBJECT: case SCRIPT_TYPE_OBJECT_PROTOTYPE: { JS::RootedObject obj(rq.cx); if (type == SCRIPT_TYPE_ARRAY) { u32 length; NumberU32_Unbounded("array length", length); obj.set(JS::NewArrayObject(rq.cx, length)); } else if (type == SCRIPT_TYPE_OBJECT) { obj.set(JS_NewPlainObject(rq.cx)); } else // SCRIPT_TYPE_OBJECT_PROTOTYPE { CStrW prototypeName; String("proto", prototypeName, 0, 256); // If an object was passed, no need to construct a new one. if (preexistingObject != nullptr) obj.set(preexistingObject); else { JS::RootedValue constructor(rq.cx); if (!ScriptInterface::GetGlobalProperty(rq, prototypeName.ToUTF8(), &constructor)) throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object"); JS::RootedObject newObj(rq.cx); if (!JS::Construct(rq.cx, constructor, JS::HandleValueArray::empty(), &newObj)) throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object"); obj.set(newObj); } JS::RootedObject prototype(rq.cx); JS_GetPrototype(rq.cx, obj, &prototype); SPrototypeSerialization info = GetPrototypeInfo(rq, prototype); if (preexistingObject != nullptr && prototypeName != wstring_from_utf8(info.name)) throw PSERROR_Deserialize_ScriptError("Deserializer failed: incorrect pre-existing object"); if (info.hasCustomDeserialize) { AddScriptBackref(obj); // If Serialize is null, we'll still call Deserialize but with undefined argument JS::RootedValue data(rq.cx); if (!info.hasNullSerialize) ScriptVal("data", &data); JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj)); ScriptFunction::CallVoid(rq, objVal, "Deserialize", data); return JS::ObjectValue(*obj); } else if (info.hasNullSerialize) { // If we serialized null, this means we're pretty much a default-constructed object. // Nothing to do. AddScriptBackref(obj); return JS::ObjectValue(*obj); } } if (!obj) throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object"); AddScriptBackref(obj); uint32_t numProps; NumberU32_Unbounded("num props", numProps); bool isLatin1; for (uint32_t i = 0; i < numProps; ++i) { Bool("isLatin1", isLatin1); if (isLatin1) { std::vector propname; ReadStringLatin1("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); utf16string prp(propname.begin(), propname.end());; // TODO: Should ask upstream about getting a variant of JS_SetProperty with a length param. if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)prp.data(), prp.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } else { utf16string propname; ReadStringUTF16("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)propname.data(), propname.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } } return JS::ObjectValue(*obj); } case SCRIPT_TYPE_STRING: { JS::RootedString str(rq.cx); ScriptString("string", &str); return JS::StringValue(str); } case SCRIPT_TYPE_INT: { int32_t value; NumberI32("value", value, JSVAL_INT_MIN, JSVAL_INT_MAX); return JS::NumberValue(value); } case SCRIPT_TYPE_DOUBLE: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue rval(rq.cx, JS::NumberValue(value)); if (rval.isNull()) throw PSERROR_Deserialize_ScriptError("JS_NewNumberValue failed"); return rval; } case SCRIPT_TYPE_BOOLEAN: { uint8_t value; NumberU8("value", value, 0, 1); return JS::BooleanValue(value ? true : false); } case SCRIPT_TYPE_BACKREF: { i32 tag; NumberI32("tag", tag, 0, JSVAL_INT_MAX); JS::RootedObject obj(rq.cx); GetScriptBackref(tag, &obj); if (!obj) throw PSERROR_Deserialize_ScriptError("Invalid backref tag"); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_NUMBER: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue val(rq.cx, JS::NumberValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Number, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_STRING: { JS::RootedString str(rq.cx); ScriptString("value", &str); if (!str) throw PSERROR_Deserialize_ScriptError(); JS::RootedValue val(rq.cx, JS::StringValue(str)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_String, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_BOOLEAN: { bool value; Bool("value", value); JS::RootedValue val(rq.cx, JS::BooleanValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Boolean, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_TYPED_ARRAY: { u8 arrayType; u32 byteOffset, length; NumberU8_Unbounded("array type", arrayType); NumberU32_Unbounded("byte offset", byteOffset); NumberU32_Unbounded("length", length); // To match the serializer order, we reserve the typed array's backref tag here JS::RootedObject arrayObj(rq.cx); AddScriptBackref(arrayObj); // Get buffer object JS::RootedValue bufferVal(rq.cx, ReadScriptVal("buffer", nullptr)); if (!bufferVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject bufferObj(rq.cx, &bufferVal.toObject()); if (!JS::IsArrayBufferObject(bufferObj)) throw PSERROR_Deserialize_ScriptError("js_IsArrayBuffer failed"); switch(arrayType) { case SCRIPT_TYPED_ARRAY_INT8: arrayObj = JS_NewInt8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8: arrayObj = JS_NewUint8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT16: arrayObj = JS_NewInt16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT16: arrayObj = JS_NewUint16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT32: arrayObj = JS_NewInt32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT32: arrayObj = JS_NewUint32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT32: arrayObj = JS_NewFloat32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT64: arrayObj = JS_NewFloat64ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8_CLAMPED: arrayObj = JS_NewUint8ClampedArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; default: throw PSERROR_Deserialize_ScriptError("Failed to deserialize unrecognized typed array view"); } if (!arrayObj) throw PSERROR_Deserialize_ScriptError("js_CreateTypedArrayWithBuffer failed"); return JS::ObjectValue(*arrayObj); } case SCRIPT_TYPE_ARRAY_BUFFER: { u32 length; NumberU32_Unbounded("buffer length", length); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: need to convert JS ArrayBuffer data from little-endian #endif void* contents = malloc(length); ENSURE(contents); RawBytes("buffer data", (u8*)contents, length); JS::RootedObject bufferObj(rq.cx, JS::NewArrayBufferWithContents(rq.cx, length, contents)); AddScriptBackref(bufferObj); return JS::ObjectValue(*bufferObj); } case SCRIPT_TYPE_OBJECT_MAP: { JS::RootedObject obj(rq.cx, JS::NewMapObject(rq.cx)); AddScriptBackref(obj); u32 mapSize; NumberU32_Unbounded("map size", mapSize); for (u32 i=0; i& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len); str.resize(len); Get(name, (u8*)str.data(), len); } void CStdDeserializer::ReadStringUTF16(const char* name, utf16string& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len*2); str.resize(len); Get(name, (u8*)str.data(), len*2); } void CStdDeserializer::ScriptString(const char* name, JS::MutableHandleString out) { #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: probably need to convert JS strings from little-endian #endif ScriptRequest rq(m_ScriptInterface); bool isLatin1; Bool("isLatin1", isLatin1); if (isLatin1) { std::vector str; ReadStringLatin1(name, str); out.set(JS_NewStringCopyN(rq.cx, (const char*)str.data(), str.size())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewStringCopyN failed"); } else { utf16string str; ReadStringUTF16(name, str); out.set(JS_NewUCStringCopyN(rq.cx, (const char16_t*)str.data(), str.length())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewUCStringCopyN failed"); } } void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out) { out.set(ReadScriptVal(name, nullptr)); } void CStdDeserializer::ScriptObjectAssign(const char* name, JS::HandleValue objVal) { ScriptRequest rq(m_ScriptInterface); if (!objVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject obj(rq.cx, &objVal.toObject()); ReadScriptVal(name, obj); } Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 25441) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 25442) @@ -1,1139 +1,1139 @@ /* Copyright (C) 2021 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" /** * Used for script-only message types. */ class CMessageScripted : 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, 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_Simulation(m_ScriptInterface); ScriptRequest rq(m_ScriptInterface); - constexpr ScriptFunction::ObjectGetter Getter = &ScriptFunction::ObjectFromCBData; + 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"); } // 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 (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it) { // TODO C++17: string_view if (strncmp((it->c_str()), "On", 2) != 0) continue; std::string name = (*it).substr(2); // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (strncmp(name.c_str(), "Global", 6) == 0) { isGlobal = true; name = name.substr(6); } std::map::const_iterator mit = m_MessageTypeIdsByName.find(name); if (mit == m_MessageTypeIdsByName.end()) { ScriptException::Raise(rq, "Registered component has unrecognized '%s' message handler method", it->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); } 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 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; }