diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 9971f083f0..2ffe39709c 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -736,34 +736,6 @@ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) return mapElement.GetText(); } -// TODO: this essentially duplicates the CGUI logic to load directory or scripts. -// NB: this won't make sure to not double-load scripts, unlike the GUI. -void AutostartLoadScript(const ScriptInterface& scriptInterface, const VfsPath& path) -{ - if (path.IsDirectory()) - { - VfsPaths pathnames; - vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); - for (const VfsPath& file : pathnames) - scriptInterface.LoadGlobalScriptFile(file); - } - else - scriptInterface.LoadGlobalScriptFile(path); -} - -// TODO: this essentially duplicates the CGUI function -CParamNode GetTemplate(const std::string& templateName) -{ - // This is very cheap to create so let's just do it every time. - CTemplateLoader templateLoader; - - const CParamNode& templateRoot = templateLoader.GetTemplateFileData(templateName).GetOnlyChild(); - if (!templateRoot.IsOk()) - LOGERROR("Invalid template found for '%s'", templateName.c_str()); - - return templateRoot; -} - /** * Autostart arguments are parsed in javascript for convenience and moddability. * This C++ part only handles the following arguments: @@ -785,13 +757,50 @@ bool Autostart(const CmdLineArgs& args) // We use the javascript gameSettings to handle options, but that requires running JS. // Since we don't want to use the full Gui manager, we load an entrypoint script // that can run the priviledged "LoadScript" function, and then call the appropriate function. - ScriptFunction::Register<&AutostartLoadScript>(rq, "LoadScript"); + + // TODO: this essentially duplicates the CGUI logic to load directory or scripts. + std::unordered_set templateCache; + const auto autostartLoadScript = [&templateCache](const ScriptInterface& scriptInterface, + const VfsPath& path) + { + if (!std::get<1>(templateCache.insert(path))) + return; + + if (path.IsDirectory()) + { + VfsPaths pathnames; + vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); + for (const VfsPath& file : pathnames) + scriptInterface.LoadGlobalScriptFile(file); + } + else + scriptInterface.LoadGlobalScriptFile(path); + }; + + const auto loadScriptCallback = ScriptFunction::Register(rq, "LoadScript", autostartLoadScript); // Load the entire folder to allow mods to extend the entrypoint without copying the whole file. - AutostartLoadScript(scriptInterface, VfsPath(L"autostart/")); + autostartLoadScript(scriptInterface, VfsPath(L"autostart/")); // Provide some required functions to the script. + + struct GetTemplate + { + CTemplateLoader templateLoader; + + CParamNode operator()(const std::string& templateName){ + // TODO: this essentially duplicates the CGUI function + const CParamNode& templateRoot{ + templateLoader.GetTemplateFileData(templateName).GetOnlyChild()}; + if (!templateRoot.IsOk()) + LOGERROR("Invalid template found for '%s'", templateName.c_str()); + + return templateRoot; + } + }; + + std::optional> getTemplateCallback; if (args.Has("autostart-nonvisual")) - ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); + getTemplateCallback.emplace(rq, "GetTemplate", GetTemplate{}); else { JSI_GUIManager::RegisterScriptFunctions(rq); diff --git a/source/scriptinterface/FunctionWrapper.h b/source/scriptinterface/FunctionWrapper.h index 32ad5edb6b..da7c1d245e 100644 --- a/source/scriptinterface/FunctionWrapper.h +++ b/source/scriptinterface/FunctionWrapper.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -23,9 +23,11 @@ #include "ScriptRequest.h" #include +#include #include #include #include +#include #include class ScriptInterface; @@ -257,6 +259,13 @@ private: return !ScriptException::CatchPending(rq) && success; } + struct StatefullCallbackPrivateSlot + { + static constexpr size_t callback{0}; + static constexpr size_t classInfo{1}; + static constexpr uint32_t count{2}; + }; + /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// public: @@ -453,6 +462,92 @@ public: { JS_DefineFunction(cx, scope, name, &ToJSNative, args_info::nb_args, flags); } + + template + class StatefulCallback + { + class ClassInfo + { + public: + ClassInfo(std::string className) : + name{std::move(className)} + {} + + JSClassOps classOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + /*.finalize = */finalize, /*.call = */&ToJSNative<&Callable::operator(), getter>, + nullptr, nullptr}; + std::string name; + JSClass jsClass{name.c_str(), + JSCLASS_HAS_RESERVED_SLOTS(StatefullCallbackPrivateSlot::count) | + JSCLASS_BACKGROUND_FINALIZE, + &classOps}; + }; + public: + explicit StatefulCallback(const ScriptRequest& rq, std::string name, Callable callable) : + StatefulCallback{rq, std::move(name), + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT, std::move(callable)} + {} + + explicit StatefulCallback(const ScriptRequest& rq, std::string name, const unsigned flags, + Callable callable) : + callback{std::move(callable)}, + functionObject{rq.cx} + { + auto classInfo = std::make_unique(std::move(name)); + functionObject.set(JS_NewObject(rq.cx, &classInfo->jsClass)); + JS::RootedValue functionValue{rq.cx, JS::ObjectValue(*functionObject)}; + if (!JS_DefineProperty(rq.cx, rq.nativeScope, classInfo->name.c_str(), functionValue, + flags)) + { + throw std::runtime_error{fmt::format( + "Failed defining function {:?} on the native scope.", classInfo->name)}; + } + JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::classInfo, + JS::PrivateValue(classInfo.release())); + JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::callback, + JS::PrivateValue(&callback)); + } + StatefulCallback(const StatefulCallback&) = delete; + StatefulCallback& operator=(const StatefulCallback&) = delete; + StatefulCallback(StatefulCallback&&) = delete; + StatefulCallback& operator=(StatefulCallback&&) = delete; + + ~StatefulCallback() + { + JS::SetReservedSlot(functionObject, StatefullCallbackPrivateSlot::callback, + JS::UndefinedValue()); + } + + private: + static Callable* getter(const ScriptRequest&, JS::CallArgs& args) + { + return JS::GetMaybePtrFromReservedSlot(&args.callee(), + StatefullCallbackPrivateSlot::callback); + } + + static void finalize(JS::GCContext*, JSObject* obj) + { + delete JS::GetMaybePtrFromReservedSlot(obj, + StatefullCallbackPrivateSlot::classInfo); + } + + Callable callback; + JS::RootedObject functionObject; + }; + + template + static StatefulCallback Register(const ScriptRequest& rq, std::string name, + const unsigned flags, Callable callable) + { + return StatefulCallback{rq, std::move(name), flags, std::move(callable)}; + } + + template + static StatefulCallback Register(const ScriptRequest& rq, std::string name, + Callable callable) + { + return StatefulCallback{rq, std::move(name), std::move(callable)}; + } }; #endif // INCLUDED_FUNCTIONWRAPPER diff --git a/source/scriptinterface/tests/test_FunctionWrapper.h b/source/scriptinterface/tests/test_FunctionWrapper.h index 99f2d7e2a3..659098fb6d 100644 --- a/source/scriptinterface/tests/test_FunctionWrapper.h +++ b/source/scriptinterface/tests/test_FunctionWrapper.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -110,4 +110,24 @@ public: TS_ASSERT_EQUALS(ret, 4); } } + + void test_statefull() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + JS::RootedValue nativeScope{rq.cx, JS::ObjectValue(*rq.nativeScope)}; + + constexpr const char* name{"callback"}; + { + bool called{false}; + auto _ = ScriptFunction::Register(rq, name, [&](){ + called = true; + }); + TS_ASSERT(!called); + TS_ASSERT(ScriptFunction::CallVoid(rq, nativeScope, name)); + TS_ASSERT(called); + } + + TS_ASSERT(!ScriptFunction::CallVoid(rq, nativeScope, name)); + } };