ScriptEngine.h 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #pragma once
  2. #include "Adapters/VectorAdapter.h"
  3. #include "AnimationSystem.h"
  4. #include "ConsoleObject.h"
  5. #include "EngineObject.h"
  6. #include "InputObject.h"
  7. #include "Modules/ScriptModule.h"
  8. #include "SceneObject.h"
  9. #include <chrono>
  10. #include <future>
  11. #include <map>
  12. #include <memory>
  13. #include <optional>
  14. #include <string>
  15. #include <unordered_map>
  16. #include <unordered_set>
  17. #include <vector>
  18. #include "WallpaperEngine/Data/Model/DynamicValue.h"
  19. #include "WallpaperEngine/Data/Model/Types.h"
  20. #include "WallpaperEngine/Media/MediaSource.h"
  21. #include "WallpaperEngine/Media/ThumbnailPalette.h"
  22. namespace WallpaperEngine::Media {
  23. class MediaSource;
  24. }
  25. extern "C" {
  26. #include "quickjs.h"
  27. }
  28. namespace WallpaperEngine::Render::Wallpapers {
  29. class CScene;
  30. }
  31. namespace WallpaperEngine::VideoPlayback::MPV {
  32. class GLPlayer;
  33. }
  34. namespace WallpaperEngine::Scripting {
  35. void logJSException (JSContext* ctx, const char* context);
  36. class ScriptPropertiesObject;
  37. namespace Adapters {
  38. class ScriptableObjectAdapter;
  39. }
  40. using namespace WallpaperEngine::Data::Model;
  41. // Opaque handle returned by createLayerScript. 0 means invalid / not created.
  42. using ScriptLayerHandle = int;
  43. static constexpr ScriptLayerHandle kInvalidLayerHandle = 0;
  44. class ScriptEngine {
  45. public:
  46. struct LoadedModule {
  47. DynamicValue& value;
  48. JSValue module;
  49. // Owning layer, so tick() can rebind `thisLayer` to the right object before each
  50. // module's update() runs - see ScriptEngine::tick().
  51. ScriptableObject* object = nullptr;
  52. // init()/applyUserProperties() wait for the first tick() so every layer of the scene already
  53. // exists when a script looks its siblings up with thisScene.getLayer()
  54. bool initialized = false;
  55. // cached `thisObject` handle, see makeThisObject()
  56. JSValue thisObject = JS_UNDEFINED;
  57. // name of the property the script is attached to ("origin", an effect constant, ...)
  58. std::string propertyName;
  59. // -1 until checked, then whether the module exports any cursor* handler
  60. int cursorHandlers = -1;
  61. };
  62. struct JSObjectAdapters {
  63. std::unique_ptr<Adapters::VectorAdapter<4>> vec4;
  64. std::unique_ptr<Adapters::VectorAdapter<3>> vec3;
  65. std::unique_ptr<Adapters::VectorAdapter<2>> vec2;
  66. std::unique_ptr<Adapters::ScriptableObjectAdapter> object;
  67. };
  68. ~ScriptEngine ();
  69. ScriptEngine (Render::Wallpapers::CScene& scene, Media::MediaSource& mediaSource);
  70. ScriptEngine (const ScriptEngine&) = delete;
  71. ScriptEngine& operator= (const ScriptEngine&) = delete;
  72. JSRuntime* getRuntime () const { return m_runtime; }
  73. JSContext* getContext () const { return m_context; }
  74. JSValue getGlobalThis () const { return m_globalThis; }
  75. LoadedModule* getRunningModule () const { return m_runningModule; }
  76. JSValue dynamicToJs (DynamicValue& value) const;
  77. /** Same as dynamicToJs() but colour properties come out as Vec3 like they do in real scripts, not Vec4 */
  78. JSValue userPropertyToJs (Property& property) const;
  79. // Converts a JS value read from `val` into `target` - the inverse of dynamicToJs(), exposed
  80. // so exotic property setters (e.g. `thisLayer.origin = ...` from another layer's script) can
  81. // write through to the real property instead of silently discarding the assignment.
  82. void assignJsValue (JSValue val, DynamicValue& target) const;
  83. /**
  84. * Evaluate a WallpaperEngine script's update() function.
  85. *
  86. * @param key The full JS script text (ES6 module with export function update(value))
  87. * @param currentValue The current value to pass to update()
  88. * @return The modified value from update(), or a copy of currentValue on error
  89. */
  90. void queueScript (
  91. const std::string& key, DynamicValue& currentValue, ScriptableObject& object, const std::string& propertyName = {}
  92. );
  93. /** Stops a queued script module (by its queueScript() key) and frees it at the start of the next tick(), for when a later registerProperty() supersedes it */
  94. void retireScript (const std::string& key);
  95. /** Rebinds an already-running module under key to newValue in place when its script source is identical, so init() does not run twice. Returns false if nothing was rebound */
  96. bool rebindScript (const std::string& key, DynamicValue& newValue);
  97. /**
  98. * Runs a frame tick in the javascript engine. Dispatches any pending events,
  99. * timeouts, intervals AND calls any update() functions.
  100. */
  101. void tick ();
  102. // Layer-script API (Phase 2 - dynamic text): WE text-object scripts follow a lifecycle that
  103. // doesn't fit the simple `update(value) -> value` contract above. They typically look like:
  104. //
  105. // export var scriptProperties = createScriptProperties()…finish();
  106. // export function init() { /* subscribe to events, cache data */ }
  107. // export function update() { thisLayer.text = computeCurrentText(); }
  108. //
  109. // The script mutates `thisLayer` in place rather than returning a value, and lifecycle
  110. // functions are optional, so the API below keeps per-layer state alive across frames:
  111. // `init()` runs once, `update()` re-runs every tick.
  112. /**
  113. * Create a persistent "layer script" from a WE text-object script.
  114. *
  115. * @param scriptSource The full JS script text.
  116. * @param initialScriptProps Initial values for scriptProperties entries.
  117. * Ownership stays with the caller; we only snapshot current values.
  118. * @param initialText Initial value of `thisLayer.text` (usually the
  119. * static placeholder carried in the JSON).
  120. * @return A positive handle, or kInvalidLayerHandle if evaluation failed.
  121. */
  122. ScriptLayerHandle createLayerScript (
  123. const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
  124. const std::string& initialText
  125. );
  126. /**
  127. * Advance a layer by one frame.
  128. *
  129. * On the first call, invokes `init()` (if defined) before `update()`.
  130. * Updates a `thisScene` context visible to the script (time, fps).
  131. * Silently no-ops if the handle is invalid.
  132. */
  133. void tickLayer (ScriptLayerHandle handle, double time, double deltaTime, double fps);
  134. /**
  135. * Read the current value of `thisLayer.text` for the given layer.
  136. * Returns an empty string if the handle is invalid.
  137. */
  138. std::string layerText (ScriptLayerHandle handle);
  139. /**
  140. * Tear down a layer: invokes `destroy()` (if defined) and frees state.
  141. */
  142. void destroyLayer (ScriptLayerHandle handle);
  143. /** Whether any running script on the object exports a cursor handler (cursorEnter, cursorClick, ...) */
  144. [[nodiscard]] bool hasCursorHandlers (const ScriptableObject& object);
  145. /**
  146. * Calls `handler` (cursorEnter/cursorLeave/cursorMove/cursorDown/cursorUp/cursorClick) on every script running on
  147. * the object with an event carrying worldPosition (scene coordinates) and localPosition (offset from the layer's center)
  148. */
  149. void dispatchCursorEvent (
  150. const char* handler, ScriptableObject& object, const glm::vec2& worldPosition, const glm::vec2& localPosition
  151. );
  152. /** Calls callback (once per playthrough) when player reaches the end of a non-looping video, for IVideoTexture.addEndedCallback() */
  153. void addVideoEndedCallback (VideoPlayback::MPV::GLPlayer* player, JSValueConst callback);
  154. AnimationSystem& getAnimations () { return m_animations; }
  155. /** Whether a script module is currently running for this property value */
  156. [[nodiscard]] bool hasScript (const DynamicValue& value) const;
  157. const JSObjectAdapters& getAdapters () const { return m_adapters; }
  158. const Render::Wallpapers::CScene& getScene () const { return m_scene; }
  159. const std::map<std::string, std::unique_ptr<Modules::ScriptModule>>& getModules () const { return m_modules; }
  160. private:
  161. JSValue call (JSValue module, int argc, JSValueConst argv[], const char* name);
  162. void installBuiltins ();
  163. Media::ThumbnailPalette thumbnailPaletteFor (const Media::MediaSource::MediaInfo& media);
  164. void notifyMediaUpdate (const Media::MediaSource::MediaInfo& media, LoadedModule* only = nullptr);
  165. void initializeModule (const std::string& key, LoadedModule& module);
  166. void bindThisLayer (ScriptableObject& object, LoadedModule* module = nullptr);
  167. JSValue makeThisObject (DynamicValue& value, const std::string& propertyName);
  168. void dispatchAnimationEvents ();
  169. // Installs globalThis.__layers and related helpers. Called lazily.
  170. void ensureLayerRegistry ();
  171. JSRuntime* m_runtime = nullptr;
  172. JSContext* m_context = nullptr;
  173. JSValue m_globalThis;
  174. Render::Wallpapers::CScene& m_scene;
  175. std::unique_ptr<EngineObject> m_engineObject;
  176. std::unique_ptr<InputObject> m_inputObject;
  177. std::unique_ptr<SceneObject> m_sceneObject;
  178. std::unique_ptr<ConsoleObject> m_consoleObject;
  179. std::unique_ptr<ScriptPropertiesObject> m_scriptPropertiesObject;
  180. std::map<std::string, std::unique_ptr<Modules::ScriptModule>> m_modules = {};
  181. std::map<std::string, LoadedModule> m_scriptModules = {};
  182. std::vector<std::string> m_retiredScriptKeys = {};
  183. LoadedModule* m_runningModule = nullptr;
  184. struct VideoEndedCallback {
  185. VideoPlayback::MPV::GLPlayer* player;
  186. JSValue callback;
  187. // set once the callback ran for the current end, cleared when the video is seeked back
  188. bool notified = false;
  189. };
  190. std::vector<VideoEndedCallback> m_videoEndedCallbacks = {};
  191. ScriptLayerHandle m_nextLayerId = 1;
  192. bool m_layerRegistryReady = false;
  193. std::map<ScriptLayerHandle, bool> m_layerInitialized;
  194. bool m_builtinsInstalled = false;
  195. Media::MediaSource& m_mediaSource;
  196. std::function<void ()> m_unregisterMediaUpdateCallback;
  197. std::function<void ()> m_unregisterAlbumArtUpdateCallback;
  198. std::string m_paletteUrl;
  199. Media::ThumbnailPalette m_palette;
  200. JSObjectAdapters m_adapters;
  201. AnimationSystem m_animations;
  202. };
  203. } // namespace WallpaperEngine::Scripting