ScriptEngine.cpp 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576
  1. #include "ScriptEngine.h"
  2. #include "WallpaperEngine/Media/ThumbnailPalette.h"
  3. #include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
  4. #include "Adapters/ScriptableObjectAdapter.h"
  5. #include "ScriptPropertiesObject.h"
  6. #include "ScriptableObject.h"
  7. #include "WallpaperEngine/Audio/AudioContext.h"
  8. #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
  9. #include "WallpaperEngine/Data/Model/Property.h"
  10. #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
  11. #include "WallpaperEngine/Desktop/UserShortcut.h"
  12. #include "WallpaperEngine/Logging/Log.h"
  13. #include "WallpaperEngine/Render/CObject.h"
  14. #include "WallpaperEngine/Render/Objects/CSound.h"
  15. #include "WallpaperEngine/Render/Wallpapers/CScene.h"
  16. #include "WallpaperEngine/Scripting/Builtins.generated.h"
  17. #include "quickjs.h"
  18. #include <algorithm>
  19. #include <array>
  20. #include <cctype>
  21. #include <cerrno>
  22. #include <chrono>
  23. #include <cmath>
  24. #include <cstdio>
  25. #include <cstring>
  26. #include <ctime>
  27. #include <filesystem>
  28. #include <future>
  29. #include <optional>
  30. #include <poll.h>
  31. #include <ranges>
  32. #include <signal.h>
  33. #include <spawn.h>
  34. #include <sstream>
  35. #include <string_view>
  36. #include <sys/wait.h>
  37. #include <unistd.h>
  38. #include <vector>
  39. namespace WallpaperEngine::Render::Objects {
  40. class CSound;
  41. }
  42. using namespace WallpaperEngine::Scripting;
  43. using namespace WallpaperEngine::Data::Model;
  44. extern char** environ;
  45. extern float g_Time;
  46. extern float g_TimeLast;
  47. void scriptengine_dump (JSContext* ctx, JSValueConst obj) {
  48. JSPropertyEnum* props;
  49. uint32_t len;
  50. if (JS_GetOwnPropertyNames (ctx, &props, &len, obj, JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK) < 0) {
  51. return;
  52. }
  53. for (uint32_t i = 0; i < len; ++i) {
  54. const char* name = JS_AtomToCString (ctx, props[i].atom);
  55. JSValue val = JS_GetProperty (ctx, obj, props[i].atom);
  56. const char* value_str = JS_ToCString (ctx, val);
  57. printf ("%s = %s\n", name, value_str ? value_str : "<non-string>");
  58. JS_FreeCString (ctx, value_str);
  59. JS_FreeValue (ctx, val);
  60. JS_FreeCString (ctx, name);
  61. }
  62. js_free (ctx, props);
  63. }
  64. std::optional<std::string> ScriptEngine::readScriptAsset (const std::string& path) const {
  65. try {
  66. return this->m_scene.getAssetLocator ().readString (path);
  67. } catch (std::filesystem::filesystem_error& e) {
  68. sLog.error ("ScriptEngine: cannot read ", path, ": ", e.what ());
  69. return std::nullopt;
  70. }
  71. }
  72. // same lookup as scenescript64: the import name lowercased under scripts/jsmodules/, ".js" appended
  73. // unless the name already has it ('WEMath' -> scripts/jsmodules/wemath.js)
  74. JSModuleDef* scriptengine_module_loader (JSContext* ctx, const char* module, void* opaque) {
  75. const auto* scriptEngine = static_cast<ScriptEngine*> (opaque);
  76. std::string name (module);
  77. std::ranges::transform (name, name.begin (), [] (unsigned char c) { return std::tolower (c); });
  78. std::string path = "scripts/jsmodules/" + name;
  79. if (name.find (".js") == std::string::npos) {
  80. path += ".js";
  81. }
  82. const auto source = scriptEngine->readScriptAsset (path);
  83. if (!source.has_value ()) {
  84. JS_ThrowReferenceError (ctx, "could not load module '%s'", module);
  85. return nullptr;
  86. }
  87. JSValue compiled
  88. = JS_Eval (ctx, source->c_str (), source->size (), module, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
  89. if (JS_IsException (compiled)) {
  90. return nullptr;
  91. }
  92. auto* definition = static_cast<JSModuleDef*> (JS_VALUE_GET_PTR (compiled));
  93. JS_FreeValue (ctx, compiled);
  94. return definition;
  95. }
  96. JSValue ScriptEngine::dynamicToJs (DynamicValue& value) const {
  97. switch (value.getType ()) {
  98. case DynamicValue::Null:
  99. return JS_NULL;
  100. case DynamicValue::String:
  101. return JS_NewString (this->m_context, value.getString ().c_str ());
  102. case DynamicValue::Float:
  103. return JS_NewFloat64 (this->m_context, value.getFloat ());
  104. case DynamicValue::Int:
  105. return JS_NewInt32 (this->m_context, value.getInt ());
  106. case DynamicValue::Boolean:
  107. return JS_NewBool (this->m_context, value.getBool ());
  108. case DynamicValue::Vec2:
  109. return this->m_adapters.vec2->instantiate (value, true);
  110. case DynamicValue::Vec3:
  111. return this->m_adapters.vec3->instantiate (value, true);
  112. case DynamicValue::Vec4:
  113. return this->m_adapters.vec4->instantiate (value, true);
  114. default:
  115. return JS_UNDEFINED;
  116. }
  117. }
  118. bool ScriptEngine::hasScript (const DynamicValue& value) const {
  119. return std::ranges::any_of (this->m_scriptModules, [&value] (const auto& entry) {
  120. return &entry.second.value == &value;
  121. });
  122. }
  123. JSValue ScriptEngine::userPropertyToJs (Property& property) const {
  124. // same shape the real engine hands scripts (jsclasses/baseclasses.js), never the command itself
  125. if (property.is<PropertyUserShortcut> ()) {
  126. const auto shortcut = Desktop::UserShortcut::parse (property.getString ());
  127. JSValue result = JS_NewObject (this->m_context);
  128. JS_SetPropertyStr (this->m_context, result, "isbound", JS_NewBool (this->m_context, shortcut.has_value ()));
  129. return result;
  130. }
  131. if (property.is<PropertyColor> () && property.getType () == DynamicValue::Vec4) {
  132. DynamicValue rgb (glm::vec3 (property.getVec4 ()));
  133. return this->m_adapters.vec3->instantiate (rgb, true);
  134. }
  135. return this->dynamicToJs (property);
  136. }
  137. // real WE spreads a number returned for a vector property over every component and the property stays a
  138. // vector, so the next update() still gets a Vec
  139. static bool updateVectorFromNumber (DynamicValue& source, float number) {
  140. switch (source.getType ()) {
  141. case DynamicValue::Vec4:
  142. source.update (glm::vec4 (number), DynamicValue::UpdateSource::Script);
  143. return true;
  144. case DynamicValue::Vec3:
  145. source.update (glm::vec3 (number), DynamicValue::UpdateSource::Script);
  146. return true;
  147. case DynamicValue::Vec2:
  148. source.update (glm::vec2 (number), DynamicValue::UpdateSource::Script);
  149. return true;
  150. default:
  151. return false;
  152. }
  153. }
  154. static bool jsToDynamicValue (JSContext* ctx, JSValue val, DynamicValue& source) {
  155. if (JS_IsException (val)) {
  156. return false;
  157. }
  158. int tag = JS_VALUE_GET_TAG (val);
  159. // update()'s contract is "return the new value"; falling off the end of a function (or an
  160. // early "if (cond) return x;" with no else) yields undefined and is meant as "nothing to
  161. // change this frame", not "reset this property to zero" - DynamicValue::update(source) with
  162. // no value does the latter (it's meant for genuinely-null JSON properties at parse time, see
  163. // DynamicValueParser), and calling it here would zero out (e.g. scale -> 0, i.e. invisible)
  164. // any property whose script doesn't explicitly return on every path.
  165. if (tag == JS_TAG_UNDEFINED || tag == JS_TAG_UNINITIALIZED || tag == JS_TAG_NULL) {
  166. return false;
  167. }
  168. if (tag == JS_TAG_INT) {
  169. if (!updateVectorFromNumber (source, static_cast<float> (JS_VALUE_GET_INT (val)))) {
  170. source.update (JS_VALUE_GET_INT (val), DynamicValue::UpdateSource::Script);
  171. }
  172. return true;
  173. }
  174. if (tag == JS_TAG_BOOL) {
  175. source.update (static_cast<bool> (JS_VALUE_GET_BOOL (val)), DynamicValue::UpdateSource::Script);
  176. return true;
  177. }
  178. if (JS_TAG_IS_FLOAT64 (tag)) {
  179. const auto number = static_cast<float> (JS_VALUE_GET_FLOAT64 (val));
  180. if (!updateVectorFromNumber (source, number)) {
  181. source.update (number, DynamicValue::UpdateSource::Script);
  182. }
  183. return true;
  184. }
  185. if (tag == JS_TAG_STRING) {
  186. const char* str = JS_ToCString (ctx, val);
  187. source.update (std::string (str == nullptr ? "" : str), DynamicValue::UpdateSource::Script);
  188. JS_FreeCString (ctx, str);
  189. return true;
  190. }
  191. if (tag == JS_TAG_OBJECT) {
  192. JSValue x = JS_GetPropertyStr (ctx, val, "x");
  193. JSValue y = JS_GetPropertyStr (ctx, val, "y");
  194. JSValue z = JS_GetPropertyStr (ctx, val, "z");
  195. JSValue w = JS_GetPropertyStr (ctx, val, "w");
  196. ScopeGuard guard ([=] {
  197. JS_FreeValue (ctx, x);
  198. JS_FreeValue (ctx, y);
  199. JS_FreeValue (ctx, z);
  200. JS_FreeValue (ctx, w);
  201. });
  202. // runs every frame, a bad value from one script must not take the whole wallpaper down
  203. if (!JS_IsNumber (x) || !JS_IsNumber (y)) {
  204. static bool reported = false;
  205. if (!reported) {
  206. reported = true;
  207. sLog.error ("Script returned a vector without numeric x and y components, keeping the previous value");
  208. }
  209. return false;
  210. }
  211. double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f, wVal = 0.0f;
  212. JS_ToFloat64 (ctx, &xVal, x);
  213. JS_ToFloat64 (ctx, &yVal, y);
  214. if (!JS_IsNumber (z)) {
  215. source.update (glm::vec2 (xVal, yVal), DynamicValue::UpdateSource::Script);
  216. return true;
  217. }
  218. JS_ToFloat64 (ctx, &zVal, z);
  219. if (!JS_IsNumber (w)) {
  220. source.update (glm::vec3 (xVal, yVal, zVal), DynamicValue::UpdateSource::Script);
  221. return true;
  222. }
  223. JS_ToFloat64 (ctx, &wVal, w);
  224. source.update (glm::vec4 (xVal, yVal, zVal, wVal), DynamicValue::UpdateSource::Script);
  225. return true;
  226. }
  227. return false;
  228. }
  229. void ScriptEngine::assignJsValue (JSValue val, DynamicValue& target) const {
  230. jsToDynamicValue (this->m_context, val, target);
  231. }
  232. // Objects keep "angles" in radians, scripts get degrees: scenescript64 2.8.42 converts every property whose
  233. // descriptor has flag 4 on the way in (sub_1816208E0, * 57.29578) and out (sub_181620E10, * 0.017453292), and
  234. // wallpaper64 only sets that flag on the object "angles" property (sub_1401E0530)
  235. static bool isDegreeProperty (std::string_view name) { return name == "angles"; }
  236. static DynamicValue scaledAngles (const DynamicValue& value, float factor) {
  237. switch (value.getType ()) {
  238. case DynamicValue::Float:
  239. return DynamicValue (value.getFloat () * factor);
  240. case DynamicValue::Vec2:
  241. return DynamicValue (value.getVec2 () * factor);
  242. case DynamicValue::Vec3:
  243. return DynamicValue (value.getVec3 () * factor);
  244. case DynamicValue::Vec4:
  245. return DynamicValue (value.getVec4 () * factor);
  246. default:
  247. return value;
  248. }
  249. }
  250. JSValue ScriptEngine::propertyToJs (DynamicValue& value, std::string_view name) const {
  251. if (!isDegreeProperty (name)) {
  252. return this->dynamicToJs (value);
  253. }
  254. DynamicValue degrees = scaledAngles (value, 57.29578f);
  255. return this->dynamicToJs (degrees);
  256. }
  257. void ScriptEngine::assignPropertyJsValue (JSValue val, DynamicValue& target, std::string_view name) const {
  258. // scenescript64 (sub_181620E10, string descriptors) stores ToString of whatever the script assigns, only
  259. // undefined/null are ignored: 3577513994 sets a text layer to getDate()'s number
  260. if (target.getType () == DynamicValue::String && !JS_IsString (val) && !JS_IsUndefined (val) && !JS_IsNull (val)
  261. && !JS_IsException (val) && JS_VALUE_GET_TAG (val) != JS_TAG_UNINITIALIZED) {
  262. const char* str = JS_ToCString (this->m_context, val);
  263. if (str != nullptr) {
  264. target.update (std::string (str), DynamicValue::UpdateSource::Script);
  265. JS_FreeCString (this->m_context, str);
  266. }
  267. return;
  268. }
  269. if (!isDegreeProperty (name)) {
  270. jsToDynamicValue (this->m_context, val, target);
  271. return;
  272. }
  273. DynamicValue degrees = scaledAngles (target, 57.29578f);
  274. if (!jsToDynamicValue (this->m_context, val, degrees)) {
  275. return;
  276. }
  277. const DynamicValue radians = scaledAngles (degrees, 0.017453292f);
  278. switch (radians.getType ()) {
  279. case DynamicValue::Float:
  280. target.update (radians.getFloat (), DynamicValue::UpdateSource::Script);
  281. break;
  282. case DynamicValue::Vec2:
  283. target.update (radians.getVec2 (), DynamicValue::UpdateSource::Script);
  284. break;
  285. case DynamicValue::Vec3:
  286. target.update (radians.getVec3 (), DynamicValue::UpdateSource::Script);
  287. break;
  288. case DynamicValue::Vec4:
  289. target.update (radians.getVec4 (), DynamicValue::UpdateSource::Script);
  290. break;
  291. default:
  292. target.update (radians, DynamicValue::UpdateSource::Script);
  293. break;
  294. }
  295. }
  296. ScriptEngine::ScriptEngine (Wallpapers::CScene& scene, Media::MediaSource& mediaSource) :
  297. m_scene (scene), m_mediaSource (mediaSource) {
  298. this->m_unregisterMediaUpdateCallback
  299. = mediaSource.addMetadataListener ([this] (const Media::MediaSource::MediaInfo& info) {
  300. this->notifyMediaUpdate (info);
  301. });
  302. this->m_unregisterAlbumArtUpdateCallback
  303. = mediaSource.addAlbumArtListener ([this] (const Media::MediaSource::MediaInfo& info) {
  304. // TODO: SEPARATE THESE INTO THEIR OWN UPDATES SO JS ONLY RECEIVES THE MEANINGFUL UPDATES
  305. this->notifyMediaUpdate (info);
  306. });
  307. this->m_runtime = JS_NewRuntime ();
  308. if (!this->m_runtime) {
  309. sLog.exception ("ScriptEngine: Failed to create JS runtime");
  310. }
  311. // debug leaks on termination
  312. JS_SetDumpFlags (this->m_runtime, JS_DUMP_LEAKS);
  313. this->m_context = JS_NewContext (this->m_runtime);
  314. if (!this->m_context) {
  315. JS_FreeRuntime (this->m_runtime);
  316. sLog.exception ("ScriptEngine: Failed to create JS context");
  317. }
  318. this->m_globalThis = JS_GetGlobalObject (this->m_context);
  319. // WE's own Vec2/Vec3/Vec4/Mat3/Mat4 classes, the natives below only create instances of them
  320. if (const auto baseClasses = this->readScriptAsset ("scripts/jsclasses/baseclasses.js"); baseClasses.has_value ()) {
  321. JSValue result = JS_Eval (
  322. this->m_context, baseClasses->c_str (), baseClasses->size (), "baseclasses.js", JS_EVAL_TYPE_GLOBAL
  323. );
  324. if (JS_IsException (result)) {
  325. logJSException (this->m_context, "baseclasses.js", baseClasses);
  326. }
  327. JS_FreeValue (this->m_context, result);
  328. }
  329. this->m_adapters = {
  330. .vec4 = std::unique_ptr<Adapters::VectorAdapter<4>> (new Adapters::VectorAdapter<4> (*this)),
  331. .vec3 = std::unique_ptr<Adapters::VectorAdapter<3>> (new Adapters::VectorAdapter<3> (*this)),
  332. .vec2 = std::unique_ptr<Adapters::VectorAdapter<2>> (new Adapters::VectorAdapter<2> (*this)),
  333. .object
  334. = std::unique_ptr<Adapters::ScriptableObjectAdapter> (new Adapters::ScriptableObjectAdapter (*this, "ILayer")),
  335. };
  336. this->m_engineObject = std::make_unique<EngineObject> (*this, scene);
  337. this->m_inputObject = std::make_unique<InputObject> (*this, scene);
  338. this->m_sceneObject = std::make_unique<SceneObject> (*this, scene);
  339. this->m_consoleObject = std::make_unique<ConsoleObject> (*this, scene);
  340. this->m_scriptPropertiesObject = std::make_unique<ScriptPropertiesObject> (*this, scene);
  341. JS_SetModuleLoaderFunc (this->m_runtime, nullptr, scriptengine_module_loader, this);
  342. this->installBuiltins ();
  343. JS_DefinePropertyValueStr (
  344. this->m_context, this->m_globalThis, "engine", this->m_engineObject->getInstance (), JS_PROP_C_W_E
  345. );
  346. JS_DefinePropertyValueStr (
  347. this->m_context, this->m_globalThis, "input", this->m_inputObject->getInstance (), JS_PROP_C_W_E
  348. );
  349. JS_DefinePropertyValueStr (
  350. this->m_context, this->m_globalThis, "thisScene", this->m_sceneObject->getInstance (), JS_PROP_C_W_E
  351. );
  352. JS_DefinePropertyValueStr (
  353. this->m_context, this->m_globalThis, "console", this->m_consoleObject->getInstance (), JS_PROP_C_W_E
  354. );
  355. JS_DefinePropertyValueStr (
  356. this->m_context, this->m_globalThis, "shared", JS_NewObject (this->m_context), JS_PROP_C_W_E
  357. );
  358. }
  359. ScriptEngine::~ScriptEngine () {
  360. this->m_unregisterMediaUpdateCallback ();
  361. this->m_unregisterAlbumArtUpdateCallback ();
  362. for (const auto& module : this->m_scriptModules | std::views::values) {
  363. JS_FreeValue (this->m_context, module.module);
  364. JS_FreeValue (this->m_context, module.thisObject);
  365. }
  366. for (const auto& entry : this->m_videoEndedCallbacks) {
  367. JS_FreeValue (this->m_context, entry.callback);
  368. }
  369. if (this->m_adapters.object) {
  370. this->m_adapters.object->clear ();
  371. }
  372. JS_FreeValue (this->m_context, this->m_globalThis);
  373. this->m_consoleObject.reset ();
  374. this->m_engineObject.reset ();
  375. this->m_inputObject.reset ();
  376. this->m_sceneObject.reset ();
  377. this->m_scriptPropertiesObject.reset ();
  378. this->m_scriptModules.clear ();
  379. if (this->m_context) {
  380. JS_FreeContext (this->m_context);
  381. }
  382. if (this->m_runtime) {
  383. JS_FreeRuntime (this->m_runtime);
  384. }
  385. // Freeing the runtime above runs a GC pass that finalizes every still-live layer object, whose
  386. // class belongs to the object adapter - so the adapters must outlive JS_FreeRuntime().
  387. this->m_adapters.vec4.reset ();
  388. this->m_adapters.vec3.reset ();
  389. this->m_adapters.vec2.reset ();
  390. this->m_adapters.object.reset ();
  391. }
  392. namespace {
  393. // the line the first stack frame inside `context` points at, with a caret under the column
  394. std::string sourceExcerpt (const std::string& stack, const char* context, const std::string& source) {
  395. const std::string marker = std::string (context) + ":";
  396. const auto at = stack.find (marker);
  397. if (at == std::string::npos) {
  398. return {};
  399. }
  400. int line = 0;
  401. int column = 0;
  402. if (std::sscanf (stack.c_str () + at + marker.size (), "%d:%d", &line, &column) < 1 || line < 1) {
  403. return {};
  404. }
  405. size_t begin = 0;
  406. for (int i = 1; i < line; i++) {
  407. begin = source.find ('\n', begin);
  408. if (begin == std::string::npos) {
  409. return {};
  410. }
  411. begin++;
  412. }
  413. const auto lineEnd = source.find ('\n', begin);
  414. std::string text = source.substr (begin, lineEnd == std::string::npos ? std::string::npos : lineEnd - begin);
  415. std::ranges::replace (text, '\t', ' ');
  416. std::string result = std::to_string (line) + ": " + text;
  417. if (column > 0) {
  418. result += "\n" + std::string (std::to_string (line).size () + 2 + column - 1, ' ') + "^";
  419. }
  420. return result;
  421. }
  422. } // namespace
  423. void WallpaperEngine::Scripting::logJSException (
  424. JSContext* ctx, const char* context, const std::optional<std::string>& source
  425. ) {
  426. // scripts that fail every tick would otherwise print the same error every frame
  427. static std::map<std::string, std::string> lastReported;
  428. JSValue exc = JS_GetException (ctx);
  429. ScopeGuard freeException ([=] { JS_FreeValue (ctx, exc); });
  430. if (JS_IsNull (exc) || JS_IsUndefined (exc) || JS_IsUninitialized (exc)) {
  431. sLog.error ("ScriptEngine [", context, "]: a native call failed without setting an exception");
  432. return;
  433. }
  434. std::string message;
  435. std::string stack;
  436. if (const char* str = JS_ToCString (ctx, exc); str != nullptr) {
  437. message = str;
  438. JS_FreeCString (ctx, str);
  439. }
  440. JSValue stackValue = JS_GetPropertyStr (ctx, exc, "stack");
  441. if (!JS_IsUndefined (stackValue)) {
  442. if (const char* str = JS_ToCString (ctx, stackValue); str != nullptr) {
  443. stack = str;
  444. JS_FreeCString (ctx, str);
  445. }
  446. }
  447. JS_FreeValue (ctx, stackValue);
  448. auto& last = lastReported[context];
  449. if (last == message + stack) {
  450. return;
  451. }
  452. last = message + stack;
  453. sLog.error ("ScriptEngine [", context, "]: ", message);
  454. if (!stack.empty ()) {
  455. sLog.error ("ScriptEngine [", context, "] stack: ", stack);
  456. }
  457. if (source.has_value ()) {
  458. if (const auto excerpt = sourceExcerpt (stack, context, *source); !excerpt.empty ()) {
  459. sLog.error ("ScriptEngine [", context, "] source:\n", excerpt);
  460. }
  461. }
  462. }
  463. void ScriptEngine::installBuiltins () {
  464. if (this->m_builtinsInstalled || !this->m_context) {
  465. return;
  466. }
  467. JSValue result = JS_Eval (
  468. this->m_context, SCENE_SCRIPT_BUILTINS, strlen (SCENE_SCRIPT_BUILTINS), "<scene-script-builtins>",
  469. JS_EVAL_TYPE_GLOBAL
  470. );
  471. if (JS_IsException (result)) {
  472. logJSException (this->m_context, "installBuiltins");
  473. }
  474. JS_FreeValue (this->m_context, result);
  475. this->m_builtinsInstalled = true;
  476. }
  477. // Layer-script API (Phase 2)
  478. void ScriptEngine::ensureLayerRegistry () {
  479. if (this->m_layerRegistryReady || !this->m_context) {
  480. return;
  481. }
  482. JSContext* ctx = this->m_context;
  483. JSValue globalObj = JS_GetGlobalObject (ctx);
  484. JS_SetPropertyStr (ctx, globalObj, "__textLayers", JS_NewObject (ctx));
  485. JS_FreeValue (ctx, globalObj);
  486. this->m_layerRegistryReady = true;
  487. }
  488. ScriptLayerHandle ScriptEngine::createLayerScript (
  489. const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
  490. const std::string& initialText
  491. ) {
  492. if (!this->m_context) {
  493. sLog.error ("ScriptEngine: No JS context available");
  494. return kInvalidLayerHandle;
  495. }
  496. this->ensureLayerRegistry ();
  497. JSContext* ctx = this->m_context;
  498. JSValue globalObj = JS_GetGlobalObject (ctx);
  499. // Seed initial scriptProperties and text as temporary globals the IIFE reads.
  500. JSValue seedProps = JS_NewObject (ctx);
  501. for (auto& [name, dynVal] : initialScriptProps) {
  502. JS_SetPropertyStr (ctx, seedProps, name.c_str (), this->dynamicToJs (*dynVal->value));
  503. }
  504. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", seedProps);
  505. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_NewString (ctx, initialText.c_str ()));
  506. const ScriptLayerHandle id = this->m_nextLayerId++;
  507. // Same stripping logic as evaluate(): WE scripts come as ES6 modules but
  508. // QuickJS is easier to drive as plain script evaluation.
  509. std::string body = scriptSource;
  510. size_t pos;
  511. while ((pos = body.find ("'use strict';")) != std::string::npos) {
  512. body.erase (pos, 13);
  513. }
  514. while ((pos = body.find ("\"use strict\";")) != std::string::npos) {
  515. body.erase (pos, 13);
  516. }
  517. while ((pos = body.find ("export ")) != std::string::npos) {
  518. body.erase (pos, 7);
  519. }
  520. // The IIFE gives every layer its own closure so two layers both defining `function update()`
  521. // or a top-level `var scriptProperties` don't clobber each other. Lifecycle hooks are
  522. // captured into globalThis.__textLayers[id] so tick/destroy can reach them later.
  523. // `typeof init === 'function'` is safe even when `init` was never declared.
  524. std::ostringstream wrapper;
  525. wrapper
  526. << "(function() {\n"
  527. << " var __id = " << id << ";\n"
  528. << " var __props = Object.assign({}, globalThis.__layerSeedProps || {});\n"
  529. << " var thisLayer = { text: String(globalThis.__layerSeedText || '') };\n"
  530. << " var thisScene = {\n"
  531. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  532. << " get currentTime() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  533. << " get dt() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  534. << " get fps() { var c = globalThis.__sceneCtx; return c ? c.fps : 60; },\n"
  535. << " };\n"
  536. // Minimal WE `engine` shim - just enough for built-in text scripts to run without
  537. // ReferenceError. `frametime` is the per-frame delta in seconds (what InsertFPS reads).
  538. << " var engine = {\n"
  539. << " get frametime() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  540. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  541. << " };\n"
  542. << " function createScriptProperties() {\n"
  543. << " var builder = {\n"
  544. << " addSlider: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  545. << " addCheckbox: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  546. << " addCombo: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  547. << " addColor: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  548. << " addText: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  549. << " finish: function(){ return __props; }\n"
  550. << " };\n"
  551. << " return builder;\n"
  552. << " }\n"
  553. << body
  554. << "\n"
  555. // `_tick` wraps the user's `update()` so both WE text conventions work: mutating
  556. // `thisLayer.text` in place, or returning the new text as a string. Non-string/undefined
  557. // return leaves `thisLayer.text` as whatever the function assigned itself.
  558. << " globalThis.__textLayers[__id] = {\n"
  559. << " thisLayer: thisLayer,\n"
  560. << " thisScene: thisScene,\n"
  561. << " _init: (typeof init === 'function') ? init : null,\n"
  562. << " _destroy: (typeof destroy === 'function') ? destroy : null,\n"
  563. << " _tick: (typeof update === 'function')\n"
  564. << " ? function() {\n"
  565. << " var r = update(thisLayer.text);\n"
  566. << " if (typeof r === 'string') thisLayer.text = r;\n"
  567. << " }\n"
  568. << " : null,\n"
  569. << " _scriptProperties: (typeof scriptProperties !== 'undefined') ? scriptProperties : __props\n"
  570. << " };\n"
  571. << "})();\n";
  572. const std::string evalScript = wrapper.str ();
  573. JSValue result = JS_Eval (ctx, evalScript.c_str (), evalScript.size (), "<layer-script>", JS_EVAL_TYPE_GLOBAL);
  574. // Unset seeds so they don't leak into the next createLayerScript call.
  575. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", JS_UNDEFINED);
  576. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_UNDEFINED);
  577. JS_FreeValue (ctx, globalObj);
  578. if (JS_IsException (result)) {
  579. logJSException (ctx, "createLayerScript");
  580. JS_FreeValue (ctx, result);
  581. return kInvalidLayerHandle;
  582. }
  583. JS_FreeValue (ctx, result);
  584. this->m_layerInitialized[id] = false;
  585. return id;
  586. }
  587. void ScriptEngine::tickLayer (ScriptLayerHandle handle, double time, double deltaTime, double fps) {
  588. if (!this->m_context || handle == kInvalidLayerHandle) {
  589. return;
  590. }
  591. JSContext* ctx = this->m_context;
  592. JSValue globalObj = JS_GetGlobalObject (ctx);
  593. JSValue sceneCtx = JS_NewObject (ctx);
  594. JS_SetPropertyStr (ctx, sceneCtx, "time", JS_NewFloat64 (ctx, time));
  595. JS_SetPropertyStr (ctx, sceneCtx, "dt", JS_NewFloat64 (ctx, deltaTime));
  596. JS_SetPropertyStr (ctx, sceneCtx, "fps", JS_NewFloat64 (ctx, fps));
  597. JS_SetPropertyStr (ctx, globalObj, "__sceneCtx", sceneCtx);
  598. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  599. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  600. JS_FreeValue (ctx, layers);
  601. if (JS_IsUndefined (layerObj) || JS_IsNull (layerObj)) {
  602. JS_FreeValue (ctx, layerObj);
  603. JS_FreeValue (ctx, globalObj);
  604. return;
  605. }
  606. auto callHook = [&] (const char* prop, const char* tag) {
  607. JSValue fn = JS_GetPropertyStr (ctx, layerObj, prop);
  608. if (JS_IsFunction (ctx, fn)) {
  609. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  610. if (JS_IsException (ret)) {
  611. logJSException (ctx, tag);
  612. }
  613. JS_FreeValue (ctx, ret);
  614. }
  615. JS_FreeValue (ctx, fn);
  616. };
  617. auto it = this->m_layerInitialized.find (handle);
  618. if (it != this->m_layerInitialized.end () && !it->second) {
  619. callHook ("_init", "layer.init");
  620. it->second = true;
  621. }
  622. callHook ("_tick", "layer.update");
  623. JS_FreeValue (ctx, layerObj);
  624. JS_FreeValue (ctx, globalObj);
  625. }
  626. std::string ScriptEngine::layerText (ScriptLayerHandle handle) {
  627. if (!this->m_context || handle == kInvalidLayerHandle) {
  628. return {};
  629. }
  630. JSContext* ctx = this->m_context;
  631. JSValue globalObj = JS_GetGlobalObject (ctx);
  632. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  633. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  634. std::string result;
  635. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  636. JSValue thisLayer = JS_GetPropertyStr (ctx, layerObj, "thisLayer");
  637. JSValue textVal = JS_GetPropertyStr (ctx, thisLayer, "text");
  638. if (!JS_IsUndefined (textVal) && !JS_IsNull (textVal)) {
  639. const char* cstr = JS_ToCString (ctx, textVal);
  640. if (cstr) {
  641. result.assign (cstr);
  642. JS_FreeCString (ctx, cstr);
  643. }
  644. }
  645. JS_FreeValue (ctx, textVal);
  646. JS_FreeValue (ctx, thisLayer);
  647. }
  648. JS_FreeValue (ctx, layerObj);
  649. JS_FreeValue (ctx, layers);
  650. JS_FreeValue (ctx, globalObj);
  651. return result;
  652. }
  653. void ScriptEngine::destroyLayer (ScriptLayerHandle handle) {
  654. if (!this->m_context || handle == kInvalidLayerHandle) {
  655. return;
  656. }
  657. JSContext* ctx = this->m_context;
  658. JSValue globalObj = JS_GetGlobalObject (ctx);
  659. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  660. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  661. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  662. JSValue fn = JS_GetPropertyStr (ctx, layerObj, "_destroy");
  663. if (JS_IsFunction (ctx, fn)) {
  664. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  665. if (JS_IsException (ret)) {
  666. logJSException (ctx, "layer.destroy");
  667. }
  668. JS_FreeValue (ctx, ret);
  669. }
  670. JS_FreeValue (ctx, fn);
  671. }
  672. JS_FreeValue (ctx, layerObj);
  673. JS_FreeValue (ctx, layers);
  674. JS_FreeValue (ctx, globalObj);
  675. // Remove the entry from globalThis.__textLayers so GC can reclaim its closures.
  676. const std::string delScript = "delete globalThis.__textLayers[" + std::to_string (handle) + "];";
  677. JSValue delResult = JS_Eval (ctx, delScript.c_str (), delScript.size (), "<layer-destroy>", JS_EVAL_TYPE_GLOBAL);
  678. if (JS_IsException (delResult)) {
  679. logJSException (ctx, "layer.destroy.delete");
  680. }
  681. JS_FreeValue (ctx, delResult);
  682. this->m_layerInitialized.erase (handle);
  683. }
  684. JSValue ScriptEngine::call (JSValue module, int argc, JSValue argv[], const char* name) {
  685. JSValue function = JS_GetPropertyStr (this->m_context, module, name);
  686. ScopeGuard guard ([&] () { JS_FreeValue (this->m_context, function); });
  687. if (!JS_IsFunction (this->m_context, function)) {
  688. return JS_UNDEFINED;
  689. }
  690. return JS_Call (this->m_context, function, module, argc, argv);
  691. }
  692. void ScriptEngine::retireScript (const std::string& key) { this->m_retiredScriptKeys.push_back (key); }
  693. void ScriptEngine::dropObjectScripts (const ScriptableObject& object) {
  694. this->m_adapters.object->forget (object);
  695. for (auto& [key, module] : this->m_scriptModules) {
  696. if (module.object != &object) {
  697. continue;
  698. }
  699. module.object = nullptr;
  700. module.dropped = true;
  701. this->retireScript (key);
  702. }
  703. for (const char* global : { "thisLayer", "thisObject" }) {
  704. JSValue current = JS_GetPropertyStr (this->m_context, this->m_globalThis, global);
  705. if (Adapters::ScriptableObjectAdapter::getObject (current) == &object) {
  706. JS_SetPropertyStr (this->m_context, this->m_globalThis, global, JS_UNDEFINED);
  707. }
  708. JS_FreeValue (this->m_context, current);
  709. }
  710. }
  711. bool ScriptEngine::rebindScript (const std::string& key, DynamicValue& newValue) {
  712. const auto it = this->m_scriptModules.find (key);
  713. if (it == this->m_scriptModules.end () || it->second.value.getScriptSource () != newValue.getScriptSource ()) {
  714. return false;
  715. }
  716. // LoadedModule::value is a reference member, so the entry is erased and re-emplaced under the same key,
  717. // m_runningModule may currently point at the erased entry
  718. // the cached thisObject points at the old value, the replacement builds its own
  719. JS_FreeValue (this->m_context, it->second.thisObject);
  720. LoadedModule replacement { .value = newValue,
  721. .module = it->second.module,
  722. .object = it->second.object,
  723. .propertyName = it->second.propertyName,
  724. .order = it->second.order };
  725. this->m_scriptModules.erase (it);
  726. const auto inserted = this->m_scriptModules.emplace (key, replacement);
  727. this->m_runningModule = &inserted.first->second;
  728. return true;
  729. }
  730. void ScriptEngine::queueScript (
  731. const std::string& key, DynamicValue& currentValue, ScriptableObject& object, const std::string& propertyName
  732. ) {
  733. const auto source = currentValue.getScriptSource ();
  734. if (!source.has_value ()) {
  735. return;
  736. }
  737. auto it = this->m_scriptModules.find (key);
  738. if (it != this->m_scriptModules.end ()) {
  739. return;
  740. }
  741. // Compile-only first: JS_Eval(..., JS_EVAL_TYPE_MODULE) alone compiles AND evaluates in one
  742. // step, but its return value is always a Promise (the module's completion value), never the
  743. // namespace object. Compiling separately keeps the JS_TAG_MODULE value around so the real
  744. // namespace can be fetched via JS_GetModuleNamespace afterward.
  745. JSValue compiledModule = JS_Eval (
  746. this->m_context, source->c_str (), source->size (), key.c_str (),
  747. JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY
  748. );
  749. if (JS_IsException (compiledModule)) {
  750. logJSException (this->m_context, key.c_str (), source);
  751. return;
  752. }
  753. auto* moduleDef = static_cast<JSModuleDef*> (JS_VALUE_GET_PTR (compiledModule));
  754. // Register the entry (with a placeholder module value) and point m_runningModule at it before
  755. // evaluating below - top-level module code commonly does `export var scriptProperties =
  756. // createScriptProperties()...finish();`, and scriptpropertiescreator_finish()
  757. // (ScriptPropertiesObject.cpp) resolves it through getRunningModule(), which must already
  758. // point here.
  759. auto inserted = this->m_scriptModules.emplace (
  760. key,
  761. LoadedModule {
  762. .value = currentValue,
  763. .module = JS_UNDEFINED,
  764. .object = &object,
  765. .propertyName = propertyName,
  766. .order = this->m_nextModuleOrder++,
  767. }
  768. );
  769. if (!inserted.second) {
  770. JS_FreeValue (this->m_context, compiledModule);
  771. return;
  772. }
  773. this->m_runningModule = &inserted.first->second;
  774. // module top-level code (class bodies, helpers instantiated on load) can already touch thisLayer
  775. this->bindThisLayer (object, this->m_runningModule);
  776. // JS_EvalFunction runs the module body and returns a Promise - for a module with no top-level
  777. // await this resolves/rejects synchronously, so its state can be checked right away.
  778. // JS_IsException() on it is always false even if the module threw, since that exception gets
  779. // caught by the module machinery and stored as the promise's rejection reason instead.
  780. JSValue evalResult = JS_EvalFunction (this->m_context, compiledModule);
  781. // 3378399626's weather widget stores its offsets in shared at the top level from the defaults and only
  782. // picks the user's location up in applyUserProperties
  783. this->m_scriptPropertiesObject->deliverValues (inserted.first->second.value);
  784. if (JS_PromiseState (this->m_context, evalResult) == JS_PROMISE_REJECTED) {
  785. JSValue reason = JS_PromiseResult (this->m_context, evalResult);
  786. const char* str = JS_ToCString (this->m_context, reason);
  787. sLog.error ("ScriptEngine [", key, "] module evaluation rejected: ", str != nullptr ? str : "(no message)");
  788. if (str) {
  789. JS_FreeCString (this->m_context, str);
  790. }
  791. JS_FreeValue (this->m_context, reason);
  792. JS_FreeValue (this->m_context, evalResult);
  793. this->m_runningModule = nullptr;
  794. this->m_scriptModules.erase (inserted.first);
  795. return;
  796. }
  797. JS_FreeValue (this->m_context, evalResult);
  798. JSValue module = JS_GetModuleNamespace (this->m_context, moduleDef);
  799. if (JS_IsException (module)) {
  800. logJSException (this->m_context, key.c_str ());
  801. this->m_runningModule = nullptr;
  802. this->m_scriptModules.erase (inserted.first);
  803. return;
  804. }
  805. inserted.first->second.module = module;
  806. this->bindThisLayer (object, this->m_runningModule);
  807. // init() and the first update() run from tick(), once the whole scene has been constructed
  808. }
  809. namespace {
  810. enum AnimationOp {
  811. AnimPlay,
  812. AnimPause,
  813. AnimStop,
  814. AnimGetFrame,
  815. AnimSetFrame,
  816. AnimIsPlaying,
  817. AnimGetRate,
  818. AnimSetRate,
  819. AnimGetFrameCount,
  820. AnimGetName,
  821. AnimGetFps,
  822. };
  823. // backs every method/accessor of the IAnimation handle; func_data is [animation system id, clock id]
  824. JSValue animation_access (
  825. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  826. ) {
  827. int systemId = 0;
  828. int clockId = 0;
  829. JS_ToInt32 (ctx, &systemId, func_data[0]);
  830. JS_ToInt32 (ctx, &clockId, func_data[1]);
  831. auto* system = AnimationSystem::find (systemId);
  832. auto* clock = system == nullptr ? nullptr : system->clock (clockId);
  833. if (clock == nullptr) {
  834. return JS_UNDEFINED;
  835. }
  836. double number = 0.0;
  837. switch (magic) {
  838. case AnimPlay:
  839. clock->play ();
  840. return JS_UNDEFINED;
  841. case AnimPause:
  842. clock->pause ();
  843. return JS_UNDEFINED;
  844. case AnimStop:
  845. clock->stop ();
  846. return JS_UNDEFINED;
  847. case AnimGetFrame:
  848. return JS_NewFloat64 (ctx, clock->getFrame ());
  849. case AnimSetFrame:
  850. if (argc > 0 && JS_ToFloat64 (ctx, &number, argv[0]) == 0) {
  851. clock->setFrame (static_cast<float> (number));
  852. }
  853. return JS_UNDEFINED;
  854. case AnimIsPlaying:
  855. return JS_NewBool (ctx, clock->isPlaying ());
  856. case AnimGetRate:
  857. return JS_NewFloat64 (ctx, clock->getRate ());
  858. case AnimSetRate:
  859. if (argc > 0 && JS_ToFloat64 (ctx, &number, argv[0]) == 0) {
  860. clock->setRate (static_cast<float> (number));
  861. }
  862. return JS_UNDEFINED;
  863. case AnimGetFrameCount:
  864. return JS_NewFloat64 (ctx, clock->getDefinition ().length);
  865. case AnimGetName:
  866. return JS_NewString (ctx, clock->getDefinition ().name.c_str ());
  867. case AnimGetFps:
  868. return JS_NewFloat64 (ctx, clock->getDefinition ().fps);
  869. default:
  870. return JS_UNDEFINED;
  871. }
  872. }
  873. JSValue makeAnimationHandle (JSContext* ctx, int systemId, int clockId) {
  874. JSValue handle = JS_NewObject (ctx);
  875. JSValue data[] = { JS_NewInt32 (ctx, systemId), JS_NewInt32 (ctx, clockId) };
  876. const auto method = [&] (const char* name, int op, int length) {
  877. JS_SetPropertyStr (ctx, handle, name, JS_NewCFunctionData (ctx, animation_access, length, op, 2, data));
  878. };
  879. const auto accessor = [&] (const char* name, int getter, int setter) {
  880. JSAtom atom = JS_NewAtom (ctx, name);
  881. JSValue get = JS_NewCFunctionData (ctx, animation_access, 0, getter, 2, data);
  882. JSValue set = setter < 0 ? JS_UNDEFINED : JS_NewCFunctionData (ctx, animation_access, 1, setter, 2, data);
  883. JS_DefinePropertyGetSet (ctx, handle, atom, get, set, JS_PROP_ENUMERABLE);
  884. JS_FreeAtom (ctx, atom);
  885. };
  886. method ("play", AnimPlay, 0);
  887. method ("pause", AnimPause, 0);
  888. method ("stop", AnimStop, 0);
  889. method ("getFrame", AnimGetFrame, 0);
  890. method ("setFrame", AnimSetFrame, 1);
  891. method ("isPlaying", AnimIsPlaying, 0);
  892. accessor ("rate", AnimGetRate, AnimSetRate);
  893. accessor ("frameCount", AnimGetFrameCount, -1);
  894. accessor ("name", AnimGetName, -1);
  895. accessor ("fps", AnimGetFps, -1);
  896. JS_FreeValue (ctx, data[0]);
  897. JS_FreeValue (ctx, data[1]);
  898. return handle;
  899. }
  900. // thisObject.getAnimation(): func_data is [animation system id, address of the property's DynamicValue]
  901. JSValue this_object_get_animation (
  902. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  903. ) {
  904. int systemId = 0;
  905. int64_t address = 0;
  906. JS_ToInt32 (ctx, &systemId, func_data[0]);
  907. JS_ToInt64 (ctx, &address, func_data[1]);
  908. auto* system = AnimationSystem::find (systemId);
  909. auto* value = reinterpret_cast<DynamicValue*> (static_cast<intptr_t> (address));
  910. auto* clock = system == nullptr ? nullptr : system->clockOf (*value);
  911. if (clock == nullptr) {
  912. return JS_NULL;
  913. }
  914. return makeAnimationHandle (ctx, systemId, clock->getId ());
  915. }
  916. } // namespace
  917. // thisObject.<property name>: lets a listener registered by a property's script write that property
  918. // from outside (`listener.thisObject['Inner Color'] = color`). func_data is [engine address, value address, is angles]
  919. JSValue this_object_property (
  920. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  921. ) {
  922. int64_t engineAddress = 0;
  923. int64_t valueAddress = 0;
  924. JS_ToInt64 (ctx, &engineAddress, func_data[0]);
  925. JS_ToInt64 (ctx, &valueAddress, func_data[1]);
  926. auto* engine = reinterpret_cast<ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  927. auto* value = reinterpret_cast<DynamicValue*> (static_cast<intptr_t> (valueAddress));
  928. int64_t degrees = 0;
  929. JS_ToInt64 (ctx, &degrees, func_data[2]);
  930. const std::string_view name = degrees != 0 ? "angles" : "";
  931. if (magic == 0) {
  932. return engine->propertyToJs (*value, name);
  933. }
  934. if (argc > 0) {
  935. engine->assignPropertyJsValue (argv[0], *value, name);
  936. }
  937. return JS_UNDEFINED;
  938. }
  939. JSValue ScriptEngine::makeThisObject (DynamicValue& value, const std::string& propertyName) {
  940. JSValue handle = JS_NewObject (this->m_context);
  941. JSValue data[] = {
  942. JS_NewInt32 (this->m_context, this->m_animations.getId ()),
  943. JS_NewInt64 (this->m_context, static_cast<int64_t> (reinterpret_cast<intptr_t> (&value))),
  944. };
  945. JS_SetPropertyStr (
  946. this->m_context, handle, "getAnimation",
  947. JS_NewCFunctionData (this->m_context, this_object_get_animation, 0, 0, 2, data)
  948. );
  949. JS_FreeValue (this->m_context, data[0]);
  950. JS_FreeValue (this->m_context, data[1]);
  951. if (!propertyName.empty ()) {
  952. JSValue propertyData[] = {
  953. JS_NewInt64 (this->m_context, static_cast<int64_t> (reinterpret_cast<intptr_t> (this))),
  954. JS_NewInt64 (this->m_context, static_cast<int64_t> (reinterpret_cast<intptr_t> (&value))),
  955. JS_NewInt64 (this->m_context, propertyName == "angles" ? 1 : 0),
  956. };
  957. JSAtom atom = JS_NewAtom (this->m_context, propertyName.c_str ());
  958. JS_DefinePropertyGetSet (
  959. this->m_context, handle, atom,
  960. JS_NewCFunctionData (this->m_context, this_object_property, 0, 0, 3, propertyData),
  961. JS_NewCFunctionData (this->m_context, this_object_property, 1, 1, 3, propertyData), JS_PROP_ENUMERABLE
  962. );
  963. JS_FreeAtom (this->m_context, atom);
  964. JS_FreeValue (this->m_context, propertyData[0]);
  965. JS_FreeValue (this->m_context, propertyData[1]);
  966. JS_FreeValue (this->m_context, propertyData[2]);
  967. }
  968. return handle;
  969. }
  970. void ScriptEngine::bindThisLayer (ScriptableObject& object, LoadedModule* module) {
  971. JS_SetPropertyStr (this->m_context, this->m_globalThis, "thisLayer", this->m_adapters.object->instantiate (object));
  972. // thisObject is the property the running script is attached to (what getAnimation() hangs
  973. // off), not the layer
  974. if (module == nullptr) {
  975. JS_SetPropertyStr (
  976. this->m_context, this->m_globalThis, "thisObject", this->m_adapters.object->instantiate (object)
  977. );
  978. return;
  979. }
  980. if (JS_IsUndefined (module->thisObject)) {
  981. module->thisObject = this->makeThisObject (module->value, module->propertyName);
  982. }
  983. JS_SetPropertyStr (
  984. this->m_context, this->m_globalThis, "thisObject", JS_DupValue (this->m_context, module->thisObject)
  985. );
  986. }
  987. void ScriptEngine::dispatchAnimationEvents () {
  988. for (const auto& event : this->m_animations.takeEvents ()) {
  989. const auto* clock = this->m_animations.clock (event.clock);
  990. if (clock == nullptr) {
  991. continue;
  992. }
  993. for (auto& [key, module] : this->m_scriptModules) {
  994. if (&module.value != &clock->getRootValue () || !module.initialized || module.dropped) {
  995. continue;
  996. }
  997. this->m_runningModule = &module;
  998. if (module.object != nullptr) {
  999. this->bindThisLayer (*module.object, &module);
  1000. }
  1001. JSValue eventObject = JS_NewObject (this->m_context);
  1002. JS_SetPropertyStr (
  1003. this->m_context, eventObject, "name", JS_NewString (this->m_context, event.name.c_str ())
  1004. );
  1005. JS_SetPropertyStr (this->m_context, eventObject, "frame", JS_NewFloat64 (this->m_context, event.frame));
  1006. JSValue args[] = { eventObject, this->propertyToJs (module.value, module.propertyName) };
  1007. JSValue result = this->call (module.module, 2, args, "animationEvent");
  1008. if (JS_IsException (result)) {
  1009. logJSException (this->m_context, key.c_str (), module.value.getScriptSource ());
  1010. } else {
  1011. // like update(), the handler's return value becomes the property's new value
  1012. this->assignPropertyJsValue (result, module.value, module.propertyName);
  1013. }
  1014. JS_FreeValue (this->m_context, result);
  1015. JS_FreeValue (this->m_context, args[0]);
  1016. JS_FreeValue (this->m_context, args[1]);
  1017. break;
  1018. }
  1019. }
  1020. }
  1021. void ScriptEngine::initializeModule (const std::string& key, LoadedModule& module) {
  1022. module.initialized = true;
  1023. // init() receives the property's static/base value exactly once, before update() starts being
  1024. // called every tick - scripts commonly stash it (e.g. audio-reactive scale scripts scaling a
  1025. // captured base value) and would otherwise read undefined forever.
  1026. JSValue initArgs[] = { this->propertyToJs (module.value, module.propertyName) };
  1027. const DynamicValue valueBeforeInit (module.value);
  1028. JSValue initResult = this->call (module.module, 1, initArgs, "init");
  1029. if (JS_IsException (initResult)) {
  1030. logJSException (this->m_context, key.c_str (), module.value.getScriptSource ());
  1031. } else if (
  1032. valueBeforeInit.getType () == module.value.getType () && valueBeforeInit.getVec4 () == module.value.getVec4 ()
  1033. && valueBeforeInit.getString () == module.value.getString ()
  1034. ) {
  1035. // init() may hand back a different starting value, unless something already moved the property meanwhile
  1036. this->assignPropertyJsValue (initResult, module.value, module.propertyName);
  1037. }
  1038. JS_FreeValue (this->m_context, initResult);
  1039. JS_FreeValue (this->m_context, initArgs[0]);
  1040. // the real engine hands every user property to applyUserProperties() once at load (and only the
  1041. // changed ones afterward) - scripts like music selectors rely on that first call to start up
  1042. JSValue userProps = JS_NewObject (this->m_context);
  1043. for (const auto& [name, property] : this->m_engineObject->getScene ().getUserProperties ()) {
  1044. JS_SetPropertyStr (this->m_context, userProps, name.c_str (), this->userPropertyToJs (*property));
  1045. }
  1046. JSValue applyArgs[] = { userProps };
  1047. JSValue applyResult = this->call (module.module, 1, applyArgs, "applyUserProperties");
  1048. if (JS_IsException (applyResult)) {
  1049. logJSException (this->m_context, key.c_str (), module.value.getScriptSource ());
  1050. }
  1051. JS_FreeValue (this->m_context, applyResult);
  1052. JS_FreeValue (this->m_context, userProps);
  1053. if (this->m_mediaSource.getMediaInfo ().available) {
  1054. this->notifyMediaUpdate (this->m_mediaSource.getMediaInfo (), &module);
  1055. }
  1056. }
  1057. namespace {
  1058. constexpr const char* CURSOR_HANDLERS[]
  1059. = { "cursorEnter", "cursorLeave", "cursorMove", "cursorDown", "cursorUp", "cursorClick" };
  1060. }
  1061. bool ScriptEngine::hasCursorHandlers (const ScriptableObject& object) {
  1062. for (auto& module : this->m_scriptModules | std::views::values) {
  1063. if (module.object != &object || !module.initialized) {
  1064. continue;
  1065. }
  1066. if (module.cursorHandlers < 0) {
  1067. module.cursorHandlers = 0;
  1068. for (const char* handler : CURSOR_HANDLERS) {
  1069. JSValue function = JS_GetPropertyStr (this->m_context, module.module, handler);
  1070. if (JS_IsFunction (this->m_context, function)) {
  1071. module.cursorHandlers = 1;
  1072. }
  1073. JS_FreeValue (this->m_context, function);
  1074. }
  1075. }
  1076. if (module.cursorHandlers > 0) {
  1077. return true;
  1078. }
  1079. }
  1080. return false;
  1081. }
  1082. void ScriptEngine::dispatchCursorEvent (
  1083. const char* handler, ScriptableObject& object, const glm::vec2& worldPosition, const glm::vec3& localPosition
  1084. ) {
  1085. // Vec3 per WE's lib.sceneScript.d.ts, scripts call add()/subtract() on them
  1086. const auto makePosition = [this] (const glm::vec3& value) {
  1087. DynamicValue position (value);
  1088. return this->m_adapters.vec3->instantiate (position);
  1089. };
  1090. for (auto& [key, module] : this->m_scriptModules) {
  1091. if (module.object != &object || !module.initialized || module.cursorHandlers <= 0) {
  1092. continue;
  1093. }
  1094. this->m_runningModule = &module;
  1095. this->bindThisLayer (*module.object, &module);
  1096. JSValue event = JS_NewObject (this->m_context);
  1097. JS_SetPropertyStr (this->m_context, event, "worldPosition", makePosition (glm::vec3 (worldPosition, 0.0f)));
  1098. JS_SetPropertyStr (this->m_context, event, "localPosition", makePosition (localPosition));
  1099. JSValue args[] = { event };
  1100. JSValue result = this->call (module.module, 1, args, handler);
  1101. if (JS_IsException (result)) {
  1102. logJSException (this->m_context, key.c_str (), module.value.getScriptSource ());
  1103. }
  1104. JS_FreeValue (this->m_context, result);
  1105. JS_FreeValue (this->m_context, event);
  1106. }
  1107. this->m_runningModule = nullptr;
  1108. }
  1109. void ScriptEngine::addVideoEndedCallback (VideoPlayback::MPV::GLPlayer* player, JSValueConst callback) {
  1110. if (player == nullptr || !JS_IsFunction (this->m_context, callback)) {
  1111. return;
  1112. }
  1113. this->m_videoEndedCallbacks.push_back ({ player, JS_DupValue (this->m_context, callback) });
  1114. }
  1115. void ScriptEngine::tick () {
  1116. this->m_engineObject->tick ();
  1117. // index based, a callback is free to register more callbacks (and reallocate the vector)
  1118. for (size_t index = 0; index < this->m_videoEndedCallbacks.size (); index++) {
  1119. auto& entry = this->m_videoEndedCallbacks[index];
  1120. if (!entry.player->hasEnded ()) {
  1121. entry.notified = false;
  1122. continue;
  1123. }
  1124. if (entry.notified) {
  1125. continue;
  1126. }
  1127. entry.notified = true;
  1128. JSValue callback = JS_DupValue (this->m_context, entry.callback);
  1129. JSValue result = JS_Call (this->m_context, callback, JS_UNDEFINED, 0, nullptr);
  1130. if (JS_IsException (result)) {
  1131. logJSException (this->m_context, "video ended callback");
  1132. }
  1133. JS_FreeValue (this->m_context, result);
  1134. JS_FreeValue (this->m_context, callback);
  1135. }
  1136. // safe to erase retired modules now, nothing is mid-iteration and no queueScript() is on the stack
  1137. for (const auto& key : this->m_retiredScriptKeys) {
  1138. const auto it = this->m_scriptModules.find (key);
  1139. if (it == this->m_scriptModules.end ()) {
  1140. continue;
  1141. }
  1142. JS_FreeValue (this->m_context, it->second.module);
  1143. JS_FreeValue (this->m_context, it->second.thisObject);
  1144. this->m_scriptModules.erase (it);
  1145. }
  1146. this->m_retiredScriptKeys.clear ();
  1147. // long stalls (window dragged, suspend) shouldn't skip whole animations
  1148. this->m_animations.tick (std::clamp (g_Time - g_TimeLast, 0.0f, 0.25f));
  1149. // run any pending notifications
  1150. // scene order like WE, not the order of the keys: 3444535389's bee script reads shared values that the
  1151. // setup script of an earlier object writes, run before it the bee's position turned NaN for good.
  1152. // Keys are looked up again per module, an update() may rebind (erase and re-add) an entry.
  1153. std::vector<std::pair<uint64_t, std::string>> order;
  1154. order.reserve (this->m_scriptModules.size ());
  1155. for (const auto& [key, module] : this->m_scriptModules) {
  1156. order.emplace_back (module.order, key);
  1157. }
  1158. std::ranges::sort (order);
  1159. for (const auto& [unused, key] : order) {
  1160. const auto entry = this->m_scriptModules.find (key);
  1161. if (entry == this->m_scriptModules.end () || entry->second.dropped) {
  1162. continue;
  1163. }
  1164. auto& module = entry->second;
  1165. this->m_runningModule = &module;
  1166. // `thisLayer` is a single global binding shared by every module and only set once, at
  1167. // registration in queueScript() - without rebinding it here, every module but the last
  1168. // registered would see whatever object queueScript() last ran for, not its own layer.
  1169. if (module.object != nullptr) {
  1170. this->bindThisLayer (*module.object, &module);
  1171. }
  1172. if (!module.initialized) {
  1173. this->initializeModule (key, module);
  1174. }
  1175. JSValue args[] = { this->propertyToJs (module.value, module.propertyName) };
  1176. JSValue result = this->call (module.module, 1, args, "update");
  1177. ScopeGuard guard ([result, args, this] () {
  1178. JS_FreeValue (this->m_context, result);
  1179. JS_FreeValue (this->m_context, args[0]);
  1180. });
  1181. if (JS_IsException (result)) {
  1182. logJSException (this->m_context, key.c_str (), module.value.getScriptSource ());
  1183. continue;
  1184. }
  1185. if (key.starts_with ("scale_")) {
  1186. static int scaleDiagnosticCounter = 0;
  1187. if (++scaleDiagnosticCounter >= 300) {
  1188. scaleDiagnosticCounter = 0;
  1189. sLog.debug (
  1190. "scale script '", key, "': update() returned tag=", JS_VALUE_GET_TAG (result), ", current vec3 = (",
  1191. module.value.getVec3 ().x, ", ", module.value.getVec3 ().y, ", ", module.value.getVec3 ().z, ")"
  1192. );
  1193. }
  1194. // Edge-triggered marker for when a pulse lands visually, timestamped so it can be
  1195. // correlated against when the sound was played.
  1196. static std::map<std::string, bool> wasPulsing;
  1197. const bool pulsingNow = std::abs (module.value.getVec3 ().x - 1.0f) > 0.03f;
  1198. if (pulsingNow && !wasPulsing[key]) {
  1199. const auto now = std::chrono::system_clock::now ();
  1200. const auto ms = std::chrono::duration_cast<std::chrono::milliseconds> (now.time_since_epoch ()) % 1000;
  1201. const std::time_t t = std::chrono::system_clock::to_time_t (now);
  1202. std::tm tmBuf {};
  1203. localtime_r (&t, &tmBuf);
  1204. char buf[16];
  1205. std::strftime (buf, sizeof (buf), "%H:%M:%S", &tmBuf);
  1206. sLog.debug (
  1207. "[", buf, ".", (ms.count () < 100 ? "0" : ""), (ms.count () < 10 ? "0" : ""), ms.count (),
  1208. "] scale script '", key, "': PULSE, vec3.x=", module.value.getVec3 ().x
  1209. );
  1210. }
  1211. wasPulsing[key] = pulsingNow;
  1212. }
  1213. this->assignPropertyJsValue (result, module.value, module.propertyName);
  1214. }
  1215. this->dispatchAnimationEvents ();
  1216. }
  1217. Media::ThumbnailPalette ScriptEngine::thumbnailPaletteFor (const Media::MediaSource::MediaInfo& media) {
  1218. if (!media.url.has_value ()) {
  1219. return {};
  1220. }
  1221. // every module gets the event, decode the cover once per file
  1222. if (this->m_paletteUrl == *media.url) {
  1223. return this->m_palette;
  1224. }
  1225. this->m_paletteUrl = *media.url;
  1226. this->m_palette = {};
  1227. std::string path = *media.url;
  1228. if (!path.starts_with ("file://")) {
  1229. return this->m_palette;
  1230. }
  1231. this->m_palette = Media::loadThumbnailPalette (path.substr (7));
  1232. return this->m_palette;
  1233. }
  1234. void ScriptEngine::notifyMediaUpdate (const Media::MediaSource::MediaInfo& media, LoadedModule* only) {
  1235. JSContext* ctx = this->m_context;
  1236. const auto palette = this->thumbnailPaletteFor (media);
  1237. DynamicValue primaryColorValue (palette.primary);
  1238. DynamicValue secondaryColorValue (palette.secondary);
  1239. DynamicValue tertiaryColorValue (palette.tertiary);
  1240. DynamicValue textColorValue (palette.text);
  1241. DynamicValue highContrastColorValue (palette.highContrast);
  1242. JSValue primaryColor = this->m_adapters.vec3->instantiate (primaryColorValue, true);
  1243. JSValue secondaryColor = this->m_adapters.vec3->instantiate (secondaryColorValue, true);
  1244. JSValue tertiaryColor = this->m_adapters.vec3->instantiate (tertiaryColorValue, true);
  1245. JSValue textColor = this->m_adapters.vec3->instantiate (textColorValue, true);
  1246. JSValue highContrastColor = this->m_adapters.vec3->instantiate (highContrastColorValue, true);
  1247. JSValue propertiesEvent = JS_NewObject (ctx);
  1248. JS_SetPropertyStr (ctx, propertiesEvent, "title", JS_NewString (ctx, media.title.c_str ()));
  1249. JS_SetPropertyStr (ctx, propertiesEvent, "artist", JS_NewString (ctx, media.artist.c_str ()));
  1250. JS_SetPropertyStr (ctx, propertiesEvent, "albumTitle", JS_NewString (ctx, media.album.c_str ()));
  1251. JSValue playbackEvent = JS_NewObject (ctx);
  1252. JS_SetPropertyStr (ctx, playbackEvent, "state", JS_NewInt32 (ctx, media.playbackState));
  1253. JSValue mediaTimelineEvent = JS_NewObject (ctx);
  1254. JS_SetPropertyStr (ctx, mediaTimelineEvent, "position", JS_NewFloat64 (ctx, media.position));
  1255. JS_SetPropertyStr (ctx, mediaTimelineEvent, "duration", JS_NewFloat64 (ctx, media.duration));
  1256. JSValue mediaThumbnailEvent = JS_NewObject (ctx);
  1257. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "hasThumbnail", JS_NewBool (ctx, media.url.has_value ()));
  1258. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "primaryColor", primaryColor);
  1259. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "secondaryColor", secondaryColor);
  1260. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "tertiaryColor", tertiaryColor);
  1261. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "textColor", textColor);
  1262. JS_SetPropertyStr (ctx, mediaThumbnailEvent, "highContrastColor", highContrastColor);
  1263. JSValue propertiesArgs[] = { propertiesEvent };
  1264. JSValue playbackArgs[] = { playbackEvent };
  1265. JSValue mediaTimelineArgs[] = { mediaTimelineEvent };
  1266. JSValue mediaThumbnailArgs[] = { mediaThumbnailEvent };
  1267. for (auto& module : this->m_scriptModules | std::views::values) {
  1268. // modules that haven't run init() yet get the current media state replayed once they do
  1269. if (!module.initialized || (only != nullptr && only != &module)) {
  1270. continue;
  1271. }
  1272. JSValue result1 = this->call (module.module, 1, propertiesArgs, "mediaPropertiesChanged");
  1273. JSValue result2 = this->call (module.module, 1, playbackArgs, "mediaPlaybackChanged");
  1274. JSValue result3 = this->call (module.module, 1, mediaTimelineArgs, "mediaTimelineChanged");
  1275. JSValue result4 = this->call (module.module, 1, mediaThumbnailArgs, "mediaThumbnailChanged");
  1276. JS_FreeValue (ctx, result1);
  1277. JS_FreeValue (ctx, result2);
  1278. JS_FreeValue (ctx, result3);
  1279. JS_FreeValue (ctx, result4);
  1280. }
  1281. // free all created objects as we don't keep a ref to them anymore
  1282. JS_FreeValue (ctx, propertiesEvent);
  1283. JS_FreeValue (ctx, playbackEvent);
  1284. JS_FreeValue (ctx, mediaTimelineEvent);
  1285. JS_FreeValue (ctx, mediaThumbnailEvent);
  1286. }