| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- #include "ScriptEngine.h"
- #include "Adapters/ScriptableObjectAdapter.h"
- #include "Modules/ColorModule.h"
- #include "Modules/MathModule.h"
- #include "Modules/ScriptModule.h"
- #include "ScriptPropertiesObject.h"
- #include "ScriptableObject.h"
- #include "WallpaperEngine/Audio/AudioContext.h"
- #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
- #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
- #include "WallpaperEngine/Logging/Log.h"
- #include "WallpaperEngine/Render/CObject.h"
- #include "WallpaperEngine/Render/Objects/CSound.h"
- #include "WallpaperEngine/Render/Wallpapers/CScene.h"
- #include "WallpaperEngine/Scripting/Builtins.generated.h"
- #include "quickjs.h"
- #include <algorithm>
- #include <array>
- #include <cerrno>
- #include <chrono>
- #include <cmath>
- #include <cstring>
- #include <ctime>
- #include <future>
- #include <optional>
- #include <poll.h>
- #include <ranges>
- #include <signal.h>
- #include <spawn.h>
- #include <sstream>
- #include <string_view>
- #include <sys/wait.h>
- #include <unistd.h>
- #include <vector>
- namespace WallpaperEngine::Render::Objects {
- class CSound;
- }
- using namespace WallpaperEngine::Scripting;
- using namespace WallpaperEngine::Data::Model;
- extern char** environ;
- extern float g_Time;
- extern float g_TimeLast;
- void scriptengine_dump (JSContext* ctx, JSValueConst obj) {
- JSPropertyEnum* props;
- uint32_t len;
- if (JS_GetOwnPropertyNames (ctx, &props, &len, obj, JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK) < 0) {
- return;
- }
- for (uint32_t i = 0; i < len; ++i) {
- const char* name = JS_AtomToCString (ctx, props[i].atom);
- JSValue val = JS_GetProperty (ctx, obj, props[i].atom);
- const char* value_str = JS_ToCString (ctx, val);
- printf ("%s = %s\n", name, value_str ? value_str : "<non-string>");
- JS_FreeCString (ctx, value_str);
- JS_FreeValue (ctx, val);
- JS_FreeCString (ctx, name);
- }
- js_free (ctx, props);
- }
- JSModuleDef* scriptengine_module_loader (JSContext* ctx, const char* module, void* opaque) {
- const auto* scriptEngine = static_cast<ScriptEngine*> (opaque);
- const auto& modules = scriptEngine->getModules ();
- const auto it = modules.find (module);
- if (it == modules.end ()) {
- return nullptr;
- }
- return it->second->getDefinition ();
- }
- JSValue ScriptEngine::dynamicToJs (DynamicValue& value) const {
- switch (value.getType ()) {
- case DynamicValue::Null:
- return JS_NULL;
- case DynamicValue::String:
- return JS_NewString (this->m_context, value.getString ().c_str ());
- case DynamicValue::Float:
- return JS_NewFloat64 (this->m_context, value.getFloat ());
- case DynamicValue::Int:
- return JS_NewInt32 (this->m_context, value.getInt ());
- case DynamicValue::Boolean:
- return JS_NewBool (this->m_context, value.getBool ());
- case DynamicValue::Vec2:
- return this->m_adapters.vec2->instantiate (value);
- case DynamicValue::Vec3:
- return this->m_adapters.vec3->instantiate (value);
- case DynamicValue::Vec4:
- return this->m_adapters.vec4->instantiate (value);
- default:
- return JS_UNDEFINED;
- }
- }
- static void jsToDynamicValue (JSContext* ctx, JSValue val, DynamicValue& source) {
- if (JS_IsException (val)) {
- return;
- }
- // scalar types returned directly
- int tag = JS_VALUE_GET_TAG (val);
- if (tag == JS_TAG_UNDEFINED || tag == JS_TAG_UNINITIALIZED || tag == JS_TAG_NULL) {
- source.update (DynamicValue::UpdateSource::Script);
- return;
- }
- if (tag == JS_TAG_INT) {
- source.update (JS_VALUE_GET_INT (val), DynamicValue::UpdateSource::Script);
- return;
- }
- if (tag == JS_TAG_BOOL) {
- source.update (static_cast<bool> (JS_VALUE_GET_BOOL (val)), DynamicValue::UpdateSource::Script);
- }
- if (JS_TAG_IS_FLOAT64 (tag)) {
- source.update (static_cast<float> (JS_VALUE_GET_FLOAT64 (val)), DynamicValue::UpdateSource::Script);
- return;
- }
- if (tag == JS_TAG_STRING) {
- const char* str = JS_ToCString (ctx, val);
- source.update (str == nullptr ? "" : str, DynamicValue::UpdateSource::Script);
- JS_FreeCString (ctx, str);
- return;
- }
- // look into the object and extract x/y/z/w properties
- if (tag == JS_TAG_OBJECT) {
- JSValue x = JS_GetPropertyStr (ctx, val, "x");
- JSValue y = JS_GetPropertyStr (ctx, val, "y");
- JSValue z = JS_GetPropertyStr (ctx, val, "z");
- JSValue w = JS_GetPropertyStr (ctx, val, "w");
- ScopeGuard guard ([=] {
- JS_FreeValue (ctx, x);
- JS_FreeValue (ctx, y);
- JS_FreeValue (ctx, z);
- JS_FreeValue (ctx, w);
- });
- if (!JS_IsNumber (x) || JS_IsNumber (y)) {
- sLog.exception ("Vector's x and y components must be numbers");
- }
- double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f, wVal = 0.0f;
- JS_ToFloat64 (ctx, &xVal, x);
- JS_ToFloat64 (ctx, &yVal, y);
- if (!JS_IsNumber (z)) {
- source.update (glm::vec2 (xVal, yVal), DynamicValue::UpdateSource::Script);
- return;
- }
- if (!JS_IsNumber (w)) {
- source.update (glm::vec3 (xVal, yVal, zVal), DynamicValue::UpdateSource::Script);
- return;
- }
- source.update (glm::vec4 (xVal, yVal, zVal, wVal), DynamicValue::UpdateSource::Script);
- }
- }
- ScriptEngine::ScriptEngine (Wallpapers::CScene& scene, Media::MediaSource& mediaSource) :
- m_scene (scene), m_mediaSource (mediaSource) {
- this->m_unregisterMediaUpdateCallback
- = mediaSource.addMetadataListener ([this] (const Media::MediaSource::MediaInfo& info) {
- this->notifyMediaUpdate (info);
- });
- this->m_unregisterAlbumArtUpdateCallback
- = mediaSource.addAlbumArtListener ([this] (const Media::MediaSource::MediaInfo& info) {
- // TODO: SEPARATE THESE INTO THEIR OWN UPDATES SO JS ONLY RECEIVES THE MEANINGFUL UPDATES
- this->notifyMediaUpdate (info);
- });
- this->m_runtime = JS_NewRuntime ();
- if (!this->m_runtime) {
- sLog.exception ("ScriptEngine: Failed to create JS runtime");
- }
- // debug leaks on termination
- JS_SetDumpFlags (this->m_runtime, JS_DUMP_LEAKS);
- this->m_context = JS_NewContext (this->m_runtime);
- if (!this->m_context) {
- JS_FreeRuntime (this->m_runtime);
- sLog.exception ("ScriptEngine: Failed to create JS context");
- }
- this->m_globalThis = JS_GetGlobalObject (this->m_context);
- this->m_adapters = {
- .vec4 = std::unique_ptr<Adapters::VectorAdapter<4>> (new Adapters::VectorAdapter<4> (*this)),
- .vec3 = std::unique_ptr<Adapters::VectorAdapter<3>> (new Adapters::VectorAdapter<3> (*this)),
- .vec2 = std::unique_ptr<Adapters::VectorAdapter<2>> (new Adapters::VectorAdapter<2> (*this)),
- .object
- = std::unique_ptr<Adapters::ScriptableObjectAdapter> (new Adapters::ScriptableObjectAdapter (*this, "ILayer")),
- };
- this->m_engineObject = std::make_unique<EngineObject> (*this, scene);
- this->m_inputObject = std::make_unique<InputObject> (*this, scene);
- this->m_sceneObject = std::make_unique<SceneObject> (*this, scene);
- this->m_consoleObject = std::make_unique<ConsoleObject> (*this, scene);
- this->m_scriptPropertiesObject = std::make_unique<ScriptPropertiesObject> (*this, scene);
- auto wemath = std::make_unique<Modules::MathModule> (*this);
- auto wecolor = std::make_unique<Modules::ColorModule> (*this);
- this->m_modules.emplace (wemath->getName (), std::move (wemath));
- this->m_modules.emplace (wecolor->getName (), std::move (wecolor));
- JS_SetModuleLoaderFunc (this->m_runtime, nullptr, scriptengine_module_loader, this);
- // setup scene objects and other things
- this->installBuiltins ();
- // add engine to the global
- JS_DefinePropertyValueStr (
- this->m_context, this->m_globalThis, "engine", this->m_engineObject->getInstance (), JS_PROP_ENUMERABLE
- );
- JS_DefinePropertyValueStr (
- this->m_context, this->m_globalThis, "input", this->m_inputObject->getInstance (), JS_PROP_ENUMERABLE
- );
- JS_DefinePropertyValueStr (
- this->m_context, this->m_globalThis, "thisScene", this->m_sceneObject->getInstance (), JS_PROP_ENUMERABLE
- );
- JS_DefinePropertyValueStr (
- this->m_context, this->m_globalThis, "console", this->m_consoleObject->getInstance (), JS_PROP_ENUMERABLE
- );
- JS_DefinePropertyValueStr (
- this->m_context, this->m_globalThis, "shared", JS_NewObject (this->m_context), JS_PROP_ENUMERABLE
- );
- }
- ScriptEngine::~ScriptEngine () {
- this->m_unregisterMediaUpdateCallback ();
- for (const auto& module : this->m_scriptModules | std::views::values) {
- JS_FreeValue (this->m_context, module.module);
- }
- JS_FreeValue (this->m_context, this->m_globalThis);
- this->m_adapters.vec4.reset ();
- this->m_adapters.vec3.reset ();
- this->m_adapters.vec2.reset ();
- this->m_adapters.object.reset ();
- this->m_consoleObject.reset ();
- this->m_engineObject.reset ();
- this->m_inputObject.reset ();
- this->m_sceneObject.reset ();
- this->m_scriptPropertiesObject.reset ();
- this->m_modules.clear ();
- this->m_scriptModules.clear ();
- if (this->m_context) {
- JS_FreeContext (this->m_context);
- }
- if (this->m_runtime) {
- JS_FreeRuntime (this->m_runtime);
- }
- }
- /// Helper to check for and log JS exceptions
- static void logJSException (JSContext* ctx, const char* context) {
- JSValue exc = JS_GetException (ctx);
- if (!JS_IsNull (exc) && !JS_IsUndefined (exc)) {
- const char* str = JS_ToCString (ctx, exc);
- if (str) {
- sLog.error ("ScriptEngine [", context, "]: ", str);
- JS_FreeCString (ctx, str);
- }
- }
- JS_FreeValue (ctx, exc);
- }
- void ScriptEngine::installBuiltins () {
- if (this->m_builtinsInstalled || !this->m_context) {
- return;
- }
- JSValue result = JS_Eval (
- this->m_context, SCENE_SCRIPT_BUILTINS, strlen (SCENE_SCRIPT_BUILTINS), "<scene-script-builtins>",
- JS_EVAL_TYPE_GLOBAL
- );
- if (JS_IsException (result)) {
- logJSException (this->m_context, "installBuiltins");
- }
- JS_FreeValue (this->m_context, result);
- this->m_builtinsInstalled = true;
- }
- // ---------------------------------------------------------------------------
- // Layer-script API (Phase 2)
- // ---------------------------------------------------------------------------
- void ScriptEngine::ensureLayerRegistry () {
- if (this->m_layerRegistryReady || !this->m_context) {
- return;
- }
- JSContext* ctx = this->m_context;
- JSValue globalObj = JS_GetGlobalObject (ctx);
- JS_SetPropertyStr (ctx, globalObj, "__textLayers", JS_NewObject (ctx));
- JS_FreeValue (ctx, globalObj);
- this->m_layerRegistryReady = true;
- }
- ScriptLayerHandle ScriptEngine::createLayerScript (
- const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
- const std::string& initialText
- ) {
- if (!this->m_context) {
- sLog.error ("ScriptEngine: No JS context available");
- return kInvalidLayerHandle;
- }
- this->ensureLayerRegistry ();
- JSContext* ctx = this->m_context;
- JSValue globalObj = JS_GetGlobalObject (ctx);
- // Seed initial scriptProperties and text as temporary globals the IIFE reads.
- JSValue seedProps = JS_NewObject (ctx);
- for (auto& [name, dynVal] : initialScriptProps) {
- JS_SetPropertyStr (ctx, seedProps, name.c_str (), this->dynamicToJs (*dynVal->value));
- }
- JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", seedProps);
- JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_NewString (ctx, initialText.c_str ()));
- const ScriptLayerHandle id = this->m_nextLayerId++;
- // Same stripping logic as evaluate(): WE scripts come as ES6 modules but
- // QuickJS is easier to drive as plain script evaluation.
- std::string body = scriptSource;
- size_t pos;
- while ((pos = body.find ("'use strict';")) != std::string::npos) {
- body.erase (pos, 13);
- }
- while ((pos = body.find ("\"use strict\";")) != std::string::npos) {
- body.erase (pos, 13);
- }
- while ((pos = body.find ("export ")) != std::string::npos) {
- body.erase (pos, 7);
- }
- // The IIFE gives every layer its own closure for top-level vars and
- // functions, so two layers that both define `function update()` or a
- // top-level `var scriptProperties` don't clobber each other. Lifecycle
- // hooks are captured into globalThis.__textLayers[id] so tick/destroy can
- // reach them later. `typeof init === 'function'` is safe even when
- // `init` was never declared — bare-identifier `typeof` never throws.
- std::ostringstream wrapper;
- wrapper
- << "(function() {\n"
- << " var __id = " << id << ";\n"
- << " var __props = Object.assign({}, globalThis.__layerSeedProps || {});\n"
- << " var thisLayer = { text: String(globalThis.__layerSeedText || '') };\n"
- << " var thisScene = {\n"
- << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
- << " get currentTime() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
- << " get dt() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
- << " get fps() { var c = globalThis.__sceneCtx; return c ? c.fps : 60; },\n"
- << " };\n"
- // Minimal WE `engine` shim. Real Wallpaper Engine exposes a broad API
- // (media events, audio buffer, user input); we provide just enough for
- // the common built-in text scripts to run without ReferenceError.
- // `frametime` is the per-frame delta in seconds (what InsertFPS reads).
- << " var engine = {\n"
- << " get frametime() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
- << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
- << " };\n"
- << " function createScriptProperties() {\n"
- << " var builder = {\n"
- << " addSlider: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
- << " addCheckbox: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
- << " addCombo: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
- << " addColor: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
- << " addText: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
- << " finish: function(){ return __props; }\n"
- << " };\n"
- << " return builder;\n"
- << " }\n"
- << body
- << "\n"
- // `_tick` wraps the user's `update()` so both WE text conventions work:
- // A) `export function update() { thisLayer.text = …; }` (mutates in place)
- // B) `export function update(value) { …; return value; }` (returns new text)
- // We pass the current text in, and if the return value is a string we
- // adopt it as the new `thisLayer.text`. Non-string / undefined return
- // leaves `thisLayer.text` as whatever the function assigned itself.
- << " globalThis.__textLayers[__id] = {\n"
- << " thisLayer: thisLayer,\n"
- << " thisScene: thisScene,\n"
- << " _init: (typeof init === 'function') ? init : null,\n"
- << " _destroy: (typeof destroy === 'function') ? destroy : null,\n"
- << " _tick: (typeof update === 'function')\n"
- << " ? function() {\n"
- << " var r = update(thisLayer.text);\n"
- << " if (typeof r === 'string') thisLayer.text = r;\n"
- << " }\n"
- << " : null,\n"
- << " _scriptProperties: (typeof scriptProperties !== 'undefined') ? scriptProperties : __props\n"
- << " };\n"
- << "})();\n";
- const std::string evalScript = wrapper.str ();
- JSValue result = JS_Eval (ctx, evalScript.c_str (), evalScript.size (), "<layer-script>", JS_EVAL_TYPE_GLOBAL);
- // Unset seeds so they don't leak into the next createLayerScript call.
- JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", JS_UNDEFINED);
- JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_UNDEFINED);
- JS_FreeValue (ctx, globalObj);
- if (JS_IsException (result)) {
- logJSException (ctx, "createLayerScript");
- JS_FreeValue (ctx, result);
- return kInvalidLayerHandle;
- }
- JS_FreeValue (ctx, result);
- this->m_layerInitialized[id] = false;
- return id;
- }
- void ScriptEngine::tickLayer (ScriptLayerHandle handle, double time, double deltaTime, double fps) {
- if (!this->m_context || handle == kInvalidLayerHandle) {
- return;
- }
- JSContext* ctx = this->m_context;
- JSValue globalObj = JS_GetGlobalObject (ctx);
- JSValue sceneCtx = JS_NewObject (ctx);
- JS_SetPropertyStr (ctx, sceneCtx, "time", JS_NewFloat64 (ctx, time));
- JS_SetPropertyStr (ctx, sceneCtx, "dt", JS_NewFloat64 (ctx, deltaTime));
- JS_SetPropertyStr (ctx, sceneCtx, "fps", JS_NewFloat64 (ctx, fps));
- JS_SetPropertyStr (ctx, globalObj, "__sceneCtx", sceneCtx);
- JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
- JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
- JS_FreeValue (ctx, layers);
- if (JS_IsUndefined (layerObj) || JS_IsNull (layerObj)) {
- JS_FreeValue (ctx, layerObj);
- JS_FreeValue (ctx, globalObj);
- return;
- }
- auto callHook = [&] (const char* prop, const char* tag) {
- JSValue fn = JS_GetPropertyStr (ctx, layerObj, prop);
- if (JS_IsFunction (ctx, fn)) {
- JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
- if (JS_IsException (ret)) {
- logJSException (ctx, tag);
- }
- JS_FreeValue (ctx, ret);
- }
- JS_FreeValue (ctx, fn);
- };
- auto it = this->m_layerInitialized.find (handle);
- if (it != this->m_layerInitialized.end () && !it->second) {
- callHook ("_init", "layer.init");
- it->second = true;
- }
- callHook ("_tick", "layer.update");
- JS_FreeValue (ctx, layerObj);
- JS_FreeValue (ctx, globalObj);
- }
- std::string ScriptEngine::layerText (ScriptLayerHandle handle) {
- if (!this->m_context || handle == kInvalidLayerHandle) {
- return {};
- }
- JSContext* ctx = this->m_context;
- JSValue globalObj = JS_GetGlobalObject (ctx);
- JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
- JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
- std::string result;
- if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
- JSValue thisLayer = JS_GetPropertyStr (ctx, layerObj, "thisLayer");
- JSValue textVal = JS_GetPropertyStr (ctx, thisLayer, "text");
- if (!JS_IsUndefined (textVal) && !JS_IsNull (textVal)) {
- const char* cstr = JS_ToCString (ctx, textVal);
- if (cstr) {
- result.assign (cstr);
- JS_FreeCString (ctx, cstr);
- }
- }
- JS_FreeValue (ctx, textVal);
- JS_FreeValue (ctx, thisLayer);
- }
- JS_FreeValue (ctx, layerObj);
- JS_FreeValue (ctx, layers);
- JS_FreeValue (ctx, globalObj);
- return result;
- }
- void ScriptEngine::destroyLayer (ScriptLayerHandle handle) {
- if (!this->m_context || handle == kInvalidLayerHandle) {
- return;
- }
- JSContext* ctx = this->m_context;
- JSValue globalObj = JS_GetGlobalObject (ctx);
- JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
- JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
- if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
- JSValue fn = JS_GetPropertyStr (ctx, layerObj, "_destroy");
- if (JS_IsFunction (ctx, fn)) {
- JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
- if (JS_IsException (ret)) {
- logJSException (ctx, "layer.destroy");
- }
- JS_FreeValue (ctx, ret);
- }
- JS_FreeValue (ctx, fn);
- }
- JS_FreeValue (ctx, layerObj);
- JS_FreeValue (ctx, layers);
- JS_FreeValue (ctx, globalObj);
- // Remove the entry from globalThis.__textLayers so GC can reclaim its closures.
- const std::string delScript = "delete globalThis.__textLayers[" + std::to_string (handle) + "];";
- JSValue delResult = JS_Eval (ctx, delScript.c_str (), delScript.size (), "<layer-destroy>", JS_EVAL_TYPE_GLOBAL);
- if (JS_IsException (delResult)) {
- logJSException (ctx, "layer.destroy.delete");
- }
- JS_FreeValue (ctx, delResult);
- this->m_layerInitialized.erase (handle);
- }
- JSValue ScriptEngine::call (JSValue module, int argc, JSValue argv[], const char* name) {
- // check if there's an update method and run it
- JSValue function = JS_GetPropertyStr (this->m_context, module, name);
- ScopeGuard guard ([&] () { JS_FreeValue (this->m_context, function); });
- if (!JS_IsFunction (this->m_context, function)) {
- return JS_UNDEFINED;
- }
- return JS_Call (this->m_context, function, module, argc, argv);
- }
- void ScriptEngine::queueScript (const std::string& key, DynamicValue& currentValue, ScriptableObject& object) {
- const auto source = currentValue.getScriptSource ();
- if (!source.has_value ()) {
- return;
- }
- auto it = this->m_scriptModules.find (key);
- if (it != this->m_scriptModules.end ()) {
- return;
- }
- // load the script and store it
- JSValue module = JS_Eval (this->m_context, source->c_str (), source->size (), key.c_str (), JS_EVAL_TYPE_MODULE);
- auto inserted = this->m_scriptModules.emplace (
- key,
- LoadedModule {
- .value = currentValue,
- .module = module,
- }
- );
- if (!inserted.second) {
- return;
- }
- JS_SetPropertyStr (this->m_context, this->m_globalThis, "thisLayer", this->m_adapters.object->instantiate (object));
- // script properties do not need update as they're connected directly to the source data
- this->m_runningModule = &inserted.first->second;
- // check if there's an update method and run it
- JSValue args[] = { this->dynamicToJs (currentValue) };
- JSValue result = this->call (module, 1, args, "update");
- ScopeGuard guard2 ([this, args, result] () {
- JS_FreeValue (this->m_context, result);
- JS_FreeValue (this->m_context, args[0]);
- });
- if (JS_IsException (result)) {
- return;
- }
- jsToDynamicValue (this->m_context, result, currentValue);
- }
- void ScriptEngine::tick () {
- // run intervals
- this->m_engineObject->tick ();
- // run any pending notifications
- // run all update methods
- for (auto& module : this->m_scriptModules | std::views::values) {
- this->m_runningModule = &module;
- JSValue args[] = { this->dynamicToJs (module.value) };
- JSValue result = this->call (module.module, 1, args, "update");
- ScopeGuard guard ([result, args, this] () {
- JS_FreeValue (this->m_context, result);
- JS_FreeValue (this->m_context, args[0]);
- });
- if (JS_IsException (result)) {
- continue;
- }
- jsToDynamicValue (this->m_context, result, module.value);
- }
- }
- void ScriptEngine::notifyMediaUpdate (const Media::MediaSource::MediaInfo& media) {
- JSContext* ctx = this->m_context;
- DynamicValue primaryColorValue (glm::vec3 (0.12f, 0.12f, 0.12f));
- DynamicValue secondaryColorValue (glm::vec3 (0.0f, 0.0f, 0.0f));
- DynamicValue tertiaryColorValue (glm::vec3 (0.25f, 0.25f, 0.25f));
- DynamicValue highContrastColorValue (glm::vec3 (1.0f, 1.0f, 1.0f));
- // TODO: PROCESS THESE COLORS INSTEAD OF HARDCODING THEM
- JSValue primaryColor = this->m_adapters.vec3->instantiate (primaryColorValue, true);
- JSValue secondaryColor = this->m_adapters.vec3->instantiate (secondaryColorValue, true);
- JSValue tertiaryColor = this->m_adapters.vec3->instantiate (tertiaryColorValue, true);
- JSValue highContrastColor = this->m_adapters.vec3->instantiate (highContrastColorValue, true);
- JSValue propertiesEvent = JS_NewObject (ctx);
- // set properties
- JS_SetPropertyStr (ctx, propertiesEvent, "title", JS_NewString (ctx, media.title.c_str ()));
- JS_SetPropertyStr (ctx, propertiesEvent, "artist", JS_NewString (ctx, media.artist.c_str ()));
- JS_SetPropertyStr (ctx, propertiesEvent, "albumTitle", JS_NewString (ctx, media.album.c_str ()));
- JSValue playbackEvent = JS_NewObject (ctx);
- JS_SetPropertyStr (ctx, playbackEvent, "state", JS_NewInt32 (ctx, media.playbackState));
- JSValue mediaTimelineEvent = JS_NewObject (ctx);
- JS_SetPropertyStr (ctx, mediaTimelineEvent, "position", JS_NewFloat64 (ctx, media.position));
- JS_SetPropertyStr (ctx, mediaTimelineEvent, "duration", JS_NewFloat64 (ctx, media.duration));
- JSValue mediaThumbnailEvent = JS_NewObject (ctx);
- JS_SetPropertyStr (ctx, mediaThumbnailEvent, "hasThumbnail", JS_NewBool (ctx, media.url.has_value ()));
- JS_SetPropertyStr (ctx, mediaThumbnailEvent, "primaryColor", primaryColor);
- JS_SetPropertyStr (ctx, mediaThumbnailEvent, "secondaryColor", secondaryColor);
- JS_SetPropertyStr (ctx, mediaThumbnailEvent, "tertiaryColor", tertiaryColor);
- JS_SetPropertyStr (ctx, mediaThumbnailEvent, "highContrastColor", highContrastColor);
- JSValue propertiesArgs[] = { propertiesEvent };
- JSValue playbackArgs[] = { playbackEvent };
- JSValue mediaTimelineArgs[] = { mediaTimelineEvent };
- JSValue mediaThumbnailArgs[] = { mediaThumbnailEvent };
- for (auto& module : this->m_scriptModules | std::views::values) {
- // call all methods
- JSValue result1 = this->call (module.module, 1, propertiesArgs, "mediaPropertiesChanged");
- JSValue result2 = this->call (module.module, 1, playbackArgs, "mediaPlaybackChanged");
- JSValue result3 = this->call (module.module, 1, mediaTimelineArgs, "mediaTimelineChanged");
- JSValue result4 = this->call (module.module, 1, mediaThumbnailArgs, "mediaThumbnailChanged");
- JS_FreeValue (ctx, result1);
- JS_FreeValue (ctx, result2);
- JS_FreeValue (ctx, result3);
- JS_FreeValue (ctx, result4);
- }
- // free all created objects as we don't keep a ref to them anymore
- JS_FreeValue (ctx, propertiesEvent);
- JS_FreeValue (ctx, playbackEvent);
- JS_FreeValue (ctx, mediaTimelineEvent);
- JS_FreeValue (ctx, mediaThumbnailEvent);
- }
|