diff --git a/lib/roblox_emulator/.gitignore b/lib/roblox_emulator/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/lib/roblox_emulator/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lib/roblox_emulator/Cargo.lock b/lib/roblox_emulator/Cargo.lock new file mode 100644 index 0000000..21f7633 --- /dev/null +++ b/lib/roblox_emulator/Cargo.lock @@ -0,0 +1,599 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "bstr" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glam" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "luau0-src" +version = "0.11.2+luau653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02313a53daf1fae25e82f7e7ca56180b72d1f08c514426672877cd957298201c" +dependencies = [ + "cc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mlua" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae9546e4a268c309804e8bbb7526e31cbfdedca7cd60ac1b987d0b212e0d876" +dependencies = [ + "bstr", + "either", + "libloading", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa6bf1a64f06848749b7e7727417f4ec2121599e2a10ef0a8a3888b0e9a5a0d" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rbx_dom_weak" +version = "2.9.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2a6b916687c98aaea36f9c03e80906bfafab057bebee248628c8c04def807f43" +dependencies = [ + "rbx_types", + "serde", +] + +[[package]] +name = "rbx_reflection" +version = "4.7.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "c1b43fe592a4ce6fe54eb215fb82735efbb516d2cc045a94e3dc0234ff293620" +dependencies = [ + "rbx_types", + "serde", + "thiserror", +] + +[[package]] +name = "rbx_reflection_database" +version = "0.2.12+roblox-638" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2e772bb9e1bc0ebe65d338f876d1bb1ea22e15a8f9a82e8245028010c2fea3c9" +dependencies = [ + "lazy_static", + "rbx_reflection", + "rmp-serde", + "serde", +] + +[[package]] +name = "rbx_types" +version = "1.10.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "d7a390c44034fa448c53bd0983dfc2d70d8d6b2f65be4f164d4bec8b6a2a2d09" +dependencies = [ + "base64", + "bitflags 1.3.2", + "blake3", + "lazy_static", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "roblox_emulator" +version = "0.4.7" +dependencies = [ + "glam", + "mlua", + "phf", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "rbx_types", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/lib/roblox_emulator/Cargo.toml b/lib/roblox_emulator/Cargo.toml new file mode 100644 index 0000000..3521348 --- /dev/null +++ b/lib/roblox_emulator/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "roblox_emulator" +version = "0.4.7" +edition = "2021" +repository = "https://git.itzana.me/StrafesNET/roblox_emulator" +license = "MIT OR Apache-2.0" +description = "Run embedded Luau scripts which manipulate the DOM." +authors = ["Rhys Lloyd "] + +[features] +default=["run-service"] +run-service=[] + +[dependencies] +glam = "0.29.0" +mlua = { version = "0.10.1", features = ["luau"] } +phf = { version = "0.11.2", features = ["macros"] } +rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" } +rbx_reflection = { version = "4.7.0", registry = "strafesnet" } +rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" } +rbx_types = { version = "1.10.0", registry = "strafesnet" } diff --git a/lib/roblox_emulator/LICENSE-APACHE b/lib/roblox_emulator/LICENSE-APACHE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/lib/roblox_emulator/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/lib/roblox_emulator/LICENSE-MIT b/lib/roblox_emulator/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/lib/roblox_emulator/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lib/roblox_emulator/README.md b/lib/roblox_emulator/README.md new file mode 100644 index 0000000..f488cb1 --- /dev/null +++ b/lib/roblox_emulator/README.md @@ -0,0 +1,19 @@ +Roblox Emulator +=============== + +## Run embedded Lua scripts which manipulate the DOM + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/lib/roblox_emulator/src/context.rs b/lib/roblox_emulator/src/context.rs new file mode 100644 index 0000000..92b1424 --- /dev/null +++ b/lib/roblox_emulator/src/context.rs @@ -0,0 +1,93 @@ +use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom}; + +pub fn class_is_a(class:&str,superclass:&str)->bool{ + class==superclass + ||rbx_reflection_database::get().classes.get(class) + .is_some_and(|descriptor| + descriptor.superclass.as_ref().is_some_and(|class_super| + class_is_a(class_super,superclass) + ) + ) +} + +#[repr(transparent)] +pub struct Context{ + pub(crate)dom:WeakDom, +} + +impl Context{ + pub const fn new(dom:WeakDom)->Self{ + Self{dom} + } + pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance,Services){ + let script=InstanceBuilder::new("Script") + .with_property("Source",rbx_types::Variant::String(source)); + let script_ref=script.referent(); + let mut context=Self::new(WeakDom::new( + InstanceBuilder::new("DataModel") + .with_child(script) + )); + let services=context.convert_into_place(); + (context,crate::runner::instance::Instance::new(script_ref),services) + } + pub fn from_ref(dom:&WeakDom)->&Context{ + unsafe{&*(dom as *const WeakDom as *const Context)} + } + pub fn from_mut(dom:&mut WeakDom)->&mut Context{ + unsafe{&mut *(dom as *mut WeakDom as *mut Context)} + } + /// Creates an iterator over all items of a particular class. + pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator+'a{ + self.dom.descendants().filter(|&instance| + class_is_a(instance.class.as_ref(),superclass) + ).map(|instance|instance.referent()) + } + pub fn scripts(&self)->Vec{ + self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect() + } + + pub fn find_services(&self)->Option{ + Some(Services{ + workspace:*self.dom.root().children().iter().find(|&&r| + self.dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace") + )?, + game:self.dom.root_ref(), + }) + } + pub fn convert_into_place(&mut self)->Services{ + //snapshot root instances + let children=self.dom.root().children().to_owned(); + + //insert services + let game=self.dom.root_ref(); + let terrain_bldr=InstanceBuilder::new("Terrain"); + let workspace=self.dom.insert(game, + InstanceBuilder::new("Workspace") + //Set Workspace.Terrain property equal to Terrain + .with_property("Terrain",terrain_bldr.referent()) + .with_child(terrain_bldr) + ); + { + //Lowercase and upper case workspace property! + let game=self.dom.root_mut(); + game.properties.insert("workspace".to_owned(),rbx_types::Variant::Ref(workspace)); + game.properties.insert("Workspace".to_owned(),rbx_types::Variant::Ref(workspace)); + } + self.dom.insert(game,InstanceBuilder::new("Lighting")); + + //transfer original root instances into workspace + for instance in children{ + self.dom.transfer_within(instance,workspace); + } + + Services{ + game, + workspace, + } + } +} + +pub struct Services{ + pub game:Ref, + pub workspace:Ref, +} diff --git a/lib/roblox_emulator/src/lib.rs b/lib/roblox_emulator/src/lib.rs new file mode 100644 index 0000000..fd38760 --- /dev/null +++ b/lib/roblox_emulator/src/lib.rs @@ -0,0 +1,7 @@ +pub mod runner; +pub mod context; +#[cfg(feature="run-service")] +pub(crate) mod scheduler; + +#[cfg(test)] +mod tests; diff --git a/lib/roblox_emulator/src/runner/cframe.rs b/lib/roblox_emulator/src/runner/cframe.rs new file mode 100644 index 0000000..bab3c3e --- /dev/null +++ b/lib/roblox_emulator/src/runner/cframe.rs @@ -0,0 +1,174 @@ +use super::vector3::Vector3; + +#[derive(Clone,Copy)] +pub struct CFrame(pub(crate)glam::Affine3A); + +impl CFrame{ + pub fn new( + x:f32,y:f32,z:f32, + xx:f32,yx:f32,zx:f32, + xy:f32,yy:f32,zy:f32, + xz:f32,yz:f32,zz:f32, + )->Self{ + Self(glam::Affine3A::from_mat3_translation( + glam::mat3( + glam::vec3(xx,yx,zx), + glam::vec3(xy,yy,zy), + glam::vec3(xz,yz,zz) + ), + glam::vec3(x,y,z) + )) + } + pub fn point(x:f32,y:f32,z:f32)->Self{ + Self(glam::Affine3A::from_translation(glam::vec3(x,y,z))) + } + pub fn angles(x:f32,y:f32,z:f32)->Self{ + Self(glam::Affine3A::from_mat3(glam::Mat3::from_euler(glam::EulerRot::YXZ,y,x,z))) + } +} + +fn vec3_to_glam(v:glam::Vec3A)->rbx_types::Vector3{ + rbx_types::Vector3::new(v.x,v.y,v.z) +} +fn vec3_from_glam(v:rbx_types::Vector3)->glam::Vec3A{ + glam::vec3a(v.x,v.y,v.z) +} + +impl Into for CFrame{ + fn into(self)->rbx_types::CFrame{ + rbx_types::CFrame::new( + vec3_to_glam(self.0.translation), + rbx_types::Matrix3::new( + vec3_to_glam(self.0.matrix3.x_axis), + vec3_to_glam(self.0.matrix3.y_axis), + vec3_to_glam(self.0.matrix3.z_axis), + ) + ) + } +} +impl From for CFrame{ + fn from(value:rbx_types::CFrame)->Self{ + CFrame(glam::Affine3A{ + matrix3:glam::mat3a( + vec3_from_glam(value.orientation.x), + vec3_from_glam(value.orientation.y), + vec3_from_glam(value.orientation.z), + ), + translation:vec3_from_glam(value.position) + }) + } +} + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let cframe_table=lua.create_table()?; + + //CFrame.new + cframe_table.raw_set("new", + lua.create_function(|_,tuple:( + mlua::Value,mlua::Value,Option, + Option,Option,Option, + Option,Option,Option, + Option,Option,Option, + )|match tuple{ + //CFrame.new(pos) + ( + mlua::Value::UserData(pos),mlua::Value::Nil,None, + None,None,None, + None,None,None, + None,None,None, + )=>{ + let pos:Vector3=pos.take()?; + Ok(CFrame::point(pos.0.x,pos.0.y,pos.0.z)) + }, + //TODO: CFrame.new(pos,look) + ( + mlua::Value::UserData(pos),mlua::Value::UserData(look),None, + None,None,None, + None,None,None, + None,None,None, + )=>{ + let _pos:Vector3=pos.take()?; + let _look:Vector3=look.take()?; + Err(mlua::Error::runtime("Not yet implemented")) + }, + //CFrame.new(x,y,z) + ( + mlua::Value::Number(x),mlua::Value::Number(y),Some(z), + None,None,None, + None,None,None, + None,None,None, + )=>Ok(CFrame::point(x as f32,y as f32,z)), + //CFrame.new(x,y,z,xx,yx,zx,xy,yy,zy,xz,yz,zz) + ( + mlua::Value::Number(x),mlua::Value::Number(y),Some(z), + Some(xx),Some(yx),Some(zx), + Some(xy),Some(yy),Some(zy), + Some(xz),Some(yz),Some(zz), + )=>Ok(CFrame::new(x as f32,y as f32,z, + xx,yx,zx, + xy,yy,zy, + xz,yz,zz, + )), + _=>Err(mlua::Error::runtime("Invalid arguments")) + })? + )?; + + //CFrame.Angles + cframe_table.raw_set("Angles", + lua.create_function(|_,(x,y,z):(f32,f32,f32)| + Ok(CFrame::angles(x,y,z)) + )? + )?; + + globals.set("CFrame",cframe_table)?; + + Ok(()) +} + +impl mlua::UserData for CFrame{ + fn add_fields>(fields:&mut F){ + //CFrame.p + fields.add_field_method_get("p",|_,this|Ok(Vector3(this.0.translation))); + } + + fn add_methods>(methods:&mut M){ + methods.add_method("components",|_,this,()|Ok(( + this.0.translation.x, + this.0.translation.y, + this.0.translation.z, + this.0.matrix3.x_axis.x, + this.0.matrix3.y_axis.x, + this.0.matrix3.z_axis.x, + this.0.matrix3.x_axis.y, + this.0.matrix3.y_axis.y, + this.0.matrix3.z_axis.y, + this.0.matrix3.x_axis.z, + this.0.matrix3.y_axis.z, + this.0.matrix3.z_axis.z, + ))); + methods.add_method("VectorToWorldSpace",|_,this,v:Vector3| + Ok(Vector3(this.0.transform_vector3a(v.0))) + ); + + //methods.add_meta_method(mlua::MetaMethod::Mul,|_,this,val:&Vector3|Ok(Vector3(this.0.matrix3*val.0+this.0.translation))); + methods.add_meta_function(mlua::MetaMethod::Mul,|_,(this,val):(Self,Self)|Ok(Self(this.0*val.0))); + methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self| + Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})", + this.0.translation.x, + this.0.translation.y, + this.0.translation.z, + this.0.matrix3.x_axis.x, + this.0.matrix3.y_axis.x, + this.0.matrix3.z_axis.x, + this.0.matrix3.x_axis.y, + this.0.matrix3.y_axis.y, + this.0.matrix3.z_axis.y, + this.0.matrix3.x_axis.z, + this.0.matrix3.y_axis.z, + this.0.matrix3.z_axis.z, + )) + ); + } +} + +type_from_lua_userdata!(CFrame); diff --git a/lib/roblox_emulator/src/runner/color3.rs b/lib/roblox_emulator/src/runner/color3.rs new file mode 100644 index 0000000..93b8c68 --- /dev/null +++ b/lib/roblox_emulator/src/runner/color3.rs @@ -0,0 +1,68 @@ +#[derive(Clone,Copy)] +pub struct Color3{ + r:f32, + g:f32, + b:f32, +} +impl Color3{ + pub const fn new(r:f32,g:f32,b:f32)->Self{ + Self{r,g,b} + } +} +impl Into for Color3{ + fn into(self)->rbx_types::Color3{ + rbx_types::Color3::new(self.r,self.g,self.b) + } +} + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let color3_table=lua.create_table()?; + + color3_table.raw_set("new", + lua.create_function(|_,(r,g,b):(f32,f32,f32)| + Ok(Color3::new(r,g,b)) + )? + )?; + color3_table.raw_set("fromRGB", + lua.create_function(|_,(r,g,b):(u8,u8,u8)| + Ok(Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0)) + )? + )?; + + globals.set("Color3",color3_table)?; + + Ok(()) +} +fn lerp(lhs:f32,rhs:f32,t:f32)->f32{ + lhs+(rhs-lhs)*t +} + +impl mlua::UserData for Color3{ + fn add_fields>(fields:&mut F){ + fields.add_field_method_get("r",|_,this|Ok(this.r)); + fields.add_field_method_set("r",|_,this,val|{ + this.r=val; + Ok(()) + }); + fields.add_field_method_get("g",|_,this|Ok(this.g)); + fields.add_field_method_set("g",|_,this,val|{ + this.g=val; + Ok(()) + }); + fields.add_field_method_get("b",|_,this|Ok(this.b)); + fields.add_field_method_set("b",|_,this,val|{ + this.b=val; + Ok(()) + }); + } + fn add_methods>(methods:&mut M){ + methods.add_method("Lerp",|_,this,(other,t):(Self,f32)| + Ok(Color3::new( + lerp(this.r,other.r,t), + lerp(this.g,other.g,t), + lerp(this.b,other.b,t), + )) + ) + } +} +type_from_lua_userdata!(Color3); diff --git a/lib/roblox_emulator/src/runner/color_sequence.rs b/lib/roblox_emulator/src/runner/color_sequence.rs new file mode 100644 index 0000000..819fa2e --- /dev/null +++ b/lib/roblox_emulator/src/runner/color_sequence.rs @@ -0,0 +1,31 @@ +#[derive(Clone,Copy)] +pub struct ColorSequence{} +impl ColorSequence{ + pub const fn new()->Self{ + Self{} + } +} +impl Into for ColorSequence{ + fn into(self)->rbx_types::ColorSequence{ + rbx_types::ColorSequence{ + keypoints:Vec::new() + } + } +} + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let number_sequence_table=lua.create_table()?; + + number_sequence_table.raw_set("new", + lua.create_function(|_,_:mlua::MultiValue| + Ok(ColorSequence::new()) + )? + )?; + + globals.set("ColorSequence",number_sequence_table)?; + + Ok(()) +} + +impl mlua::UserData for ColorSequence{} +type_from_lua_userdata!(ColorSequence); diff --git a/lib/roblox_emulator/src/runner/enum.rs b/lib/roblox_emulator/src/runner/enum.rs new file mode 100644 index 0000000..0b6bd47 --- /dev/null +++ b/lib/roblox_emulator/src/runner/enum.rs @@ -0,0 +1,63 @@ +use mlua::IntoLua; + +#[derive(Clone,Copy)] +pub struct Enum(u32); +#[derive(Clone,Copy)] +pub struct EnumItems; +#[derive(Clone,Copy)] +pub struct EnumItem<'a>{ + ed:&'a rbx_reflection::EnumDescriptor<'a>, +} + +impl Into for Enum{ + fn into(self)->rbx_types::Enum{ + rbx_types::Enum::from_u32(self.0) + } +} + +impl<'a> EnumItem<'a>{ + const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{ + Self{ed} + } +} + +pub fn set_globals(_lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + globals.set("Enum",EnumItems) +} + +impl mlua::UserData for EnumItem<'_>{ + fn add_fields>(_fields:&mut F){ + } + fn add_methods>(methods:&mut M){ + methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,val):(EnumItem<'_>,mlua::String)|{ + match this.ed.items.get(&*val.to_str()?){ + Some(&id)=>Enum(id).into_lua(lua), + None=>mlua::Value::Nil.into_lua(lua), + } + }); + } +} +type_from_lua_userdata_lua_lifetime!(EnumItem); + +impl mlua::UserData for EnumItems{ + fn add_fields>(_fields:&mut F){ + } + fn add_methods>(methods:&mut M){ + methods.add_meta_function(mlua::MetaMethod::Index,|lua,(_,val):(Self,mlua::String)|{ + let db=rbx_reflection_database::get(); + match db.enums.get(&*val.to_str()?){ + Some(ed)=>EnumItem::new(ed).into_lua(lua), + None=>mlua::Value::Nil.into_lua(lua), + } + }); + } +} +type_from_lua_userdata!(EnumItems); + +impl mlua::UserData for Enum{ + fn add_fields>(_fields:&mut F){ + } + fn add_methods>(_methods:&mut M){ + } +} +type_from_lua_userdata!(Enum); diff --git a/lib/roblox_emulator/src/runner/instance/instance.rs b/lib/roblox_emulator/src/runner/instance/instance.rs new file mode 100644 index 0000000..7dc1f46 --- /dev/null +++ b/lib/roblox_emulator/src/runner/instance/instance.rs @@ -0,0 +1,557 @@ +use std::collections::{hash_map::Entry,HashMap}; + +use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti}; +use rbx_types::Ref; +use rbx_dom_weak::{InstanceBuilder,WeakDom}; + +use crate::runner::vector3::Vector3; + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + //class functions store + lua.set_app_data(ClassMethodsStore::default()); + lua.set_app_data(InstanceValueStore::default()); + + let instance_table=lua.create_table()?; + + //Instance.new + instance_table.raw_set("new", + lua.create_function(|lua,(class_name,parent):(mlua::String,Option)|{ + let class_name_str=&*class_name.to_str()?; + let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?; + dom_mut(lua,|dom|{ + //TODO: Nil instances + Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str)))) + }) + })? + )?; + + globals.set("Instance",instance_table)?; + + Ok(()) +} + +// LMAO look at this function! +pub fn dom_mut(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result)->mlua::Result{ + let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?; + f(&mut *dom) +} + +fn coerce_float32(value:&mlua::Value)->Option{ + match value{ + &mlua::Value::Integer(i)=>Some(i as f32), + &mlua::Value::Number(f)=>Some(f as f32), + _=>None, + } +} + +fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{ + let mut full_name=instance.name.clone(); + let mut pref=instance.parent(); + while let Some(parent)=dom.get_by_ref(pref){ + full_name.insert(0,'.'); + full_name.insert_str(0,parent.name.as_str()); + pref=parent.parent(); + } + full_name +} +//helper function for script +pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{ + dom_mut(lua,|dom|{ + let instance=script.get(dom)?; + let source=match instance.properties.get("Source"){ + Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(), + _=>Err(mlua::Error::external("Missing script.Source"))?, + }; + Ok((get_full_name(dom,instance),source)) + }) +} + +pub fn find_first_child<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{ + instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name) +} +pub fn find_first_descendant<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{ + dom.descendants_of(instance.referent()).find(|&inst|inst.name==name) +} + +pub fn find_first_child_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{ + instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.class==class) +} +pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{ + dom.descendants_of(instance.referent()).find(|&inst|inst.class==class) +} + +#[derive(Clone,Copy)] +pub struct Instance{ + referent:Ref, +} +impl Instance{ + pub const fn new(referent:Ref)->Self{ + Self{referent} + } + pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{ + dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing")) + } + pub fn get_mut<'a>(&self,dom:&'a mut WeakDom)->mlua::Result<&'a mut rbx_dom_weak::Instance>{ + dom.get_by_ref_mut(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing")) + } +} +type_from_lua_userdata!(Instance); + +//TODO: update rbx_reflection and use dom.superclasses_iter +pub struct SuperClassIter<'a> { + database: &'a rbx_reflection::ReflectionDatabase<'a>, + descriptor: Option<&'a rbx_reflection::ClassDescriptor<'a>>, +} +impl<'a> SuperClassIter<'a> { + fn next_descriptor(&self) -> Option<&'a rbx_reflection::ClassDescriptor<'a>> { + let superclass = self.descriptor?.superclass.as_ref()?; + self.database.classes.get(superclass) + } +} +impl<'a> Iterator for SuperClassIter<'a> { + type Item = &'a rbx_reflection::ClassDescriptor<'a>; + fn next(&mut self) -> Option { + let next_descriptor = self.next_descriptor(); + std::mem::replace(&mut self.descriptor, next_descriptor) + } +} + +impl mlua::UserData for Instance{ + fn add_fields>(fields:&mut F){ + fields.add_field_method_get("Parent",|lua,this|{ + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok(Instance::new(instance.parent())) + }) + }); + fields.add_field_method_set("Parent",|lua,this,val:Option|{ + let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?; + dom_mut(lua,|dom|{ + dom.transfer_within(this.referent,parent.referent); + Ok(()) + }) + }); + fields.add_field_method_get("Name",|lua,this|{ + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok(instance.name.clone()) + }) + }); + fields.add_field_method_set("Name",|lua,this,val:mlua::String|{ + dom_mut(lua,|dom|{ + let instance=this.get_mut(dom)?; + //Why does this need to be cloned? + instance.name=val.to_str()?.to_owned(); + Ok(()) + }) + }); + fields.add_field_method_get("ClassName",|lua,this|{ + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok(instance.class.clone()) + }) + }); + } + fn add_methods>(methods:&mut M){ + methods.add_method("GetChildren",|lua,this,_:()| + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + let children:Vec<_>=instance + .children() + .iter() + .copied() + .map(Instance::new) + .collect(); + Ok(children) + }) + ); + fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option))->mlua::Result>{ + let name_str=&*name.to_str()?; + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok( + match search_descendants.unwrap_or(false){ + true=>find_first_descendant(dom,instance,name_str), + false=>find_first_child(dom,instance,name_str), + } + .map(|instance| + Instance::new(instance.referent()) + ) + ) + }) + } + methods.add_method("FindFirstChild",ffc); + methods.add_method("WaitForChild",ffc); + methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option)|{ + let class_str=&*class.to_str()?; + dom_mut(lua,|dom|{ + Ok( + match search_descendants.unwrap_or(false){ + true=>find_first_descendant_of_class(dom,this.get(dom)?,class_str), + false=>find_first_child_of_class(dom,this.get(dom)?,class_str), + } + .map(|instance| + Instance::new(instance.referent()) + ) + ) + }) + }); + methods.add_method("GetDescendants",|lua,this,_:()| + dom_mut(lua,|dom|{ + let children:Vec<_>=dom + .descendants_of(this.referent) + .map(|instance| + Instance::new(instance.referent()) + ) + .collect(); + Ok(children) + }) + ); + methods.add_method("IsA",|lua,this,classname:mlua::String| + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?)) + }) + ); + methods.add_method("Destroy",|lua,this,()| + dom_mut(lua,|dom|{ + dom.destroy(this.referent); + Ok(()) + }) + ); + methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{ + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + Ok(instance.name.clone()) + }) + }); + methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{ + let index_str=&*index.to_str()?; + dom_mut(lua,|dom|{ + let instance=this.get(dom)?; + //println!("__index t={} i={index:?}",instance.name); + let db=rbx_reflection_database::get(); + let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?; + //Find existing property + match instance.properties.get(index_str) + .cloned() + //Find default value + .or_else(||db.find_default_property(class,index_str).cloned()) + //Find virtual property + .or_else(||{ + SuperClassIter{ + database:db, + descriptor:Some(class), + } + .find_map(|class| + find_virtual_property(&instance.properties,class,index_str) + ) + }) + { + Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua), + Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua), + Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua), + Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua), + Some(rbx_types::Variant::Ref(val))=>return Instance::new(val).into_lua(lua), + Some(rbx_types::Variant::CFrame(cf))=>return Into::::into(cf).into_lua(lua), + Some(rbx_types::Variant::Vector3(v))=>return Into::::into(v).into_lua(lua), + None=>(), + other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))), + } + //find a function with a matching name + if let Some(function)=class_methods_store_mut(lua,|cf|{ + let mut iter=SuperClassIter{ + database:db, + descriptor:Some(class), + }; + iter.find_map(|class|{ + let mut class_methods=cf.get_or_create_class_methods(&class.name)?; + class_methods.get_or_create_function(lua,index_str) + .transpose() + }).transpose() + })?{ + return function.into_lua(lua); + } + + //find or create an associated userdata object + if let Some(value)=instance_value_store_mut(lua,|ivs|{ + //TODO: walk class tree somehow + match ivs.get_or_create_instance_values(&instance){ + Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str), + None=>Ok(None) + } + })?{ + return value.into_lua(lua); + } + //find a child with a matching name + find_first_child(dom,instance,index_str) + .map(|instance|Instance::new(instance.referent())) + .into_lua(lua) + }) + }); + methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{ + dom_mut(lua,|dom|{ + let instance=this.get_mut(dom)?; + //println!("__newindex t={} i={index:?} v={value:?}",instance.name); + let index_str=&*index.to_str()?; + let db=rbx_reflection_database::get(); + let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?; + let mut iter=SuperClassIter{ + database:db, + descriptor:Some(class), + }; + let property=iter.find_map(|cls|cls.properties.get(index_str)).ok_or_else(||mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)))?; + match &property.data_type{ + rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{ + let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Vector3(typed_value.into())); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{ + let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Float32(typed_value)); + }, + rbx_reflection::DataType::Enum(enum_name)=>{ + let typed_value=match &value{ + &mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)), + &mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)), + mlua::Value::String(s)=>{ + let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?; + Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?)) + }, + mlua::Value::UserData(any_user_data)=>{ + let e:crate::runner::r#enum::Enum=*any_user_data.borrow()?; + Ok(e.into()) + }, + _=>Err(mlua::Error::runtime("Expected Enum")), + }?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Enum(typed_value)); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{ + let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Color3(typed_value.into())); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{ + let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Bool(typed_value)); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{ + let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::String(typed_value.to_owned())); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{ + let typed_value:crate::runner::number_sequence::NumberSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::NumberSequence(typed_value.into())); + }, + rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{ + let typed_value:crate::runner::color_sequence::ColorSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?; + instance.properties.insert(index_str.to_owned(),rbx_types::Variant::ColorSequence(typed_value.into())); + }, + other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))), + } + Ok(()) + }) + }); + } +} + +/// A class function definition shorthand. +macro_rules! cf{ + ($f:expr)=>{ + |lua,mut args|{ + let this=Instance::from_lua(args.pop_front().unwrap_or(mlua::Value::Nil),lua)?; + $f(lua,this,FromLuaMulti::from_lua_multi(args,lua)?)?.into_lua_multi(lua) + } + }; +} +type ClassFunctionPointer=fn(&mlua::Lua,mlua::MultiValue)->mlua::Result; +// TODO: use macros to define these with better organization +/// A double hash map of function pointers. +/// The class tree is walked by the Instance.__index metamethod to find available class methods. +type CFD=phf::Map<&'static str,// Class name + phf::Map<&'static str,// Method name + ClassFunctionPointer + > +>; +static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{ + "DataModel"=>phf::phf_map!{ + "GetService"=>cf!(|lua,_this,service:mlua::String|{ + dom_mut(lua,|dom|{ + //dom.root_ref()==this.referent ? + let service=&*service.to_str()?; + match service{ + "Lighting"|"RunService"=>{ + let referent=find_first_child_of_class(dom,dom.root(),service) + .map(|instance|instance.referent()) + .unwrap_or_else(|| + dom.insert(dom.root_ref(),InstanceBuilder::new(service)) + ); + Ok(Instance::new(referent)) + }, + other=>Err::(mlua::Error::runtime(format!("Service '{other}' not supported"))), + } + }) + }), + }, + "Terrain"=>phf::phf_map!{ + "FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(())) + }, +}; + +/// A store of created functions for each Roblox class. +/// Functions are created the first time they are accessed and stored in this data structure. +#[derive(Default)] +struct ClassMethodsStore{ + classes:HashMap<&'static str,//ClassName + HashMap<&'static str,//Method name + mlua::Function + > + > +} +impl ClassMethodsStore{ + /// return self.classes[class] or create the ClassMethods and then return it + fn get_or_create_class_methods(&mut self,class:&str)->Option{ + // Use get_entry to get the &'static str keys of the database + // and use it as a key for the classes hashmap + CLASS_FUNCTION_DATABASE.get_entry(class) + .map(|(&static_class_str,method_pointers)| + ClassMethods{ + method_pointers, + methods:self.classes.entry(static_class_str) + .or_insert_with(||HashMap::new()), + } + ) + } +} +struct ClassMethods<'a>{ + method_pointers:&'static phf::Map<&'static str,ClassFunctionPointer>, + methods:&'a mut HashMap<&'static str,mlua::Function>, +} +impl ClassMethods<'_>{ + /// return self.methods[index] or create the function in the hashmap and then return it + fn get_or_create_function(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result>{ + Ok(match self.method_pointers.get_entry(index){ + Some((&static_index_str,&function_pointer))=>Some( + match self.methods.entry(static_index_str){ + Entry::Occupied(entry)=>entry.get().clone(), + Entry::Vacant(entry)=>entry.insert( + lua.create_function(function_pointer)? + ).clone(), + } + ), + None=>None, + }) + } +} +fn class_methods_store_mut(lua:&mlua::Lua,mut f:impl FnMut(&mut ClassMethodsStore)->mlua::Result)->mlua::Result{ + let mut cf=lua.app_data_mut::().ok_or_else(||mlua::Error::runtime("ClassMethodsStore missing"))?; + f(&mut *cf) +} + +/// A virtual property pointer definition shorthand. +type VirtualPropertyFunctionPointer=fn(&rbx_types::Variant)->Option; +const fn vpp( + property:&'static str, + pointer:VirtualPropertyFunctionPointer, +)->VirtualProperty{ + VirtualProperty{ + property, + pointer, + } +} +struct VirtualProperty{ + property:&'static str,// Source property name + pointer:VirtualPropertyFunctionPointer, +} +type VPD=phf::Map<&'static str,// Class name + phf::Map<&'static str,// Virtual property name + VirtualProperty + > +>; +static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{ + "BasePart"=>phf::phf_map!{ + "Position"=>vpp("CFrame",|c:&rbx_types::Variant|{ + let c=match c{ + rbx_types::Variant::CFrame(c)=>c, + _=>return None,//fail silently and ungracefully + }; + Some(rbx_types::Variant::Vector3(c.position)) + }), + }, +}; + +fn find_virtual_property( + properties:&HashMap, + class:&rbx_reflection::ClassDescriptor, + index:&str +)->Option{ + //Find virtual property + let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?; + let virtual_property=class_virtual_properties.get(index)?; + + //Get source property + let variant=properties.get(virtual_property.property)?; + + //Transform Source property with provided function + (virtual_property.pointer)(variant) +} + +// lazy-loaded per-instance userdata values +// This whole thing is a bad idea and a garbage collection nightmare. +// TODO: recreate rbx_dom_weak with my own instance type that owns this data. +type CreateUserData=fn(&mlua::Lua)->mlua::Result; +type LUD=phf::Map<&'static str,// Class name + phf::Map<&'static str,// Value name + CreateUserData + > +>; +static LAZY_USER_DATA:LUD=phf::phf_map!{ + "RunService"=>phf::phf_map!{ + "RenderStepped"=>|lua|{ + lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new()) + }, + }, +}; +#[derive(Default)] +pub struct InstanceValueStore{ + values:HashMap + >, +} +pub struct InstanceValues<'a>{ + named_values:&'static phf::Map<&'static str,CreateUserData>, + values:&'a mut HashMap<&'static str,mlua::AnyUserData>, +} +impl InstanceValueStore{ + pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option{ + LAZY_USER_DATA.get(instance.class.as_str()) + .map(|named_values| + InstanceValues{ + named_values, + values:self.values.entry(instance.referent()) + .or_insert_with(||HashMap::new()), + } + ) + } +} +impl InstanceValues<'_>{ + pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result>{ + Ok(match self.named_values.get_entry(index){ + Some((&static_index_str,&function_pointer))=>Some( + match self.values.entry(static_index_str){ + Entry::Occupied(entry)=>entry.get().clone(), + Entry::Vacant(entry)=>entry.insert( + function_pointer(lua)? + ).clone(), + } + ), + None=>None, + }) + } +} + +pub fn instance_value_store_mut(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result)->mlua::Result{ + let mut cf=lua.app_data_mut::().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?; + f(&mut *cf) +} diff --git a/lib/roblox_emulator/src/runner/instance/mod.rs b/lib/roblox_emulator/src/runner/instance/mod.rs new file mode 100644 index 0000000..234af3d --- /dev/null +++ b/lib/roblox_emulator/src/runner/instance/mod.rs @@ -0,0 +1,2 @@ +pub mod instance; +pub use instance::Instance; diff --git a/lib/roblox_emulator/src/runner/macros.rs b/lib/roblox_emulator/src/runner/macros.rs new file mode 100644 index 0000000..180c020 --- /dev/null +++ b/lib/roblox_emulator/src/runner/macros.rs @@ -0,0 +1,24 @@ +macro_rules! type_from_lua_userdata{ + ($asd:ident)=>{ + impl mlua::FromLua for $asd{ + fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result{ + match value{ + mlua::Value::UserData(ud)=>Ok(*ud.borrow::()?), + other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))), + } + } + } + }; +} +macro_rules! type_from_lua_userdata_lua_lifetime{ + ($asd:ident)=>{ + impl mlua::FromLua for $asd<'static>{ + fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result{ + match value{ + mlua::Value::UserData(ud)=>Ok(*ud.borrow::()?), + other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))), + } + } + } + }; +} diff --git a/lib/roblox_emulator/src/runner/mod.rs b/lib/roblox_emulator/src/runner/mod.rs new file mode 100644 index 0000000..a208868 --- /dev/null +++ b/lib/roblox_emulator/src/runner/mod.rs @@ -0,0 +1,14 @@ +#[macro_use] +mod macros; +mod runner; + +mod r#enum; +mod color3; +mod cframe; +mod vector3; +pub mod instance; +mod script_signal; +mod color_sequence; +mod number_sequence; + +pub use runner::{Runner,Runnable,Error}; diff --git a/lib/roblox_emulator/src/runner/number_sequence.rs b/lib/roblox_emulator/src/runner/number_sequence.rs new file mode 100644 index 0000000..bfa25bb --- /dev/null +++ b/lib/roblox_emulator/src/runner/number_sequence.rs @@ -0,0 +1,31 @@ +#[derive(Clone,Copy)] +pub struct NumberSequence{} +impl NumberSequence{ + pub const fn new()->Self{ + Self{} + } +} +impl Into for NumberSequence{ + fn into(self)->rbx_types::NumberSequence{ + rbx_types::NumberSequence{ + keypoints:Vec::new() + } + } +} + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let number_sequence_table=lua.create_table()?; + + number_sequence_table.raw_set("new", + lua.create_function(|_,_:mlua::MultiValue| + Ok(NumberSequence::new()) + )? + )?; + + globals.set("NumberSequence",number_sequence_table)?; + + Ok(()) +} + +impl mlua::UserData for NumberSequence{} +type_from_lua_userdata!(NumberSequence); diff --git a/lib/roblox_emulator/src/runner/runner.rs b/lib/roblox_emulator/src/runner/runner.rs new file mode 100644 index 0000000..4b25bd6 --- /dev/null +++ b/lib/roblox_emulator/src/runner/runner.rs @@ -0,0 +1,143 @@ +use crate::context::Context; +#[cfg(feature="run-service")] +use crate::scheduler::scheduler_mut; + +pub struct Runner{ + lua:mlua::Lua, +} +#[derive(Debug)] +pub enum Error{ + Lua{ + source:String, + error:mlua::Error + }, + RustLua(mlua::Error), + NoServices, +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + match self{ + Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"), + Self::RustLua(error)=>write!(f,"rust-side lua error: {error}"), + other=>write!(f,"{other:?}"), + } + } +} +impl std::error::Error for Error{} + +fn init(lua:&mlua::Lua)->mlua::Result<()>{ + lua.sandbox(true)?; + + //global environment + let globals=lua.globals(); + + #[cfg(feature="run-service")] + crate::scheduler::set_globals(lua,&globals)?; + super::script_signal::set_globals(lua,&globals)?; + super::r#enum::set_globals(lua,&globals)?; + super::color3::set_globals(lua,&globals)?; + super::vector3::set_globals(lua,&globals)?; + super::cframe::set_globals(lua,&globals)?; + super::instance::instance::set_globals(lua,&globals)?; + super::number_sequence::set_globals(lua,&globals)?; + super::color_sequence::set_globals(lua,&globals)?; + + Ok(()) +} + +impl Runner{ + pub fn new()->Result{ + let runner=Self{ + lua:mlua::Lua::new(), + }; + init(&runner.lua).map_err(Error::RustLua)?; + Ok(runner) + } + pub fn runnable_context<'a>(self,context:&'a mut Context)->Result,Error>{ + let services=context.find_services().ok_or(Error::NoServices)?; + self.runnable_context_with_services(context,&services) + } + pub fn runnable_context_with_services<'a>(self,context:&'a mut Context,services:&crate::context::Services)->Result,Error>{ + { + let globals=self.lua.globals(); + globals.set("game",super::instance::Instance::new(services.game)).map_err(Error::RustLua)?; + globals.set("workspace",super::instance::Instance::new(services.workspace)).map_err(Error::RustLua)?; + } + //this makes set_app_data shut up about the lifetime + self.lua.set_app_data::<&'static mut rbx_dom_weak::WeakDom>(unsafe{core::mem::transmute(&mut context.dom)}); + #[cfg(feature="run-service")] + self.lua.set_app_data::(crate::scheduler::Scheduler::default()); + Ok(Runnable{ + lua:self.lua, + _lifetime:&std::marker::PhantomData + }) + } +} + +//Runnable is the same thing but has context set, which it holds the lifetime for. +pub struct Runnable<'a>{ + lua:mlua::Lua, + _lifetime:&'a std::marker::PhantomData<()> +} +impl Runnable<'_>{ + pub fn drop_context(self)->Runner{ + self.lua.remove_app_data::<&'static mut rbx_dom_weak::WeakDom>(); + #[cfg(feature="run-service")] + self.lua.remove_app_data::(); + Runner{ + lua:self.lua, + } + } + pub fn run_script(&self,script:super::instance::Instance)->Result<(),Error>{ + let (name,source)=super::instance::instance::get_name_source(&self.lua,script).map_err(Error::RustLua)?; + self.lua.globals().raw_set("script",script).map_err(Error::RustLua)?; + let f=self.lua.load(source.as_str()) + .set_name(name).into_function().map_err(Error::RustLua)?; + // TODO: set_environment without losing the ability to print from Lua + let thread=self.lua.create_thread(f).map_err(Error::RustLua)?; + thread.resume::(()).map_err(|error|Error::Lua{source,error})?; + // wait() is called from inside Lua and goes to a rust function that schedules the thread and then yields + // No need to schedule the thread here + Ok(()) + } + #[cfg(feature="run-service")] + pub fn has_scheduled_threads(&self)->Result{ + scheduler_mut(&self.lua,|scheduler| + Ok(scheduler.has_scheduled_threads()) + ) + } + #[cfg(feature="run-service")] + pub fn game_tick(&self)->Result<(),mlua::Error>{ + if let Some(threads)=scheduler_mut(&self.lua,|scheduler|Ok(scheduler.tick_threads()))?{ + for thread in threads{ + //TODO: return dt and total run time + let result=thread.resume::((1.0/30.0,0.0)) + .map_err(|error|Error::Lua{source:"source unavailable".to_owned(),error}); + match result{ + Ok(_)=>(), + Err(e)=>println!("game_tick Error: {e}"), + } + } + } + Ok(()) + } + #[cfg(feature="run-service")] + pub fn run_service_step(&self)->Result<(),mlua::Error>{ + let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{ + let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?; + super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{ + //unwrap because I trust my find_first_child_of_class function to + let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?; + let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?; + //let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?; + //let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?; + Ok(render_stepped) + }) + })?; + if let Some(render_stepped)=render_stepped{ + let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?; + signal.fire(&mlua::MultiValue::new()); + } + Ok(()) + } +} diff --git a/lib/roblox_emulator/src/runner/script_signal.rs b/lib/roblox_emulator/src/runner/script_signal.rs new file mode 100644 index 0000000..877f2b6 --- /dev/null +++ b/lib/roblox_emulator/src/runner/script_signal.rs @@ -0,0 +1,172 @@ +use std::{cell::RefCell,rc::Rc}; + +use mlua::UserDataFields; + +#[derive(Clone)] +struct FunctionList{ + functions:Vec, +} +impl FunctionList{ + pub fn new()->Self{ + Self{ + functions:Vec::new(), + } + } + // This eats the Lua error + pub fn fire(self,args:&mlua::MultiValue){ + // Make a copy of the list in case Lua attempts to modify it during the loop + for function in self.functions{ + //wee let's allocate for our function calls + if let Err(e)=function.call::(args.clone()){ + println!("Script Signal Error: {e}"); + } + } + } +} +#[derive(Clone)] +struct RcFunctionList{ + functions:Rc>, +} +impl RcFunctionList{ + pub fn new()->Self{ + Self{ + functions:Rc::new(RefCell::new(FunctionList::new())), + } + } + pub fn fire(&self,args:&mlua::MultiValue){ + // Make a copy of the list in case Lua attempts to modify it during the loop + self.functions.borrow().clone().fire(args) + } +} +#[derive(Clone)] +pub struct ScriptSignal{ + // Emulate the garbage roblox api. + // ScriptConnection should not exist. + // :Disconnect should be a method on ScriptSignal, and this would be avoided entirely. + connections:RcFunctionList, + once:RcFunctionList, + wait:Rc>>, +} +pub struct ScriptConnection{ + connection:RcFunctionList, + function:mlua::Function, +} +impl ScriptSignal{ + pub fn new()->Self{ + Self{ + connections:RcFunctionList::new(), + once:RcFunctionList::new(), + wait:Rc::new(RefCell::new(Vec::new())), + } + } + pub fn fire(&self,args:&mlua::MultiValue){ + self.connections.fire(args); + //Replace the FunctionList with an empty one and drop the borrow + let once=std::mem::replace(&mut *self.once.functions.borrow_mut(),FunctionList::new()); + once.fire(args); + //resume threads waiting for this signal + let threads=std::mem::replace(&mut *self.wait.borrow_mut(),Vec::new()); + for thread in threads{ + if let Err(e)=thread.resume::(args.clone()){ + println!("Script Signal thread resume Error: {e}"); + } + } + } + pub fn connect(&self,function:mlua::Function)->ScriptConnection{ + self.connections.functions.borrow_mut().functions.push(function.clone()); + ScriptConnection{ + connection:self.connections.clone(), + function, + } + } + pub fn once(&self,function:mlua::Function)->ScriptConnection{ + self.once.functions.borrow_mut().functions.push(function.clone()); + ScriptConnection{ + connection:self.once.clone(), + function, + } + } + pub fn wait(&self,thread:mlua::Thread){ + self.wait.borrow_mut().push(thread); + } +} +impl ScriptConnection{ + pub fn position(&self)->Option{ + self.connection.functions.borrow().functions.iter().position(|function|function==&self.function) + } +} + +impl mlua::UserData for ScriptSignal{ + fn add_methods>(methods:&mut M){ + methods.add_method("Connect",|_lua,this,f:mlua::Function| + Ok(this.connect(f)) + ); + methods.add_method("Once",|_lua,this,f:mlua::Function| + Ok(this.once(f)) + ); + // Fire is not allowed to be called from Lua + // methods.add_method("Fire",|_lua,this,args:mlua::MultiValue| + // Ok(this.fire(args)) + // ); + } +} +impl mlua::FromLua for ScriptSignal{ + fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result{ + match value{ + mlua::Value::UserData(ud)=>Ok(ud.borrow::()?.clone()), + other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(ScriptSignal),other))), + } + } +} + +impl mlua::UserData for ScriptConnection{ + fn add_fields>(fields:&mut F){ + fields.add_field_method_get("Connected",|_,this|{ + Ok(this.position().is_some()) + }); + } + fn add_methods>(methods:&mut M){ + methods.add_method("Disconnect",|_,this,_:()|{ + if let Some(index)=this.position(){ + this.connection.functions.borrow_mut().functions.remove(index); + } + Ok(()) + }); + } +} + +fn wait_thread(lua:&mlua::Lua,this:ScriptSignal)->Result<(),mlua::Error>{ + Ok(this.wait(lua.current_thread())) +} + +// This is used to avoid calling coroutine.yield from the rust side. +const LUA_WAIT:&str= +"local coroutine_yield=coroutine.yield +local wait_thread=wait_thread +return function(signal) + wait_thread(signal) + return coroutine_yield() +end"; + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let coroutine_table=globals.get::("coroutine")?; + let wait_thread=lua.create_function(wait_thread)?; + + //create wait function environment + let wait_env=lua.create_table()?; + wait_env.raw_set("coroutine",coroutine_table)?; + wait_env.raw_set("wait_thread",wait_thread)?; + + //construct wait function from Lua code + let wait=lua.load(LUA_WAIT) + .set_name("wait") + .set_environment(wait_env) + .call::(())?; + + lua.register_userdata_type::(|reg|{ + reg.add_field("Wait",wait); + mlua::UserData::register(reg); + })?; + + Ok(()) +} diff --git a/lib/roblox_emulator/src/runner/vector3.rs b/lib/roblox_emulator/src/runner/vector3.rs new file mode 100644 index 0000000..2e543de --- /dev/null +++ b/lib/roblox_emulator/src/runner/vector3.rs @@ -0,0 +1,82 @@ +#[derive(Clone,Copy)] +pub struct Vector3(pub(crate)glam::Vec3A); + +impl Vector3{ + pub const fn new(x:f32,y:f32,z:f32)->Self{ + Self(glam::vec3a(x,y,z)) + } +} + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let vector3_table=lua.create_table()?; + + //Vector3.new + vector3_table.raw_set("new", + lua.create_function(|_,(x,y,z):(f32,f32,f32)| + Ok(Vector3::new(x,y,z)) + )? + )?; + + globals.set("Vector3",vector3_table)?; + + Ok(()) +} + +impl Into for Vector3{ + fn into(self)->rbx_types::Vector3{ + rbx_types::Vector3::new(self.0.x,self.0.y,self.0.z) + } +} + +impl From for Vector3{ + fn from(value:rbx_types::Vector3)->Vector3{ + Vector3::new(value.x,value.y,value.z) + } +} + +impl mlua::UserData for Vector3{ + fn add_fields>(fields:&mut F){ + fields.add_field_method_get("magnitude",|_,this|Ok(this.0.length())); + fields.add_field_method_get("x",|_,this|Ok(this.0.x)); + fields.add_field_method_set("x",|_,this,val|{ + this.0.x=val; + Ok(()) + }); + fields.add_field_method_get("y",|_,this|Ok(this.0.y)); + fields.add_field_method_set("y",|_,this,val|{ + this.0.y=val; + Ok(()) + }); + fields.add_field_method_get("z",|_,this|Ok(this.0.z)); + fields.add_field_method_set("z",|_,this,val|{ + this.0.z=val; + Ok(()) + }); + } + + fn add_methods>(methods:&mut M){ + //methods.add_method("area",|_,this,()| Ok(this.length * this.width)); + + methods.add_meta_function(mlua::MetaMethod::Add,|_,(this,val):(Self,Self)|Ok(Self(this.0+val.0))); + methods.add_meta_function(mlua::MetaMethod::Div,|_,(this,val):(Self,mlua::Value)|{ + match val{ + mlua::Value::Integer(n)=>Ok(Self(this.0/(n as f32))), + mlua::Value::Number(n)=>Ok(Self(this.0/(n as f32))), + mlua::Value::UserData(ud)=>{ + let rhs:Vector3=ud.take()?; + Ok(Self(this.0/rhs.0)) + }, + other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector3 by {other:?}"))), + } + }); + methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self| + Ok(format!("Vector3.new({},{},{})", + this.0.x, + this.0.y, + this.0.z, + )) + ); + } +} + +type_from_lua_userdata!(Vector3); diff --git a/lib/roblox_emulator/src/scheduler.rs b/lib/roblox_emulator/src/scheduler.rs new file mode 100644 index 0000000..f665e9d --- /dev/null +++ b/lib/roblox_emulator/src/scheduler.rs @@ -0,0 +1,106 @@ +pub use tick::Tick; +mod tick{ + #[derive(Clone,Copy,Default,Hash,PartialEq,Eq,PartialOrd,Ord)] + pub struct Tick(u64); + impl std::ops::Add for Tick{ + type Output=Self; + fn add(self,rhs:u64)->Self::Output{ + Self(self.0+rhs) + } + } + impl std::ops::Sub for Tick{ + type Output=Self; + fn sub(self,rhs:u64)->Self::Output{ + Self(self.0-rhs) + } + } + impl std::ops::AddAssign for Tick{ + fn add_assign(&mut self,rhs:u64){ + self.0+=rhs; + } + } + impl std::ops::SubAssign for Tick{ + fn sub_assign(&mut self,rhs:u64){ + self.0-=rhs; + } + } +} +#[derive(Default)] +pub struct Scheduler{ + tick:Tick, + schedule:std::collections::HashMap>, +} + +impl Scheduler{ + pub fn has_scheduled_threads(&self)->bool{ + !self.schedule.is_empty() + } + pub fn schedule_thread(&mut self,delay:u64,thread:mlua::Thread){ + self.schedule.entry(self.tick+delay.max(1)) + .or_insert(Vec::new()) + .push(thread); + } + pub fn tick_threads(&mut self)->Option>{ + self.tick+=1; + self.schedule.remove(&self.tick) + } +} + +pub fn scheduler_mut(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::Scheduler)->mlua::Result)->mlua::Result{ + let mut scheduler=lua.app_data_mut::().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?; + f(&mut *scheduler) +} + +fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{ + let delay=match dt{ + mlua::Value::Integer(i)=>i.max(0) as u64*60, + mlua::Value::Number(f)=>{ + let delay=f.max(0.0)*60.0; + match delay.classify(){ + std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?, + // cases where the number is too large to schedule + std::num::FpCategory::Infinite=>return Ok(()), + std::num::FpCategory::Normal=>if (u64::MAX as f64)(), + } + delay as u64 + }, + mlua::Value::Nil=>0, + _=>Err(mlua::Error::runtime("Expected float"))?, + }; + scheduler_mut(lua,|scheduler|{ + scheduler.schedule_thread(delay.max(2),lua.current_thread()); + Ok(()) + }) +} + +// This is used to avoid calling coroutine.yield from the rust side. +const LUA_WAIT:&str= +"local coroutine_yield=coroutine.yield +local schedule_thread=schedule_thread +return function(dt) + schedule_thread(dt) + return coroutine_yield() +end"; + +pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ + let coroutine_table=globals.get::("coroutine")?; + let schedule_thread=lua.create_function(schedule_thread)?; + + //create wait function environment + let wait_env=lua.create_table()?; + wait_env.raw_set("coroutine",coroutine_table)?; + wait_env.raw_set("schedule_thread",schedule_thread)?; + + //construct wait function from Lua code + let wait=lua.load(LUA_WAIT) + .set_name("wait") + .set_environment(wait_env) + .call::(())?; + + globals.raw_set("wait",wait)?; + + Ok(()) +} diff --git a/lib/roblox_emulator/src/tests.rs b/lib/roblox_emulator/src/tests.rs new file mode 100644 index 0000000..e69de29