mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Add a class to register stateful callbacks to JS
Use the class in autostart as an example.
This commit is contained in:
parent
0afb5f3d06
commit
44605c1297
3 changed files with 157 additions and 33 deletions
|
|
@ -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<VfsPath> 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<ScriptFunction::StatefulCallback<GetTemplate>> getTemplateCallback;
|
||||
if (args.Has("autostart-nonvisual"))
|
||||
ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate");
|
||||
getTemplateCallback.emplace(rq, "GetTemplate", GetTemplate{});
|
||||
else
|
||||
{
|
||||
JSI_GUIManager::RegisterScriptFunctions(rq);
|
||||
|
|
|
|||
|
|
@ -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 <fmt/format.h>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
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<callable, thisGetter>, args_info<decltype(callable)>::nb_args, flags);
|
||||
}
|
||||
|
||||
template<typename Callable>
|
||||
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<ClassInfo>(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<Callable>(&args.callee(),
|
||||
StatefullCallbackPrivateSlot::callback);
|
||||
}
|
||||
|
||||
static void finalize(JS::GCContext*, JSObject* obj)
|
||||
{
|
||||
delete JS::GetMaybePtrFromReservedSlot<ClassInfo>(obj,
|
||||
StatefullCallbackPrivateSlot::classInfo);
|
||||
}
|
||||
|
||||
Callable callback;
|
||||
JS::RootedObject functionObject;
|
||||
};
|
||||
|
||||
template<typename Callable>
|
||||
static StatefulCallback<Callable> Register(const ScriptRequest& rq, std::string name,
|
||||
const unsigned flags, Callable callable)
|
||||
{
|
||||
return StatefulCallback{rq, std::move(name), flags, std::move(callable)};
|
||||
}
|
||||
|
||||
template<typename Callable>
|
||||
static StatefulCallback<Callable> Register(const ScriptRequest& rq, std::string name,
|
||||
Callable callable)
|
||||
{
|
||||
return StatefulCallback{rq, std::move(name), std::move(callable)};
|
||||
}
|
||||
};
|
||||
|
||||
#endif // INCLUDED_FUNCTIONWRAPPER
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue