ScriptableObjectAdapter.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. #include "ScriptableObjectAdapter.h"
  2. #include <glm/gtc/type_ptr.hpp>
  3. #include <algorithm>
  4. #include <cmath>
  5. #include <cstring>
  6. #include <ranges>
  7. #include <utility>
  8. #include "WallpaperEngine/Data/Model/DynamicValue.h"
  9. #include "WallpaperEngine/Data/Model/Object.h"
  10. #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
  11. #include "WallpaperEngine/Logging/Log.h"
  12. #include "WallpaperEngine/Render/Objects/CImage.h"
  13. #include "WallpaperEngine/Scripting/ScriptEngine.h"
  14. #include "WallpaperEngine/Scripting/ScriptableObject.h"
  15. #include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
  16. using namespace WallpaperEngine::Data::Model;
  17. using namespace WallpaperEngine::Data::Utils;
  18. using namespace WallpaperEngine::Scripting::Adapters;
  19. #define SCRIPTABLE_OPAQUE_MAGIC 0xdeadbeef
  20. struct OpaqueScriptableObjectAdapter {
  21. unsigned int magic;
  22. ScriptableObjectAdapter& adapter;
  23. WallpaperEngine::Scripting::ScriptableObject& object;
  24. };
  25. // the object's address rides along as function data, safe because every ScriptableObject outlives the script context
  26. JSValue scriptableobject_playback_call (
  27. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  28. ) {
  29. int64_t address = 0;
  30. JS_ToInt64 (ctx, &address, func_data[0]);
  31. auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (address));
  32. if (magic == 2) {
  33. return JS_NewBool (ctx, object->isPlaying ());
  34. }
  35. using Playback = WallpaperEngine::Scripting::ScriptableObject::Playback;
  36. object->setPlayback (magic == 0 ? Playback::Playing : magic == 1 ? Playback::Paused : Playback::Stopped);
  37. return JS_UNDEFINED;
  38. }
  39. // only scriptable layers can be handed to scripts, a plain group parent comes back as null
  40. JSValue scriptableobject_hierarchy_call (
  41. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  42. ) {
  43. int64_t objectAddress = 0;
  44. int64_t engineAddress = 0;
  45. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  46. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  47. auto* object
  48. = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  49. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  50. const auto& scene = engine->getScene ();
  51. if (magic == 0) {
  52. const auto& parentId = object->getObject ().parent;
  53. if (!parentId.has_value ()) {
  54. return JS_NULL;
  55. }
  56. const auto* parent = scene.getObject (*parentId);
  57. if (parent == nullptr || !parent->is<WallpaperEngine::Scripting::ScriptableObject> ()) {
  58. return JS_NULL;
  59. }
  60. return engine->getAdapters ().object->instantiate (
  61. const_cast<WallpaperEngine::Scripting::ScriptableObject&> (
  62. *parent->as<WallpaperEngine::Scripting::ScriptableObject> ()
  63. )
  64. );
  65. }
  66. JSValue children = JS_NewArray (ctx);
  67. uint32_t index = 0;
  68. for (const auto* candidate : scene.getObjectsByRenderOrder ()) {
  69. const auto& candidateParent = candidate->getObject ().parent;
  70. if (!candidateParent.has_value () || *candidateParent != object->getObject ().id
  71. || !candidate->is<WallpaperEngine::Scripting::ScriptableObject> ()) {
  72. continue;
  73. }
  74. JS_SetPropertyUint32 (
  75. ctx, children, index++,
  76. engine->getAdapters ().object->instantiate (
  77. const_cast<WallpaperEngine::Scripting::ScriptableObject&> (
  78. *candidate->as<WallpaperEngine::Scripting::ScriptableObject> ()
  79. )
  80. )
  81. );
  82. }
  83. return children;
  84. }
  85. // effect handles keep the effect's address, safe for the same reason as the object address above
  86. JSValue scriptableeffect_visible_call (
  87. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  88. ) {
  89. int64_t effectAddress = 0;
  90. int64_t engineAddress = 0;
  91. JS_ToInt64 (ctx, &effectAddress, func_data[0]);
  92. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  93. auto* effect = reinterpret_cast<ImageEffect*> (static_cast<intptr_t> (effectAddress));
  94. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  95. if (magic == 0) {
  96. return JS_NewBool (ctx, effect->visible->value->getBool ());
  97. }
  98. if (argc > 0) {
  99. engine->assignJsValue (argv[0], *effect->visible->value);
  100. }
  101. return JS_UNDEFINED;
  102. }
  103. const std::vector<ImageEffectUniquePtr>* effectsOf (const Object& object) {
  104. if (object.is<Image> ()) {
  105. return &object.as<Image> ()->effects;
  106. }
  107. if (object.is<Text> ()) {
  108. return &object.as<Text> ()->effects;
  109. }
  110. return nullptr;
  111. }
  112. JSValue
  113. scriptableeffect_instantiate (JSContext* ctx, ImageEffect& effect, WallpaperEngine::Scripting::ScriptEngine& engine) {
  114. JSValue handle = JS_NewObject (ctx);
  115. JSValue data[] = {
  116. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&effect))),
  117. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&engine))),
  118. };
  119. const JSAtom visible = JS_NewAtom (ctx, "visible");
  120. JS_DefinePropertyGetSet (
  121. ctx, handle, visible, JS_NewCFunctionData (ctx, scriptableeffect_visible_call, 0, 0, 2, data),
  122. JS_NewCFunctionData (ctx, scriptableeffect_visible_call, 1, 1, 2, data), JS_PROP_ENUMERABLE
  123. );
  124. JS_FreeAtom (ctx, visible);
  125. JS_SetPropertyStr (ctx, handle, "name", JS_NewString (ctx, effect.name.c_str ()));
  126. return handle;
  127. }
  128. JSValue scriptableobject_effect_call (
  129. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  130. ) {
  131. int64_t objectAddress = 0;
  132. int64_t engineAddress = 0;
  133. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  134. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  135. auto* object
  136. = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  137. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  138. const auto* effects = effectsOf (object->getObject ());
  139. if (magic == 0) {
  140. return JS_NewInt32 (ctx, effects == nullptr ? 0 : static_cast<int> (effects->size ()));
  141. }
  142. if (effects == nullptr || argc < 1) {
  143. return JS_UNDEFINED;
  144. }
  145. if (JS_IsNumber (argv[0])) {
  146. int index = 0;
  147. JS_ToInt32 (ctx, &index, argv[0]);
  148. if (index < 0 || static_cast<size_t> (index) >= effects->size ()) {
  149. return JS_UNDEFINED;
  150. }
  151. return scriptableeffect_instantiate (ctx, *(*effects)[index], *engine);
  152. }
  153. const char* name = JS_ToCString (ctx, argv[0]);
  154. if (name == nullptr) {
  155. return JS_UNDEFINED;
  156. }
  157. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  158. for (const auto& effect : *effects) {
  159. if (effect->name == name) {
  160. return scriptableeffect_instantiate (ctx, *effect, *engine);
  161. }
  162. }
  163. return JS_UNDEFINED;
  164. }
  165. // video textures are only ever reached through the player's address, kept alive by the layer's texture for as long as
  166. // the scene exists
  167. JSValue videotexture_call (
  168. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  169. ) {
  170. int64_t playerAddress = 0;
  171. int64_t engineAddress = 0;
  172. JS_ToInt64 (ctx, &playerAddress, func_data[0]);
  173. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  174. auto* player
  175. = reinterpret_cast<WallpaperEngine::VideoPlayback::MPV::GLPlayer*> (static_cast<intptr_t> (playerAddress));
  176. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  177. switch (magic) {
  178. case 0:
  179. if (player->hasEnded ()) {
  180. player->seek (0.0);
  181. }
  182. player->clearPaused ();
  183. return JS_UNDEFINED;
  184. case 1:
  185. player->setPaused ();
  186. return JS_UNDEFINED;
  187. case 2:
  188. player->setPaused ();
  189. player->seek (0.0);
  190. return JS_UNDEFINED;
  191. case 3:
  192. return JS_NewBool (ctx, !player->isPaused ());
  193. case 4:
  194. return JS_NewBool (ctx, player->isPaused ());
  195. case 5:
  196. return JS_NewBool (ctx, player->isPaused () && player->getPlaybackPosition () < 0.001);
  197. case 6:
  198. return JS_NewFloat64 (ctx, player->getPlaybackPosition ());
  199. case 7:
  200. {
  201. double seconds = 0.0;
  202. if (argc > 0 && JS_ToFloat64 (ctx, &seconds, argv[0]) == 0) {
  203. player->seek (std::max (seconds, 0.0));
  204. }
  205. return JS_UNDEFINED;
  206. }
  207. case 8:
  208. if (argc > 0) {
  209. engine->addVideoEndedCallback (player, argv[0]);
  210. }
  211. return JS_UNDEFINED;
  212. case 9:
  213. return JS_NewBool (ctx, player->isLooping ());
  214. case 10:
  215. if (argc > 0) {
  216. player->setLoop (JS_ToBool (ctx, argv[0]) > 0);
  217. }
  218. return JS_UNDEFINED;
  219. case 11:
  220. return JS_NewFloat64 (ctx, player->getDuration ());
  221. case 12:
  222. return JS_NewFloat64 (ctx, player->getSpeed ());
  223. case 13:
  224. {
  225. double rate = 0.0;
  226. if (argc > 0 && JS_ToFloat64 (ctx, &rate, argv[0]) == 0 && rate > 0.0) {
  227. player->setSpeed (rate);
  228. }
  229. return JS_UNDEFINED;
  230. }
  231. default:
  232. return JS_UNDEFINED;
  233. }
  234. }
  235. JSValue videotexture_instantiate (
  236. JSContext* ctx, WallpaperEngine::VideoPlayback::MPV::GLPlayer& player,
  237. WallpaperEngine::Scripting::ScriptEngine& engine
  238. ) {
  239. JSValue handle = JS_NewObject (ctx);
  240. JSValue data[] = {
  241. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&player))),
  242. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&engine))),
  243. };
  244. static constexpr struct {
  245. const char* name;
  246. int magic;
  247. int length;
  248. } calls[] = { { "play", 0, 0 }, { "pause", 1, 0 }, { "stop", 2, 0 },
  249. { "isPlaying", 3, 0 }, { "isPaused", 4, 0 }, { "isStopped", 5, 0 },
  250. { "getCurrentTime", 6, 0 }, { "setCurrentTime", 7, 1 }, { "addEndedCallback", 8, 1 } };
  251. for (const auto& call : calls) {
  252. JS_SetPropertyStr (
  253. ctx, handle, call.name, JS_NewCFunctionData (ctx, videotexture_call, call.length, call.magic, 2, data)
  254. );
  255. }
  256. static constexpr struct {
  257. const char* name;
  258. int getter;
  259. int setter;
  260. } properties[] = { { "loop", 9, 10 }, { "duration", 11, 14 }, { "rate", 12, 13 } };
  261. // "duration" gets an ignoring setter (14) since strict mode scripts throw on a property with none
  262. for (const auto& property : properties) {
  263. const JSAtom atom = JS_NewAtom (ctx, property.name);
  264. JS_DefinePropertyGetSet (
  265. ctx, handle, atom, JS_NewCFunctionData (ctx, videotexture_call, 0, property.getter, 2, data),
  266. JS_NewCFunctionData (ctx, videotexture_call, 1, property.setter, 2, data), JS_PROP_ENUMERABLE
  267. );
  268. JS_FreeAtom (ctx, atom);
  269. }
  270. return handle;
  271. }
  272. // scripts call this on image layers whose material is a video (mp4 texture), everything else gets null
  273. JSValue scriptableobject_video_texture_call (
  274. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  275. ) {
  276. int64_t objectAddress = 0;
  277. int64_t engineAddress = 0;
  278. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  279. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  280. auto* object
  281. = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  282. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  283. if (!object->is<WallpaperEngine::Render::Objects::CImage> ()) {
  284. return JS_NULL;
  285. }
  286. const auto texture = object->as<WallpaperEngine::Render::Objects::CImage> ()->getTexture ();
  287. if (texture == nullptr || texture->getPlayer () == nullptr) {
  288. return JS_NULL;
  289. }
  290. return videotexture_instantiate (ctx, *texture->getPlayer (), *engine);
  291. }
  292. namespace {
  293. enum BoneCall {
  294. GetBoneCount,
  295. GetBoneTransform,
  296. SetBoneTransform,
  297. GetLocalBoneTransform,
  298. SetLocalBoneTransform,
  299. GetLocalBoneAngles,
  300. SetLocalBoneAngles,
  301. GetLocalBoneOrigin,
  302. SetLocalBoneOrigin,
  303. GetBoneIndex,
  304. GetBoneParentIndex,
  305. ApplyBonePhysicsImpulse,
  306. ResetBonePhysicsSimulation,
  307. };
  308. // WE's own Mat4 from baseclasses.js: m[i] is float i of the native matrix (scenescript64 sub_1816214A0)
  309. JSValue makeMat4 (JSContext* ctx, const WallpaperEngine::Scripting::ScriptEngine& engine, const glm::mat4& matrix) {
  310. JSValue prototype = JS_GetPropertyStr (ctx, engine.getGlobalThis (), "_Mat4");
  311. JSValue result = JS_IsObject (prototype) ? JS_NewObjectProto (ctx, prototype) : JS_NewObject (ctx);
  312. JS_FreeValue (ctx, prototype);
  313. JSValue values = JS_NewArray (ctx);
  314. const float* floats = glm::value_ptr (matrix);
  315. for (uint32_t i = 0; i < 16; i++) {
  316. JS_SetPropertyUint32 (ctx, values, i, JS_NewFloat64 (ctx, floats[i]));
  317. }
  318. JS_SetPropertyStr (ctx, result, "m", values);
  319. return result;
  320. }
  321. // elements that aren't numbers keep the identity's value, like sub_1816214A0
  322. glm::mat4 readMat4 (JSContext* ctx, JSValueConst value) {
  323. glm::mat4 result (1.0f);
  324. if (!JS_IsObject (value)) {
  325. return result;
  326. }
  327. JSValue values = JS_GetPropertyStr (ctx, value, "m");
  328. float* floats = glm::value_ptr (result);
  329. if (JS_IsObject (values)) {
  330. for (uint32_t i = 0; i < 16; i++) {
  331. JSValue element = JS_GetPropertyUint32 (ctx, values, i);
  332. double number = 0.0;
  333. if (JS_IsNumber (element) && JS_ToFloat64 (ctx, &number, element) == 0) {
  334. floats[i] = static_cast<float> (number);
  335. }
  336. JS_FreeValue (ctx, element);
  337. }
  338. }
  339. JS_FreeValue (ctx, values);
  340. return result;
  341. }
  342. glm::vec3 readVec3 (JSContext* ctx, JSValueConst value) {
  343. glm::vec3 result (0.0f);
  344. if (!JS_IsObject (value)) {
  345. return result;
  346. }
  347. const char* names[] = { "x", "y", "z" };
  348. for (int i = 0; i < 3; i++) {
  349. JSValue component = JS_GetPropertyStr (ctx, value, names[i]);
  350. double number = 0.0;
  351. if (JS_IsNumber (component) && JS_ToFloat64 (ctx, &number, component) == 0) {
  352. result[i] = static_cast<float> (number);
  353. }
  354. JS_FreeValue (ctx, component);
  355. }
  356. return result;
  357. }
  358. // getLocalBoneAngles (sub_14020FA10), on WE's row-major floats
  359. glm::vec3 localBoneAngles (const glm::mat4& local) {
  360. const float* m = glm::value_ptr (local);
  361. const float z = std::atan2 (m[1], m[0]);
  362. const float y = std::atan2 (-m[2], std::sqrt (m[6] * m[6] + m[10] * m[10]));
  363. const float sz = std::sin (z);
  364. const float cz = std::cos (z);
  365. const float x = std::atan2 (sz * m[8] - cz * m[9], cz * m[5] - sz * m[4]);
  366. return { x, y, z };
  367. }
  368. // setLocalBoneAngles (sub_14020FCE0): the rotation rows are replaced, scale dropped, the origin kept
  369. void setLocalBoneAngles (glm::mat4& local, const glm::vec3& angles) {
  370. const float cx = std::cos (angles.x), sx = std::sin (angles.x);
  371. const float cy = std::cos (angles.y), sy = std::sin (angles.y);
  372. const float cz = std::cos (angles.z), sz = std::sin (angles.z);
  373. float* m = glm::value_ptr (local);
  374. m[0] = cy * cz;
  375. m[1] = cy * sz;
  376. m[2] = -sy;
  377. m[3] = 0.0f;
  378. m[4] = sy * cz * sx - cx * sz;
  379. m[5] = sy * sz * sx + cx * cz;
  380. m[6] = sx * cy;
  381. m[7] = 0.0f;
  382. m[8] = cx * cz * sy + sx * sz;
  383. m[9] = cx * sz * sy - sx * cz;
  384. m[10] = cx * cy;
  385. m[11] = 0.0f;
  386. }
  387. } // namespace
  388. // thisLayer bone calls (wallpaper64 2.8.42 image methods, sub_140211070). A bone is a number (index) or a string
  389. // (name); anything else does nothing, like a failed lookup
  390. JSValue scriptableobject_bone_call (
  391. JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
  392. ) {
  393. using WallpaperEngine::Render::Objects::CImage;
  394. int64_t objectAddress = 0;
  395. int64_t engineAddress = 0;
  396. JS_ToInt64 (ctx, &objectAddress, func_data[0]);
  397. JS_ToInt64 (ctx, &engineAddress, func_data[1]);
  398. auto* object
  399. = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
  400. auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
  401. // both index lookups write -1 before looking at the layer
  402. const JSValue fallback = magic == GetBoneIndex || magic == GetBoneParentIndex ? JS_NewInt32 (ctx, -1) : JS_UNDEFINED;
  403. if (!object->is<CImage> ()) {
  404. return fallback;
  405. }
  406. auto* image = object->as<CImage> ();
  407. const auto& bones = image->getPuppetBones ();
  408. const int count = static_cast<int> (bones.size ());
  409. if (count == 0) {
  410. return fallback;
  411. }
  412. if (magic == GetBoneCount) {
  413. return JS_NewInt32 (ctx, count);
  414. }
  415. const JSValueConst boneArgument = argc > 0 ? argv[0] : JS_UNDEFINED;
  416. std::optional<int> index;
  417. std::optional<std::string> name;
  418. if (JS_IsNumber (boneArgument)) {
  419. int32_t value = 0;
  420. JS_ToInt32 (ctx, &value, boneArgument);
  421. index = value;
  422. } else if (JS_IsString (boneArgument)) {
  423. const char* value = JS_ToCString (ctx, boneArgument);
  424. if (value != nullptr) {
  425. name = value;
  426. JS_FreeCString (ctx, value);
  427. }
  428. }
  429. if (!index.has_value () && !name.has_value ()) {
  430. return fallback;
  431. }
  432. if (magic == GetBoneIndex) {
  433. // names only, an index argument or an empty name finds nothing
  434. if (!name.has_value () || name->empty ()) {
  435. return fallback;
  436. }
  437. return JS_NewInt32 (ctx, image->findPuppetBone (*name));
  438. }
  439. if (magic == GetBoneParentIndex) {
  440. // by name the first bone of that name that has a parent (sub_140210860)
  441. for (int bone = 0; bone < count; bone++) {
  442. const bool matches = index.has_value () ? bone == *index : !name->empty () && bones[bone].name == *name;
  443. if (matches && bones[bone].parent != -1) {
  444. return JS_NewInt32 (ctx, bones[bone].parent);
  445. }
  446. }
  447. return fallback;
  448. }
  449. // an empty name is the first bone here (sub_140210990, sub_140210E10), the other calls need a match
  450. int bone = -1;
  451. if (index.has_value ()) {
  452. bone = *index;
  453. } else if (magic == ApplyBonePhysicsImpulse || magic == ResetBonePhysicsSimulation) {
  454. bone = name->empty () ? 0 : image->findPuppetBone (*name);
  455. } else if (!name->empty ()) {
  456. bone = image->findPuppetBone (*name);
  457. }
  458. if (bone < 0 || bone >= count) {
  459. return fallback;
  460. }
  461. if (magic == ApplyBonePhysicsImpulse) {
  462. image->applyPuppetBonePhysicsImpulse (
  463. bone, readVec3 (ctx, argc > 1 ? argv[1] : JS_UNDEFINED), readVec3 (ctx, argc > 2 ? argv[2] : JS_UNDEFINED)
  464. );
  465. return JS_UNDEFINED;
  466. }
  467. if (magic == ResetBonePhysicsSimulation) {
  468. image->resetPuppetBonePhysics (bone);
  469. return JS_UNDEFINED;
  470. }
  471. // the matrices exist from the layer's first update on
  472. if (!image->hasPuppetPose ()) {
  473. return fallback;
  474. }
  475. const JSValueConst value = argc > 1 ? argv[1] : JS_UNDEFINED;
  476. const auto makeVec3 = [engine] (const glm::vec3& vector) {
  477. WallpaperEngine::Data::Model::DynamicValue dynamic (vector);
  478. return engine->getAdapters ().vec3->instantiate (dynamic);
  479. };
  480. switch (magic) {
  481. case GetBoneTransform:
  482. return makeMat4 (ctx, *engine, image->getPuppetBoneTransform (bone));
  483. case SetBoneTransform:
  484. image->setPuppetBoneTransform (bone, readMat4 (ctx, value));
  485. break;
  486. case GetLocalBoneTransform:
  487. return makeMat4 (ctx, *engine, image->getPuppetLocalBoneTransform (bone));
  488. case SetLocalBoneTransform:
  489. image->setPuppetLocalBoneTransform (bone, readMat4 (ctx, value));
  490. break;
  491. case GetLocalBoneAngles:
  492. return makeVec3 (localBoneAngles (image->getPuppetLocalBoneTransform (bone)));
  493. case SetLocalBoneAngles: {
  494. glm::mat4 local = image->getPuppetLocalBoneTransform (bone);
  495. setLocalBoneAngles (local, readVec3 (ctx, value));
  496. image->setPuppetLocalBoneTransform (bone, local);
  497. break;
  498. }
  499. case GetLocalBoneOrigin:
  500. return makeVec3 (glm::vec3 (image->getPuppetLocalBoneTransform (bone)[3]));
  501. case SetLocalBoneOrigin: {
  502. // sub_140210250: floats 12..14, the rest stays
  503. glm::mat4 local = image->getPuppetLocalBoneTransform (bone);
  504. local[3] = glm::vec4 (readVec3 (ctx, value), local[3].w);
  505. image->setPuppetLocalBoneTransform (bone, local);
  506. break;
  507. }
  508. default:
  509. break;
  510. }
  511. return JS_UNDEFINED;
  512. }
  513. JSValue scriptableobject_property_get (JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst receiver) {
  514. JSClassID classId = 0;
  515. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
  516. if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  517. return JS_ThrowTypeError (ctx, "scriptableobject_property_get: not a layer");
  518. }
  519. const char* name = JS_AtomToCString (ctx, atom);
  520. if (name == nullptr) {
  521. return JS_ThrowTypeError (ctx, "scriptableobject_property_get: invalid property name");
  522. }
  523. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  524. if (auto* property = container->object.tryGetProperty (name); property != nullptr) {
  525. return container->adapter.getEngine ().propertyToJs (*property, name);
  526. }
  527. static constexpr struct {
  528. const char* name;
  529. int magic;
  530. } playbackCalls[] = { { "play", 0 }, { "pause", 1 }, { "stop", 3 }, { "isPlaying", 2 } };
  531. for (const auto& call : playbackCalls) {
  532. if (std::strcmp (name, call.name) == 0) {
  533. JSValue address[]
  534. = { JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))) };
  535. return JS_NewCFunctionData (ctx, scriptableobject_playback_call, 0, call.magic, 1, address);
  536. }
  537. }
  538. static constexpr struct {
  539. const char* name;
  540. int magic;
  541. } hierarchyCalls[] = { { "getParent", 0 }, { "getChildren", 1 } };
  542. for (const auto& call : hierarchyCalls) {
  543. if (std::strcmp (name, call.name) == 0) {
  544. JSValue data[] = {
  545. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  546. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  547. };
  548. return JS_NewCFunctionData (ctx, scriptableobject_hierarchy_call, 0, call.magic, 2, data);
  549. }
  550. }
  551. static constexpr struct {
  552. const char* name;
  553. int magic;
  554. } effectCalls[] = { { "getEffectCount", 0 }, { "getEffect", 1 } };
  555. for (const auto& call : effectCalls) {
  556. if (std::strcmp (name, call.name) == 0) {
  557. JSValue data[] = {
  558. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  559. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  560. };
  561. return JS_NewCFunctionData (ctx, scriptableobject_effect_call, 1, call.magic, 2, data);
  562. }
  563. }
  564. static constexpr struct {
  565. const char* name;
  566. int magic;
  567. int length;
  568. } boneCalls[] = {
  569. { "getBoneCount", GetBoneCount, 0 },
  570. { "getBoneTransform", GetBoneTransform, 1 },
  571. { "setBoneTransform", SetBoneTransform, 2 },
  572. { "getLocalBoneTransform", GetLocalBoneTransform, 1 },
  573. { "setLocalBoneTransform", SetLocalBoneTransform, 2 },
  574. { "getLocalBoneAngles", GetLocalBoneAngles, 1 },
  575. { "setLocalBoneAngles", SetLocalBoneAngles, 2 },
  576. { "getLocalBoneOrigin", GetLocalBoneOrigin, 1 },
  577. { "setLocalBoneOrigin", SetLocalBoneOrigin, 2 },
  578. { "getBoneIndex", GetBoneIndex, 1 },
  579. { "getBoneParentIndex", GetBoneParentIndex, 1 },
  580. { "applyBonePhysicsImpulse", ApplyBonePhysicsImpulse, 3 },
  581. { "resetBonePhysicsSimulation", ResetBonePhysicsSimulation, 1 },
  582. };
  583. for (const auto& call : boneCalls) {
  584. if (std::strcmp (name, call.name) == 0) {
  585. JSValue data[] = {
  586. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  587. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  588. };
  589. return JS_NewCFunctionData (ctx, scriptableobject_bone_call, call.length, call.magic, 2, data);
  590. }
  591. }
  592. if (std::strcmp (name, "getVideoTexture") == 0) {
  593. JSValue data[] = {
  594. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->object))),
  595. JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&container->adapter.getEngine ()))),
  596. };
  597. return JS_NewCFunctionData (ctx, scriptableobject_video_texture_call, 0, 0, 2, data);
  598. }
  599. if (std::strcmp (name, "name") == 0) {
  600. return JS_NewString (ctx, container->object.getObject ().name.c_str ());
  601. }
  602. if (std::strcmp (name, "id") == 0) {
  603. return JS_NewInt32 (ctx, container->object.getObject ().id);
  604. }
  605. // "size" isn't a DynamicValue-backed property, but thisLayer.size is a commonly used part
  606. // of the WE scripting API, so it's special-cased here. Checked against the data model
  607. // (Image/Text) rather than the render object (CImage/CText) because scripted properties can
  608. // run their first update() from inside the base ScriptableObject constructor, before the
  609. // derived render object has finished constructing - a dynamic_cast to it at that point would
  610. // incorrectly report "not yet that type".
  611. if (std::strcmp (name, "size") == 0) {
  612. const auto& modelObject = container->object.getObject ();
  613. const glm::vec2* size = nullptr;
  614. if (modelObject.is<Image> ()) {
  615. size = &modelObject.as<Image> ()->size;
  616. } else if (modelObject.is<Text> ()) {
  617. size = &modelObject.as<Text> ()->size;
  618. }
  619. if (size != nullptr) {
  620. const DynamicValue sizeValue (*size);
  621. return container->adapter.getEngine ().getAdapters ().vec2->instantiate (
  622. const_cast<DynamicValue&> (sizeValue), true
  623. );
  624. }
  625. }
  626. return JS_UNDEFINED;
  627. }
  628. int scriptableobject_property_set (
  629. JSContext* ctx, JSValueConst obj_val, JSAtom atom, JSValueConst val, JSValueConst receiver, int flags
  630. ) {
  631. JSClassID classId = 0;
  632. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (obj_val, &classId));
  633. if (!container || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  634. return -1;
  635. }
  636. const char* name = JS_AtomToCString (ctx, atom);
  637. if (name == nullptr) {
  638. return -1;
  639. }
  640. ScopeGuard guard ([=] { JS_FreeCString (ctx, name); });
  641. // Write through to the real property so `thisLayer.visible = ...` etc. actually takes
  642. // effect. Properties not backed by a DynamicValue (e.g. "horizontalalign", plain model
  643. // strings) fall through and return 0 rather than -1: since scripts run as strict-mode ES
  644. // modules, returning -1 here would throw and abort the whole script over an unsupported
  645. // property, so silently accepting the write is the safer default.
  646. if (auto* property = container->object.tryGetProperty (name); property != nullptr) {
  647. container->adapter.getEngine ().assignPropertyJsValue (val, *property, name);
  648. return 0;
  649. }
  650. if (std::strcmp (name, "name") == 0 || std::strcmp (name, "id") == 0 || std::strcmp (name, "size") == 0) {
  651. return 0;
  652. }
  653. // WE's layer objects are ordinary V8 objects, scripts hang their own state off them
  654. // (3378399626 keeps each widget's base origin on the layer). Own properties are found
  655. // before the exotic handlers, so later reads and writes never come back here.
  656. return JS_DefinePropertyValue (ctx, receiver, atom, JS_DupValue (ctx, val), JS_PROP_C_W_E);
  657. }
  658. ScriptableObjectAdapter::ScriptableObjectAdapter (ScriptEngine& engine, std::string name) :
  659. ObjectAdapter (engine),
  660. m_exoticMethods ({ .get_property = scriptableobject_property_get, .set_property = scriptableobject_property_set }),
  661. m_name (std::move (name)) {
  662. this->registerType (
  663. {
  664. .class_name = m_name.c_str (),
  665. .exotic = &m_exoticMethods,
  666. }
  667. );
  668. }
  669. JSValue ScriptableObjectAdapter::instantiate (ScriptableObject& object) {
  670. JSContext* ctx = this->getEngine ().getContext ();
  671. if (const auto it = this->m_instances.find (&object); it != this->m_instances.end ()) {
  672. return JS_DupValue (ctx, it->second);
  673. }
  674. JSValue result = this->ObjectAdapter::instantiate (object);
  675. JS_SetOpaque (
  676. result,
  677. new OpaqueScriptableObjectAdapter { .magic = SCRIPTABLE_OPAQUE_MAGIC, .adapter = *this, .object = object }
  678. );
  679. this->m_instances.emplace (&object, JS_DupValue (ctx, result));
  680. return result;
  681. }
  682. void ScriptableObjectAdapter::forget (const ScriptableObject& object) {
  683. if (const auto it = this->m_instances.find (&object); it != this->m_instances.end ()) {
  684. JS_FreeValue (this->getEngine ().getContext (), it->second);
  685. this->m_instances.erase (it);
  686. }
  687. }
  688. void ScriptableObjectAdapter::clear () {
  689. for (const auto& value : this->m_instances | std::views::values) {
  690. JS_FreeValue (this->getEngine ().getContext (), value);
  691. }
  692. this->m_instances.clear ();
  693. }
  694. JSValue ScriptableObjectAdapter::instantiate (DynamicValue& value) {
  695. throw std::runtime_error ("Cannot create a ScriptableObject instance from a DynamicValue");
  696. }
  697. WallpaperEngine::Scripting::ScriptableObject* ScriptableObjectAdapter::getObject (JSValueConst value) {
  698. JSClassID classId = 0;
  699. auto* container = static_cast<OpaqueScriptableObjectAdapter*> (JS_GetAnyOpaque (value, &classId));
  700. if (container == nullptr || container->magic != SCRIPTABLE_OPAQUE_MAGIC) {
  701. return nullptr;
  702. }
  703. return &container->object;
  704. }