Compare commits

..

114 Commits

Author SHA1 Message Date
d0f7799448 debug 2025-05-14 18:16:07 -07:00
81c9e3470b physics: use Bounds 2025-05-14 18:15:26 -07:00
b45e02c487 integer: export Parity trait 2025-05-14 17:26:41 -07:00
8698ca4a7e integer: time bitshift operations 2025-05-14 17:26:41 -07:00
8f04953326 physics: simplify face crawler trait bound 2025-05-14 16:52:51 -07:00
ae81d8ceaf derive Debug for many structs 2025-05-14 16:52:51 -07:00
2ecaeb1615 physics: test_collision_small_mv 2025-05-14 13:51:14 -07:00
768cd4ad1a physics: deref can be coerced 2025-05-13 17:26:10 -07:00
708462441a physics: clean up PhysicsMesh generation 2025-05-13 16:08:51 -07:00
da3ab52fe0 physics: do not require complete_mesh as first submesh
This removes a silent assumption about the input meshes and moves the branching from submeshes() to complete_mesh()
2025-05-13 15:45:30 -07:00
20f3e79cde rbx_loader: anything that uses velocity property should not be a booster 2025-05-09 20:56:42 -07:00
217f7fd7c3 physics: recalculate acceleration in collision_{start|end}_intersect 2025-05-09 20:56:42 -07:00
1e4d98f386 physics: use scratch vector in vert_edges 2025-05-09 20:56:42 -07:00
71bce361e6 rbx_loader: default meshpart mesh to empty string 2025-05-08 16:02:22 -07:00
d36c184f7e rbx_loader: auto-scale union graphics to fit size 2025-05-08 15:33:37 -07:00
2c3f257f0e rbx_loader: move mesh size detection into mesh convert 2025-05-08 15:33:37 -07:00
9e7e115809 deferred_loader: load generic mesh 2025-05-08 15:31:32 -07:00
2ea60b07fe rbx_loader: default physics for unions 2025-05-08 15:31:32 -07:00
2fe884175e rbx_loader: export primitive cube info 2025-05-07 15:57:19 -07:00
9f570d0f3e rbx_loader: prepare union convert for default physics 2025-05-07 15:57:19 -07:00
6e6bafe719 use .entry() match in generate_models 2025-05-02 19:36:34 -07:00
5f0ddc2f28 stop using transmute and document unsafe replacement code 2025-04-27 15:16:57 -07:00
ba1c1ec8c6 roblox_emulator: move lifetime inside PhantomData, saving 8 bytes 2025-04-24 13:51:28 -07:00
67cafd8cbb update deps including Decal.TextureContent 2025-04-23 18:05:09 -07:00
ca88eb1cad roblox_emulator: convert numbers to string in Instance.__newindex 2025-04-23 17:28:08 -07:00
2bf34fd04c roblox_emulator: move coerce method onto CoerceEnum struct 2025-04-23 17:19:59 -07:00
b5da30fd9a silence some warnings 2025-04-23 16:07:28 -07:00
95fffbbf42 roblox_emulator: bump version 2025-04-23 16:04:03 -07:00
9a636d5b50 roblox_emulator: Terrain.FillCylinder 2025-04-23 15:52:46 -07:00
2c8954c2b4 roblox_emulator: add Instance.Destroy aliases 2025-04-23 15:52:46 -07:00
985a703811 roblox_emulator: add CFrame.new(nil) constructor 2025-04-23 15:43:59 -07:00
37dbe35222 roblox_emulator: coerce enums in Terrain functions 2025-04-23 15:41:31 -07:00
f0de24d161 roblox_emulator: more BrickColor constructors 2025-04-23 15:39:34 -07:00
7d5ff55803 roblox_emulator: add Terrain:FillBall stub 2025-04-23 15:27:24 -07:00
7fffe05751 roblox_emulator: Color3.FromRGB alias 2025-04-23 15:22:16 -07:00
2ac38efef6 roblox_emulator: ClickDetector.MouseClick stub 2025-04-23 15:03:10 -07:00
5881e593b2 roblox_emulator: implement Instance.GetFullName 2025-04-23 15:03:10 -07:00
64a4499544 roblox_emulator: export mlua error 2025-04-23 15:03:10 -07:00
fca9e1c325 roblox_emulator: implement Error trait for ServicesError 2025-04-23 15:03:10 -07:00
6a88003b09 roblox_emulator: CFrame.FromEulerAnglesXYZ aliases 2025-04-23 15:03:10 -07:00
08b358c192 roblox_emulator: Vector2 2025-04-23 14:19:18 -07:00
62d9bcff81 roblox_emulator: implement __newindex CFrame 2025-04-23 14:12:21 -07:00
08f9163605 roblox_emulator: Tween Create & Play stub 2025-04-23 14:07:52 -07:00
b507624d91 roblox_emulator: shove tick into task module 2025-04-23 13:56:47 -07:00
d8358ec25c roblox_emulator: create task module 2025-04-23 13:56:47 -07:00
e673a12beb roblox_emulator: support more Vector3.new argument variants 2025-04-23 13:56:47 -07:00
18269423a5 roblox_emulator: implement TweenInfo 2025-04-23 13:56:47 -07:00
1d82799400 roblox_emulator: implement CoerceEnum 2025-04-23 13:28:47 -07:00
3a9fdebb4d roblox_emulator: refactor Enums 2025-04-23 13:28:47 -07:00
f8ef17e3f5 roblox_emulator: TweenService:Create() stub 2025-04-23 12:23:35 -07:00
8b2f37d3d5 roblox_emulator: Sound:Play() stub 2025-04-23 12:23:21 -07:00
16dfe7524f roblox_emulator: deduplicate static_ustr 2025-04-23 12:11:02 -07:00
3d399635d7 roblox_emulator: refactor nil instances 2025-04-23 12:04:46 -07:00
8bfc201d1f roblox_emulator: fix EnumItems.GetEnumItems 2025-04-23 12:04:46 -07:00
bb8e131464 roblox_emulator: use extended instances 2025-04-23 11:42:11 -07:00
05bbe05979 use forked rbx-dom for extended instances 2025-04-23 11:17:16 -07:00
17a2199f36 roblox_emulator: add ScriptSignal:wait alias for :Wait 2025-04-23 02:12:31 -07:00
84db6503f9 roblox_emulator: implement EnumItem.__eq 2025-04-23 02:00:09 -07:00
5b5f356863 roblox_emulator: implement __index Enum 2025-04-23 01:54:18 -07:00
d07571519e roblox_emulator: remove EnumItem::new 2025-04-23 01:38:51 -07:00
6464343428 roblox_emulator: rename Enum stuff to somewhat match Roblox 2025-04-23 01:38:27 -07:00
534b45d7dd roblox_emulator: EnumItems:GetEnumItems & Enum.Name 2025-04-23 01:32:10 -07:00
3ff8ccf58d roblox_emulator: implement Instance event stubs 2025-04-23 01:17:08 -07:00
67d2a3e398 roblox_emulator: implement __newindex UDim2 2025-04-23 01:15:08 -07:00
c1f8a13888 roblox_emulator: implement Vector3.__mul 2025-04-23 01:15:08 -07:00
e11af82443 roblox_emulator: tweak From impls 2025-04-23 01:15:08 -07:00
5b1d5502d8 roblox_emulator: fixup CFrame 2025-04-23 01:01:30 -07:00
f48d86fe56 roblox_emulator: replace coerce_float32 with a special Number type 2025-04-23 01:01:30 -07:00
877964be1f roblox_emulator: implement case aliases for CFrame 2025-04-22 22:53:37 -07:00
62d983ef2a roblox_emulator: implement NumberRange 2025-04-22 22:49:57 -07:00
8b7a4ee8c1 roblox_emulator: fixup NumberSequence 2025-04-22 22:49:41 -07:00
7ebcfa7bc3 roblox_emulator: stub BasePart.Touched & TouchEnded 2025-04-22 22:29:36 -07:00
c98f53a151 roblox_emulator: implement IsAncestorOf & IsDescendantOf 2025-04-22 22:26:46 -07:00
94025b52e2 roblox_emulator: .parent alias to .Parent 2025-04-22 21:43:57 -07:00
213d2f3f6a roblox_emulator: Vector3.__sub + .Unit 2025-04-22 21:35:26 -07:00
438a22bd86 roblox_emulator: fixup CFrame 2025-04-22 21:33:02 -07:00
e6fb8437b3 roblox_emulator: implement __newindex Int32 2025-04-22 21:26:26 -07:00
8b6166f48e roblox_emulator: support Workspace & MaterialService via GetService 2025-04-22 21:26:26 -07:00
3f89f2c1c5 roblox_emulator: implement FindFirstChildWhichIsA 2025-04-22 21:19:24 -07:00
c6ebb179a1 roblox_emulator: use common function pointer for lazy user data 2025-04-22 21:06:18 -07:00
14fa450e3e roblox_emulator: implement Players service stub 2025-04-22 21:04:29 -07:00
448f40a5d8 roblox_emulator: stub Terrain.SetMaterialColor 2025-04-22 20:57:09 -07:00
4ecdcec17c roblox_emulator: implement Instance.Clone 2025-04-22 20:53:49 -07:00
261b88ada6 roblox_emulator: isA alias for IsA 2025-04-22 19:56:56 -07:00
5ea0a1b07d roblox_emulator: implement __newindex Instance 2025-04-22 19:42:54 -07:00
3b8d32913e roblox_emulator: implement __index Color3 2025-04-22 19:42:35 -07:00
3928431ac7 roblox_emulator: implement __index String 2025-04-22 19:29:06 -07:00
6ecf403a4f roblox_emulator: add :connect alias to ScriptSignal.Connect 2025-04-22 19:28:26 -07:00
1f7ee20c1e roblox_emulator: implement Stepped, Heartbeat for RunService 2025-04-22 19:22:01 -07:00
19c30c8701 roblox_emulator: implement :children alias for GetChildren 2025-04-22 19:18:46 -07:00
dabb25b3d3 roblox_emulator: use coerce_float32 in CFrame 2025-04-22 19:15:26 -07:00
78593200eb roblox_emulator: fixup Vector3 2025-04-22 19:15:04 -07:00
7649d30b55 roblox_emulator: do not take UserData 2025-04-22 19:11:24 -07:00
6af8dd3c1f roblox_emulator: implement upper case methods on Vector3 2025-04-22 18:52:55 -07:00
861a8afc47 roblox_emulator: remove Vector3 component setters 2025-04-22 18:52:24 -07:00
f0b2470039 roblox_emulator: implement :service alias for GetService 2025-04-22 18:44:39 -07:00
c0ad20d54c roblox_emulator: implement UDim & UDim2 2025-04-22 18:44:21 -07:00
9aea73e134 roblox_emulator: implement BrickColor 2025-04-22 18:22:48 -07:00
b34d3f89f9 roblox_emulator: implement __index bool 2025-04-22 18:12:47 -07:00
5600948d8a roblox_emulator: fix error message 2025-04-22 18:12:47 -07:00
d722bcb46f roblox_emulator: implement __newindex ContentId 2025-04-22 18:12:47 -07:00
081c95190f roblox_emulator: ignore disabled scripts and ModuleScripts 2025-04-22 18:02:58 -07:00
d1f4e2132f common: do a minute amount of error reporting 2025-04-22 17:47:07 -07:00
5317d96243 roblox_emulator: rename Instance constructors 2025-04-22 17:28:44 -07:00
8bb20ffe81 roblox_emulator: nil instances 2025-04-22 17:28:44 -07:00
03b16db10a roblox_emulator: use a type alias to minimize errors 2025-04-22 16:08:45 -07:00
f0d4915fba roblox_emulator: move services into Context 2025-04-22 15:31:56 -07:00
6e8da50b35 rbx_loader: tweak model read 2025-04-22 14:55:20 -07:00
615372aad5 rbx_loader: stronger Place & Model types 2025-04-22 14:55:20 -07:00
08d47b0f63 roblox_emulator: do not create ustr if there is going to be an error 2025-04-22 14:00:30 -07:00
08b5445838 roblox_emulator: fix subtle ustr bugs 2025-04-22 14:00:29 -07:00
fa07d16cf4 refactor some util functions to use new rbx-dom features 2025-04-18 11:17:03 -07:00
327688d79e rbx-dom: guard rail ustr footguns 2025-04-18 11:17:03 -07:00
1662d814ec rbx_loader: disable roblox_emulator run-service feature 2025-04-18 10:28:48 -07:00
50 changed files with 1765 additions and 848 deletions

115
Cargo.lock generated

@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom 0.2.15",
"getrandom 0.2.16",
"once_cell",
"version_check",
"zerocopy 0.7.35",
@ -360,9 +360,9 @@ checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]]
name = "blake3"
version = "1.8.1"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
@ -559,9 +559,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.36"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
@ -569,9 +569,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.36"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
@ -1094,9 +1094,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
@ -1409,7 +1409,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
"windows-core 0.61.0",
]
[[package]]
@ -1843,14 +1843,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
name = "libm"
version = "0.2.11"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
[[package]]
name = "libredox"
@ -2900,9 +2900,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@ -2992,7 +2992,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.15",
"getrandom 0.2.16",
]
[[package]]
@ -3098,9 +3098,9 @@ dependencies = [
[[package]]
name = "rbx_binary"
version = "1.1.0-sn3"
version = "1.1.0-sn4"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "f3f4cd69381a3631e117ee9542f5f8d9a2ab3b5dd8c7bdc7eb1680cc805233bc"
checksum = "dc304e66f18316b8ee8729cec639528c9c5182aacbef1f8967619bd8b2363be4"
dependencies = [
"ahash",
"log",
@ -3115,11 +3115,12 @@ dependencies = [
[[package]]
name = "rbx_dom_weak"
version = "3.1.0-sn1"
version = "3.1.0-sn4"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "c449e42d26c5af9d50e036219513e7a3e3bd0bc31fa32b74b82d9d4d481d74a5"
checksum = "5fcb44ffa83385055a42e5e499be3a4cce04b2ceb9af2b9825f70dcc4d2578ce"
dependencies = [
"ahash",
"mlua",
"rbx_types",
"serde",
"ustr",
@ -3148,9 +3149,9 @@ dependencies = [
[[package]]
name = "rbx_reflection_database"
version = "1.0.1+roblox-666"
version = "1.0.2+roblox-670"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea11f26cfddc57a136781baed2e68eda5a3aa0735152d1562d65b1909c8d65d"
checksum = "5349b19e5e94fbcaba7a52175263ab64011e0a13f17ff57729f2f560ccdec615"
dependencies = [
"lazy_static",
"rbx_reflection",
@ -3175,9 +3176,9 @@ dependencies = [
[[package]]
name = "rbx_xml"
version = "1.1.0-sn3"
version = "1.1.0-sn4"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "ce7d7ec62a3dc88179a61efa4827c36887a1b2bcbc258aec58de0c5149c565eb"
checksum = "08750f2f23366cda4a71d05c11c40b55bfeccd4bbc22037264b510c90809f7bd"
dependencies = [
"ahash",
"base64 0.13.1",
@ -3228,7 +3229,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
]
@ -3333,7 +3334,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
@ -3363,7 +3364,7 @@ dependencies = [
[[package]]
name = "roblox_emulator"
version = "0.4.7"
version = "0.5.0"
dependencies = [
"glam",
"mlua",
@ -4152,9 +4153,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.14"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes",
"futures-core",
@ -4358,9 +4359,9 @@ dependencies = [
[[package]]
name = "vbsp"
version = "0.8.2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75944cc43c7d4ebcb3bd97a875b4f3307d79328de16f8dcd57745cba926b784b"
checksum = "fdab0507169ff47ea95acab2d08d91c7a31455e272b134879757d19c87075cb1"
dependencies = [
"ahash",
"arrayvec",
@ -4870,7 +4871,7 @@ dependencies = [
"web-sys",
"wgpu-types",
"windows",
"windows-core",
"windows-core 0.58.0",
]
[[package]]
@ -4902,7 +4903,7 @@ version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
@ -4912,13 +4913,26 @@ version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
@ -4930,6 +4944,17 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
@ -4941,6 +4966,17 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-link"
version = "0.1.1"
@ -4995,6 +5031,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

@ -1,7 +1,10 @@
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use crate::model::{into_giga_time,GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
use core::ops::Bound;
#[derive(Debug)]
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<M>,GigaTime),
@ -27,17 +30,60 @@ impl<M:MeshQuery> CrawlResult<M>{
}
}
// TODO: move predict_collision_face_out algorithm in here or something
/// check_lower_bound
pub fn low<LhsNum,LhsDen,RhsNum,RhsDen,T>(lower_bound:&Bound<Ratio<LhsNum,LhsDen>>,dt:&Ratio<RhsNum,RhsDen>)->bool
where
RhsNum:Copy,
RhsDen:Copy,
LhsNum:Copy,
LhsDen:Copy,
LhsDen:strafesnet_common::integer::Parity,
RhsDen:strafesnet_common::integer::Parity,
LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
T:Ord+Copy,
{
match lower_bound{
Bound::Included(time)=>time.le_ratio(*dt),
Bound::Excluded(time)=>time.lt_ratio(*dt),
Bound::Unbounded=>true,
}
}
/// check_upper_bound
pub fn upp<LhsNum,LhsDen,RhsNum,RhsDen,T>(dt:&Ratio<LhsNum,LhsDen>,upper_bound:&Bound<Ratio<RhsNum,RhsDen>>)->bool
where
RhsNum:Copy,
RhsDen:Copy,
LhsNum:Copy,
LhsDen:Copy,
LhsDen:strafesnet_common::integer::Parity,
RhsDen:strafesnet_common::integer::Parity,
LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
T:Ord+Copy,
{
match upper_bound{
Bound::Included(time)=>dt.le_ratio(*time),
Bound::Excluded(time)=>dt.lt_ratio(*time),
Bound::Unbounded=>true,
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
M:std::fmt::Debug,
F:std::fmt::Display,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
M::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
fn next_transition(&self,mesh:&M,body:&Body,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss;
@ -47,11 +93,16 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//n=face.normal d=face.dot
//n.a t^2+n.v t+n.p-d==0
let (n,d)=mesh.face_nd(face_id);
println!("Face n={} d={}",n,d);
//TODO: use higher precision d value?
//use the mesh transform translation instead of baking it into the d value.
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
let low=low(&lower_bound,&dt);
let upp=upp(&dt,&upper_bound);
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
if low&&upp&&into{
upper_bound=Bound::Included(dt);
best_transition=Transition::Hit(face_id,dt);
break;
}
@ -65,8 +116,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//WARNING: precision is swept under the rug!
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
@ -84,11 +135,17 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
let d=n.dot(delta_pos).wrap_4();
println!("Edge Face={:?} boundary_n={} boundary_d={}",edge_face_id,n,d>>1);
//WARNING yada yada d *2
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
for dt in Fixed::<4,128>::zeroes2(d,n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
let low=low(&lower_bound,&dt);
let upp=upp(&dt,&upper_bound);
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
if low&&upp&&into{
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break;
}
@ -99,9 +156,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//vertex normal gets parity from vert index
let n=edge_n*(1-2*(i as i64));
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
}
@ -115,9 +172,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//edge is directed away from vertex, but we want the dot product to turn out negative
let n=-mesh.directed_edge_n(directed_edge_id);
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
@ -128,19 +185,16 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}
best_transition
}
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
pub fn crawl(mut self,mesh:&M,relative_body:&Body,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time));
let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time));
println!("crawl begin={self:?}");
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
let transition=self.next_transition(mesh,relative_body,lower_bound,upper_bound);
println!("transition={transition:?}");
match transition{
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Next(next_fev,next_time)=>(self,lower_bound)=(next_fev,Bound::Included(next_time)),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
}
}

@ -1,5 +1,5 @@
use std::collections::{HashSet,HashMap};
use core::ops::Range;
use core::ops::{Bound,RangeBounds};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@ -68,16 +68,17 @@ pub enum FEV<M:MeshQuery>{
}
//use Unit32 #[repr(C)] for map files
#[derive(Clone,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
struct Face{
normal:Planar64Vec3,
dot:Planar64,
}
#[derive(Debug)]
struct Vert(Planar64Vec3);
pub trait MeshQuery{
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
type Face:Copy+std::fmt::Debug;
type Edge:Copy+DirectedEdge+std::fmt::Debug;
type Vert:Copy+std::fmt::Debug;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
@ -97,18 +98,22 @@ pub trait MeshQuery{
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
}
#[derive(Debug)]
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
//verts:Vec<VertId>,
}
#[derive(Debug)]
struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top
}
#[derive(Debug)]
struct VertRefs{
faces:Vec<SubmeshFaceId>,
edges:Vec<SubmeshDirectedEdgeId>,
}
#[derive(Debug)]
pub struct PhysicsMeshData{
//this contains all real and virtual faces used in both the complete mesh and convex submeshes
//faces are sorted such that all faces that belong to the complete mesh appear first, and then
@ -118,6 +123,7 @@ pub struct PhysicsMeshData{
faces:Vec<Face>,//MeshFaceId indexes this list
verts:Vec<Vert>,//MeshVertId indexes this list
}
#[derive(Debug)]
pub struct PhysicsMeshTopology{
//mapping of local ids to PhysicsMeshData ids
faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list
@ -143,10 +149,12 @@ impl From<MeshId> for PhysicsMeshId{
pub struct PhysicsSubmeshId(u32);
pub struct PhysicsMesh{
data:PhysicsMeshData,
complete_mesh:PhysicsMeshTopology,
//Most objects in roblox maps are already convex, so the list length is 0
//as soon as the mesh is divided into 2 submeshes, the list length jumps to 2.
//length 1 is unnecessary since the complete mesh would be a duplicate of the only submesh, but would still function properly
// The complete mesh is unused at this time.
// complete_mesh:PhysicsMeshTopology,
// Submeshes are guaranteed to be convex and may contain
// "virtual" faces which are not part of the complete mesh.
// Physics calculations should never resolve to hitting
// a virtual face.
submeshes:Vec<PhysicsMeshTopology>,
}
impl PhysicsMesh{
@ -210,19 +218,24 @@ impl PhysicsMesh{
};
Self{
data,
complete_mesh:mesh_topology,
submeshes:Vec::new(),
// complete_mesh:mesh_topology.clone(),
submeshes:vec![mesh_topology],
}
}
pub fn unit_cylinder()->Self{
Self::unit_cube()
}
#[inline]
pub const fn complete_mesh(&self)->&PhysicsMeshTopology{
&self.complete_mesh
pub fn complete_mesh(&self)->&PhysicsMeshTopology{
// If there is exactly one submesh, then the complete mesh is identical to it.
if self.submeshes.len()==1{
self.submeshes.first().unwrap()
}else{
panic!("PhysicsMesh complete mesh is not known");
}
}
#[inline]
pub const fn complete_mesh_view(&self)->PhysicsMeshView{
pub fn complete_mesh_view(&self)->PhysicsMeshView{
PhysicsMeshView{
data:&self.data,
topology:self.complete_mesh(),
@ -230,12 +243,7 @@ impl PhysicsMesh{
}
#[inline]
pub fn submeshes(&self)->&[PhysicsMeshTopology]{
//the complete mesh is already a convex mesh when len()==0, len()==1 is invalid but will still work
if self.submeshes.len()==0{
std::slice::from_ref(&self.complete_mesh)
}else{
&self.submeshes.as_slice()
}
&self.submeshes
}
#[inline]
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView{
@ -313,14 +321,20 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
if mesh.unique_pos.len()==0{
return Err(PhysicsMeshError::ZeroVertices);
}
// An empty physics mesh is a waste of resources
if mesh.physics_groups.len()==0{
return Err(PhysicsMeshError::NoPhysicsGroups);
}
let verts=mesh.unique_pos.iter().copied().map(Vert).collect();
//TODO: fix submeshes
//flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id
//lower face_id points to upper face_id
//the same face is not allowed to be in multiple polygon groups
// because SubmeshFaceId -> CompleteMeshFaceId -> SubmeshFaceId is ambiguous
// when multiple SubmeshFaceId point to one MeshFaceId
let mut faces=Vec::new();
let mut face_id_from_face=HashMap::new();
let mut mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{
let mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{
//construct submesh
let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId
let mut submesh_verts=Vec::new();
@ -381,15 +395,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
};
let face_id=match face_id_from_face.get(&face){
Some(&face_id)=>face_id,
None=>{
let face_id=MeshFaceId::new(faces.len() as u32);
face_id_from_face.insert(face.clone(),face_id);
faces.push(face);
face_id
}
};
let face_id=*face_id_from_face.entry(face).or_insert_with(||{
let face_id=MeshFaceId::new(faces.len() as u32);
faces.push(face);
face_id
});
submesh_faces.push(face_id);
face_ref_guys.push(FaceRefEdges(face_edges));
}
@ -397,16 +407,16 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
PhysicsMeshTopology{
faces:submesh_faces,
verts:submesh_verts,
face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{
FaceRefs{edges:face_ref_guy.0}
face_topology:face_ref_guys.into_iter().map(|FaceRefEdges(edges)|{
FaceRefs{edges}
}).collect(),
edge_topology:edge_pool.edge_guys.into_iter().map(|(edge_ref_verts,edge_ref_faces)|
EdgeRefs{faces:edge_ref_faces.0,verts:edge_ref_verts.0}
edge_topology:edge_pool.edge_guys.into_iter().map(|(EdgeRefVerts(verts),EdgeRefFaces(faces))|
EdgeRefs{faces,verts}
).collect(),
vert_topology:vert_ref_guys.into_iter().map(|vert_ref_guy|
vert_topology:vert_ref_guys.into_iter().map(|VertRefGuy{edges,faces}|
VertRefs{
edges:vert_ref_guy.edges.into_iter().collect(),
faces:vert_ref_guy.faces.into_iter().collect(),
edges:edges.into_iter().collect(),
faces:faces.into_iter().collect(),
}
).collect(),
}
@ -416,12 +426,13 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
faces,
verts,
},
complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?,
// complete_mesh:None,
submeshes:mesh_topologies,
})
}
}
#[derive(Debug)]
pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
@ -458,6 +469,7 @@ impl MeshQuery for PhysicsMeshView<'_>{
}
}
#[derive(Debug)]
pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
@ -473,6 +485,7 @@ impl PhysicsMeshTransform{
}
}
#[derive(Debug)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
@ -598,6 +611,7 @@ pub enum MinkowskiFace{
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
@ -615,6 +629,10 @@ enum EV{
}
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
let r=(time-relative_to).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
}
impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
@ -730,44 +748,46 @@ impl MinkowskiMesh<'_>{
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values?
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
println!("@@@BEGIN SETUP@@@");
self.closest_fev_not_inside(relative_body.clone(),range.start_bound()).and_then(|fev|{
println!("@@@BEGIN REAL CRAWL@@@");
//continue forwards along the body parabola
fev.crawl(self,relative_body,start_time,time_limit).hit()
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
})
}
pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
//create an extrapolated body at time_limit
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body.clone();
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
})
}
pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better
use crate::face_crawler::{low,upp};
//no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case)
let start_time={
let r=(start_time-relative_body.time).to_ratio();
let start_time=range.start_bound().map(|&t|{
let r=(t-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
});
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time));
let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
@ -780,18 +800,19 @@ impl MinkowskiMesh<'_>{
//WARNING: truncated precision
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_edge=Some(directed_edge_id);
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=Bound::Included(dt);
best_edge=Some((directed_edge_id,dt));
break;
}
}
}
best_edge.map(|e|(e.as_undirected(),best_time))
best_edge
}
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
// Bound::Included means that the surface of the mesh is included in the mesh
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@ -929,10 +950,10 @@ impl MeshQuery for MinkowskiMesh<'_>{
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1|
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0|
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
@ -942,38 +963,43 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir
let v0f=self.mesh0.vert_faces(v0);
let v1f=self.mesh1.vert_faces(v1);
let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
let the_len=v0f.as_ref().len()+v1f.as_ref().len();
let v0f_thing=self.mesh0.vert_faces(v0);
let v1f_thing=self.mesh1.vert_faces(v1);
let v0f=v0f_thing.as_ref();
let v1f=v1f_thing.as_ref();
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// TODO: there's gotta be a better way to do this
//make a set of faces
let mut face_normals=Vec::with_capacity(the_len);
//add mesh0 faces as-is
face_normals.clone_from(&v0f_n);
// drop faces beyond v0f_n
face_normals.truncate(v0f.len());
// make a set of faces from mesh0's perspective
for face_n in &v1f_n{
//add reflected mesh1 faces
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
}
}
face_normals.clone_from(&v1f_n);
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
let mut face_normals=Vec::with_capacity(the_len);
face_normals.clone_from(&v1f_n);
// drop faces beyond v1f_n
face_normals.truncate(v1f.len());
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
}
}
@ -987,7 +1013,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
}
}
fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
let len=normals.len();
for i in 0..len-1{
for j in i+1..len{
@ -1013,6 +1039,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
#[test]
fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec()));
assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec()));
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
}

@ -417,7 +417,7 @@ impl HitboxMesh{
}
}
#[inline]
const fn transformed_mesh(&self)->TransformedMesh{
fn transformed_mesh(&self)->TransformedMesh{
TransformedMesh::new(self.mesh.complete_mesh_view(),&self.transform)
}
}
@ -1006,31 +1006,30 @@ impl PhysicsData{
let mut used_meshes=Vec::new();
let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new();
for (model_id,model) in map.models.iter().enumerate(){
//TODO: use .entry().or_insert_with(||{
let attr_id=if let Some(&attr_id)=physics_attr_id_from_model_attr_id.get(&model.attributes){
attr_id
}else{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
physics_attr_id_from_model_attr_id.insert(model.attributes,attr_id);
Some(attr_id)
})
}){
Some(attr_id)=>attr_id,
None=>continue,
let attr_id=match physics_attr_id_from_model_attr_id.entry(model.attributes){
std::collections::hash_map::Entry::Occupied(entry)=>*entry.get(),
std::collections::hash_map::Entry::Vacant(entry)=>{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
Some(*entry.insert(attr_id))
})
}){
Some(attr_id)=>attr_id,
None=>continue,
}
}
};
let mesh_id=if let Some(&mesh_id)=physics_mesh_id_from_model_mesh_id.get(&model.mesh){
@ -1194,7 +1193,7 @@ fn recalculate_touching(
collision_end_contact(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,models.contact_attr(contact.model_id),contact)
}
while let Some(&intersect)=touching.intersects.iter().next(){
collision_end_intersect(touching,mode,run,models.intersect_attr(intersect.model_id),intersect,time);
collision_end_intersect(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,mode,run,models.intersect_attr(intersect.model_id),intersect,time);
}
//find all models in the teleport region
let mut aabb=aabb::Aabb::default();
@ -1606,6 +1605,7 @@ fn collision_start_intersect(
None=>(),
}
}
move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
run_teleport_behaviour(intersect.model_id.into(),attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
}
@ -1638,7 +1638,14 @@ fn collision_end_contact(
}
}
fn collision_end_intersect(
move_state:&mut MoveState,
body:&mut Body,
touching:&mut TouchingState,
models:&PhysicsModels,
hitbox_mesh:&HitboxMesh,
style:&StyleModifiers,
camera:&PhysicsCamera,
input_state:&InputState,
mode:Option<&gameplay_modes::Mode>,
run:&mut run::Run,
_attr:&gameplay_attributes::IntersectAttributes,
@ -1646,6 +1653,7 @@ fn collision_end_intersect(
time:Time,
){
touching.remove(&Collision::Intersect(intersect));
move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
if let Some(mode)=mode{
let zone=mode.get_zone(intersect.model_id.into());
match zone{
@ -1702,7 +1710,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
contact
),
Collision::Intersect(intersect)=>collision_end_intersect(
&mut state.touching,
&mut state.move_state,&mut state.body,&mut state.touching,&data.models,&data.hitbox_mesh,&state.style,&state.camera,&state.input_state,
data.modes.get_mode(state.mode_state.get_mode_id()),
&mut state.run,
data.models.intersect_attr(intersect.model_id),
@ -1925,6 +1933,15 @@ mod test{
),Some(Time::from_secs(2)));
}
#[test]
fn test_collision_small_mv(){
test_collision(Body::new(
int3(0,5,0),
int3(0,-1,0)+(vec3::X>>32),
vec3::ZERO,
Time::ZERO
),Some(Time::from_secs(2)));
}
#[test]
fn test_collision_degenerate_east(){
test_collision(Body::new(
int3(3,5,0),

@ -13,7 +13,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
glam = "0.30.0"
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
vbsp = "0.8.0"
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vpk = "0.3.0"

@ -349,7 +349,7 @@ pub struct PartialMap1{
impl PartialMap1{
pub fn add_prop_meshes<'a>(
self,
prop_meshes:Meshes,
prop_meshes:Meshes<model::Mesh>,
)->PartialMap2{
PartialMap2{
attributes:self.attributes,

@ -440,11 +440,11 @@ impl ModesBuilder{
}
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
}
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode){
assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode");
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode)->Result<(),ExistingEntryError>{
error_if_exists(self.modes.insert(mode_id,mode))
}
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage){
assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage");
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage)->Result<(),ExistingEntryError>{
error_if_exists(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage))
}
pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
self.mode_updates.push((mode_id,mode_update));
@ -453,3 +453,12 @@ impl ModesBuilder{
// self.stage_updates.push((mode_id,stage_id,stage_update));
// }
}
#[derive(Debug)]
pub struct ExistingEntryError;
fn error_if_exists<T>(value:Option<T>)->Result<(),ExistingEntryError>{
match value{
Some(_)=>Err(ExistingEntryError),
None=>Ok(())
}
}

@ -1,5 +1,5 @@
pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide};
pub use ratio_ops::ratio::{Ratio,Divide,Parity};
//integer units
@ -132,20 +132,21 @@ impl<T> std::ops::Mul for Time<T>{
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
}
}
impl<T> std::ops::Div<i64> for Time<T>{
type Output=Self;
#[inline]
fn div(self,rhs:i64)->Self::Output{
Self::raw(self.0/rhs)
}
}
impl<T> std::ops::Mul<i64> for Time<T>{
type Output=Self;
#[inline]
fn mul(self,rhs:i64)->Self::Output{
Self::raw(self.0*rhs)
macro_rules! impl_time_i64_rhs_operator {
($op:ident,$method:ident)=>{
impl<T> core::ops::$op<i64> for Time<T>{
type Output=Self;
#[inline]
fn $method(self,rhs:i64)->Self::Output{
Self::raw(self.0.$method(rhs))
}
}
}
}
impl_time_i64_rhs_operator!(Div,div);
impl_time_i64_rhs_operator!(Mul,mul);
impl_time_i64_rhs_operator!(Shr,shr);
impl_time_i64_rhs_operator!(Shl,shl);
impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time<T>)->Self::Output{
@ -656,7 +657,7 @@ pub mod mat3{
}
}
#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3,

@ -2,7 +2,7 @@ use std::collections::HashMap;
use crate::loader::Loader;
use crate::mesh::Meshes;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{Mesh,MeshId,RenderConfig,RenderConfigId,TextureId};
use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId};
#[derive(Clone,Copy,Debug)]
pub enum LoadFailureMode{
@ -93,7 +93,7 @@ impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn into_indices(self)->impl Iterator<Item=H>{
self.mesh_id_from_asset_id.into_keys()
}
pub fn into_meshes<'a,L:Loader<Resource=Mesh,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
pub fn into_meshes<'a,M:Clone,L:Loader<Resource=M,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes<M>,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index);

@ -1,15 +1,15 @@
use strafesnet_common::model::{Mesh,MeshId};
use strafesnet_common::model::MeshId;
pub struct Meshes{
meshes:Vec<Option<Mesh>>,
pub struct Meshes<M>{
meshes:Vec<Option<M>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<Mesh>>)->Self{
impl<M> Meshes<M>{
pub(crate) const fn new(meshes:Vec<Option<M>>)->Self{
Self{
meshes,
}
}
pub fn consume(self)->impl Iterator<Item=(MeshId,Mesh)>{
pub fn consume(self)->impl Iterator<Item=(MeshId,M)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)

@ -204,13 +204,19 @@ macro_rules! impl_matrix_named_fields_shape {
type Target=$struct_outer<Vector<$size_inner,T>>;
#[inline]
fn deref(&self)->&Self::Target{
unsafe{core::mem::transmute(&self.array)}
// This cast is valid because Matrix has #[repr(transparent)]
let ptr=&self.array as *const [[T;$size_inner];$size_outer] as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
}
}
impl<T> core::ops::DerefMut for Matrix<$size_outer,$size_inner,T>{
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
unsafe{core::mem::transmute(&mut self.array)}
// This cast is valid because Matrix has #[repr(transparent)]
let ptr=&mut self.array as *mut [[T;$size_inner];$size_outer] as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}
}
}

@ -330,13 +330,19 @@ macro_rules! impl_vector_named_fields {
type Target=$struct<T>;
#[inline]
fn deref(&self)->&Self::Target{
unsafe{core::mem::transmute(&self.array)}
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&self.array as *const [T;$size] as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
}
}
impl<T> core::ops::DerefMut for Vector<$size,T>{
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
unsafe{core::mem::transmute(&mut self.array)}
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}
}
}

@ -13,12 +13,12 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
bytemuck = "1.14.3"
glam = "0.30.0"
lazy-regex = "3.1.0"
rbx_binary = { version = "1.1.0-sn3", registry = "strafesnet"}
rbx_dom_weak = { version = "3.1.0-sn1", registry = "strafesnet"}
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_mesh = "0.3.1"
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn3", registry = "strafesnet"}
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
roblox_emulator = { version = "0.5.0", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }

@ -1,5 +1,6 @@
use std::io::Read;
use roblox_emulator::types::WeakDom;
use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod rbx;
@ -27,12 +28,6 @@ impl Model{
fn new(dom:WeakDom)->Self{
Self{dom}
}
pub fn into_place(self)->Place{
let Self{mut dom}=self;
let context=roblox_emulator::context::Context::from_mut(&mut dom);
let services=context.convert_into_place();
Place{dom,services}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode)
}
@ -44,23 +39,20 @@ impl AsRef<WeakDom> for Model{
}
pub struct Place{
dom:WeakDom,
services:roblox_emulator::context::Services,
context:Context,
}
impl Place{
pub fn new(dom:WeakDom)->Option<Self>{
let context=roblox_emulator::context::Context::from_ref(&dom);
Some(Self{
services:context.find_services()?,
dom,
pub fn new(dom:WeakDom)->Result<Self,roblox_emulator::context::ServicesError>{
let context=Context::from_place(dom)?;
Ok(Self{
context,
})
}
pub fn run_scripts(&mut self){
let Place{dom,services}=self;
let Place{context}=self;
let runner=roblox_emulator::runner::Runner::new().unwrap();
let context=roblox_emulator::context::Context::from_mut(dom);
let scripts=context.scripts();
let runnable=runner.runnable_context_with_services(context,services).unwrap();
let runnable=runner.runnable_context(context).unwrap();
for script in scripts{
if let Err(e)=runnable.run_script(script){
println!("runner error: {e}");
@ -73,7 +65,15 @@ impl Place{
}
impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{
&self.dom
self.context.as_ref()
}
}
impl From<Model> for Place{
fn from(model:Model)->Self{
let context=Context::from_model(model.dom);
Self{
context,
}
}
}
@ -94,9 +94,9 @@ impl std::error::Error for ReadError{}
pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match &peek[0..8]{
b"<roblox!"=>rbx_binary::from_reader_generic(buf).map(Model::new).map_err(ReadError::RbxBinary),
b"<roblox "=>rbx_xml::from_reader_generic(buf,Default::default()).map(Model::new).map_err(ReadError::RbxXml),
match peek.get(0..8){
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
_=>Err(ReadError::UnknownFileFormat),
}
}

@ -1,5 +1,4 @@
use std::io::Read;
use rbx_dom_weak::ustr;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
@ -7,6 +6,11 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
@ -139,6 +143,12 @@ impl MeshIndex<'_>{
}
}
#[derive(Clone)]
pub struct MeshWithSize{
pub(crate) mesh:Mesh,
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
}
pub struct MeshLoader;
impl MeshLoader{
pub fn new()->Self{
@ -148,7 +158,7 @@ impl MeshLoader{
impl Loader for MeshLoader{
type Error=MeshError;
type Index<'a>=MeshIndex<'a>;
type Resource=Mesh;
type Resource=MeshWithSize;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
@ -172,12 +182,12 @@ impl Loader for MeshLoader{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("PhysicsData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("MeshData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
}

@ -1,7 +1,11 @@
use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated};
use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}};
use strafesnet_common::aabb::Aabb;
use strafesnet_common::integer::vec3;
use strafesnet_common::model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId};
use crate::loader::MeshWithSize;
#[allow(dead_code)]
#[derive(Debug)]
@ -95,7 +99,7 @@ fn ingest_faces2_lods3(
))
}
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Mesh,Error>{
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithSize,Error>{
//generate that mesh boi
let mut unique_pos=Vec::new();
let mut pos_id_from=HashMap::new();
@ -108,8 +112,10 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
let mut unique_vertices=Vec::new();
let mut vertex_id_from=HashMap::new();
let mut polygon_groups=Vec::new();
let mut aabb=Aabb::default();
let mut acquire_pos_id=|pos|{
let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?;
aabb.grow(p);
Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=unique_pos.len();
unique_pos.push(p);
@ -197,7 +203,7 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
}
Ok(model::Mesh{
let mesh=model::Mesh{
unique_pos,
unique_normal,
unique_tex,
@ -213,5 +219,6 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
}],
//disable physics
physics_groups:Vec::new(),
})
};
Ok(MeshWithSize{mesh,size:aabb.size()})
}

@ -56,7 +56,7 @@ const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[
TextureCoordinate::new(1.0,1.0),
TextureCoordinate::new(0.0,1.0),
];
const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
pub const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int(-1,-1, 1),//0 left bottom back
vec3::int( 1,-1, 1),//1 right bottom back
vec3::int( 1, 1, 1),//2 right top back
@ -66,7 +66,7 @@ const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int( 1,-1,-1),//6 right bottom front
vec3::int(-1,-1,-1),//7 left bottom front
];
const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
pub const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 1, 0, 0),//CubeFace::Right
vec3::int( 0, 1, 0),//CubeFace::Top
vec3::int( 0, 0, 1),//CubeFace::Back
@ -121,8 +121,7 @@ impl FaceDescription{
}
}
}
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
pub const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
// right (1, 0, 0)
[
[6,2],//[vertex,tex]
@ -166,6 +165,7 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
[7,2],
],
];
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new();

@ -1,9 +1,6 @@
use std::collections::HashMap;
use crate::loader::MeshIndex;
use crate::loader::{MeshWithSize,MeshIndex};
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
use rbx_dom_weak::ustr;
use roblox_emulator::types::{WeakDom,Instance};
use strafesnet_common::aabb::Aabb;
use strafesnet_common::map;
use strafesnet_common::model;
use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone};
@ -15,24 +12,30 @@ use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,Mes
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
fn recursive_collect_superclass(
objects:&mut std::vec::Vec<rbx_dom_weak::types::Ref>,
dom:&WeakDom,
instance:&Instance,
dom:&rbx_dom_weak::WeakDom,
instance:&rbx_dom_weak::Instance,
superclass:&str
){
let instance=instance.as_ref();
let instance=instance;
let db=rbx_reflection_database::get();
let Some(superclass)=db.classes.get(superclass)else{
return;
};
objects.extend(
dom.descendants_of(instance.as_ref().referent()).filter_map(|instance|{
let class=db.classes.get(instance.as_ref().class.as_str())?;
db.has_superclass(class,superclass).then(||instance.as_ref().referent())
dom.descendants_of(instance.referent()).filter_map(|instance|{
let class=db.classes.get(instance.class.as_str())?;
db.has_superclass(class,superclass).then(||instance.referent())
})
);
}
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{
Planar64Affine3::new(
Planar64Mat3::from_cols([
@ -53,15 +56,19 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
let mut contacting=attr::ContactingAttributes::default();
let mut force_can_collide=can_collide;
let mut force_intersecting=false;
let mut allow_booster=true;
match name{
"Water"=>{
force_can_collide=false;
allow_booster=false;
//TODO: read stupid CustomPhysicalProperties
intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity});
},
"Accelerator"=>{
//although the new game supports collidable accelerators, this is a roblox compatability map loader
force_can_collide=false;
// Accelerator is not allowed to be booster in roblox
allow_booster=false;
general.accelerator=Some(attr::Accelerator{acceleration:velocity});
},
// "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{
@ -70,7 +77,10 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
// force:false,
// behaviour:model::StageElementBehaviour::Unordered
// })),
"SetVelocity"=>general.trajectory=Some(attr::SetTrajectory::Velocity(velocity)),
"SetVelocity"=>{
allow_booster=false;
general.trajectory=Some(attr::SetTrajectory::Velocity(velocity));
},
"MapStart"=>{
force_can_collide=false;
force_intersecting=true;
@ -80,7 +90,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
gameplay_style::StyleModifiers::roblox_bhop(),
model_id
)
);
).unwrap();
},
"MapFinish"=>{
force_can_collide=false;
@ -126,7 +136,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
gameplay_style::StyleModifiers::roblox_bhop(),
model_id
)
);
).unwrap();
},
"WormholeOut"=>{
//the PhysicsModelId has to exist for it to be teleported to!
@ -155,7 +165,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
ModeId::MAIN,
stage_id,
Stage::empty(model_id),
);
).unwrap();
//TODO: let denormalize handle this
StageElementBehaviour::SpawnAt
},
@ -230,7 +240,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}
}
//need some way to skip this
if velocity!=vec3::ZERO{
if allow_booster&&velocity!=vec3::ZERO{
general.booster=Some(attr::Booster::Velocity(velocity));
}
match force_can_collide{
@ -399,35 +409,37 @@ fn get_content_url(content:&rbx_dom_weak::types::Content)->Option<&str>{
fn get_texture_description<'a>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
dom:&'a WeakDom,
object:&Instance,
dom:&'a rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance,
size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription{
//use the biggest one and cut it down later...
let mut part_texture_description=RobloxPartDescription::default();
temp_objects.clear();
recursive_collect_superclass(temp_objects,dom,object,"Decal");
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
for &mut decal_ref in temp_objects{
let Some(decal)=dom.get_by_ref(decal_ref) else{
println!("Decal get_by_ref failed");
continue;
};
let decal=decal.as_ref();
let (
Some(rbx_dom_weak::types::Variant::ContentId(content)),
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
)=(
decal.properties.get(&ustr("Texture")),
decal.properties.get(&ustr("Face")),
decal.properties.get(&ustr("Color3")),
decal.properties.get(&ustr("Transparency")),
decal.properties.get(&static_ustr("TextureContent")),
decal.properties.get(&static_ustr("Face")),
decal.properties.get(&static_ustr("Color3")),
decal.properties.get(&static_ustr("Transparency")),
)else{
println!("Decal is missing a required property");
continue;
};
let texture_id=Some(content.as_str());
let texture_id=match content.value(){
rbx_dom_weak::types::ContentType::Uri(uri)=>Some(uri.as_str()),
_=>None,
};
let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id);
let Ok(cube_face)=normalid.to_u32().try_into()else{
println!("NormalId is invalid");
@ -441,10 +453,10 @@ fn get_texture_description<'a>(
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
) = (
decal.properties.get(&ustr("OffsetStudsU")),
decal.properties.get(&ustr("OffsetStudsV")),
decal.properties.get(&ustr("StudsPerTileU")),
decal.properties.get(&ustr("StudsPerTileV")),
decal.properties.get(&static_ustr("OffsetStudsU")),
decal.properties.get(&static_ustr("OffsetStudsV")),
decal.properties.get(&static_ustr("StudsPerTileU")),
decal.properties.get(&static_ustr("StudsPerTileV")),
)
{
let (size_u,size_v)=match cube_face{
@ -516,7 +528,7 @@ struct GetAttributesArgs<'a>{
velocity:Planar64Vec3,
}
pub fn convert<'a>(
dom:&'a WeakDom,
dom:&'a rbx_dom_weak::WeakDom,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
)->PartialMap1<'a>{
@ -531,10 +543,9 @@ pub fn convert<'a>(
let mut object_refs=Vec::new();
let mut temp_objects=Vec::new();
recursive_collect_superclass(&mut object_refs,dom,dom.root(),"BasePart");
for object_ref in object_refs{
if let Some(object_extended)=dom.get_by_ref(object_ref){
let object=object_extended.as_ref();
recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart");
for object_ref in object_refs {
if let Some(object)=dom.get_by_ref(object_ref){
if let (
Some(rbx_dom_weak::types::Variant::CFrame(cf)),
Some(rbx_dom_weak::types::Variant::Vector3(size)),
@ -543,12 +554,12 @@ pub fn convert<'a>(
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
Some(rbx_dom_weak::types::Variant::Bool(can_collide)),
) = (
object.properties.get(&ustr("CFrame")),
object.properties.get(&ustr("Size")),
object.properties.get(&ustr("Velocity")),
object.properties.get(&ustr("Transparency")),
object.properties.get(&ustr("Color")),
object.properties.get(&ustr("CanCollide")),
object.properties.get(&static_ustr("CFrame")),
object.properties.get(&static_ustr("Size")),
object.properties.get(&static_ustr("Velocity")),
object.properties.get(&static_ustr("Transparency")),
object.properties.get(&static_ustr("Color")),
object.properties.get(&static_ustr("CanCollide")),
)
{
let model_transform=planar64_affine3_from_roblox(cf,size);
@ -557,7 +568,6 @@ pub fn convert<'a>(
let mut parent_ref=object.parent();
let mut full_path=object.name.clone();
while let Some(parent)=dom.get_by_ref(parent_ref){
let parent=parent.as_ref();
full_path=format!("{}.{}",parent.name,full_path);
parent_ref=parent.parent();
}
@ -568,7 +578,7 @@ pub fn convert<'a>(
//TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&ustr("Shape")){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape")){
Shape::Primitive(shape.to_u32().try_into().expect("Funky roblox PartType"))
}else{
panic!("Part has no Shape!");
@ -587,7 +597,7 @@ pub fn convert<'a>(
let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object_extended,size);
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
//obscure rust syntax "slice pattern"
let RobloxPartDescription([
f0,//Cube::Right
@ -642,11 +652,11 @@ pub fn convert<'a>(
Some(rbx_dom_weak::types::Variant::Content(texture_content)),
)=(
// mesh must exist
object.properties.get(&ustr("MeshContent")),
object.properties.get(&static_ustr("MeshContent")),
// texture is allowed to be none
object.properties.get(&ustr("TextureContent")),
object.properties.get(&static_ustr("TextureContent")),
){
let mesh_asset_id=get_content_url(mesh_content).expect("MeshPart Mesh is not a Uri");
let mesh_asset_id=get_content_url(mesh_content).unwrap_or_default();
let texture_asset_id=get_content_url(texture_content);
(
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
@ -659,16 +669,16 @@ pub fn convert<'a>(
let mut content="";
let mut mesh_data:&[u8]=&[];
let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&ustr("AssetId")){
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("MeshData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("PhysicsData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object_extended,size);
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
@ -705,23 +715,23 @@ pub fn convert<'a>(
deferred_unions_deferred_attributes,
}
}
struct MeshWithAabb{
mesh:model::Mesh,
aabb:Aabb,
struct MeshIdWithSize{
mesh:model::MeshId,
size:Planar64Vec3,
}
fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
old_mesh_id:model::MeshId,
render:RenderConfigId,
)->Option<(model::MeshId,&'a Aabb)>{
)->Option<MeshIdWithSize>{
//ignore meshes that fail to load completely for now
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone();
let mut mesh_clone=mesh.clone();
//set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render;
@ -729,22 +739,22 @@ fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
size,
})
}
fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription,
)->Option<(model::MeshId,&'a Aabb)>{
)->Option<MeshIdWithSize>{
//ignore uniones that fail to load completely for now
loaded_meshes.get(&old_union_id).map(|union_with_aabb|(
*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
.entry(part_texture_description.clone()).or_insert_with(||{
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut union_clone=union_with_aabb.mesh.clone();
let mut union_clone=mesh.clone();
//set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
if let Some(face_texture_description)=maybe_face_texture_description{
@ -754,8 +764,8 @@ fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes.push(union_clone);
union_id
}),
&union_with_aabb.aabb,
))
size,
})
}
pub struct PartialMap1<'a>{
primitive_meshes:Vec<model::Mesh>,
@ -766,7 +776,7 @@ pub struct PartialMap1<'a>{
impl PartialMap1<'_>{
pub fn add_meshpart_meshes_and_calculate_attributes(
mut self,
meshpart_meshes:Meshes,
meshpart_meshes:Meshes<MeshWithSize>,
)->PartialMap2{
//calculate attributes
let mut modes_builder=ModesBuilder::default();
@ -778,17 +788,8 @@ impl PartialMap1<'_>{
//decode roblox meshes
//generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>=
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
(old_mesh_id,MeshWithAabb{
mesh,
aabb,
})
}).collect();
let loaded_meshes:HashMap<model::MeshId,MeshWithSize>=
meshpart_meshes.consume().collect();
// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
// I just want to chain iterators together man
@ -801,23 +802,22 @@ impl PartialMap1<'_>{
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_mesh_id_from_render_config_id(
let MeshIdWithSize{mesh,size:mesh_size}=acquire_mesh_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut mesh_id_from_render_config_id,
&loaded_meshes,
deferred_model_deferred_attributes.model.mesh,
deferred_model_deferred_attributes.render
)?;
let size=aabb.size();
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
color:deferred_model_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
]),
deferred_model_deferred_attributes.model.transform.translation
),
@ -825,14 +825,13 @@ impl PartialMap1<'_>{
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_union_id_from_render_config_id(
let MeshIdWithSize{mesh,size}=acquire_union_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut union_id_from_render_config_id,
&loaded_meshes,
deferred_union_deferred_attributes.model.mesh,
deferred_union_deferred_attributes.render
)?;
let size=aabb.size();
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,

@ -1,4 +1,8 @@
use rbx_mesh::mesh_data::{NormalId2 as MeshDataNormalId2,VertexId as MeshDataVertexId};
use crate::loader::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS};
use rbx_mesh::mesh_data::{VertexId as MeshDataVertexId,NormalId2 as MeshDataNormalId2};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::integer::vec3;
@ -57,10 +61,10 @@ pub fn convert(
roblox_physics_data:&[u8],
roblox_mesh_data:&[u8],
size:glam::Vec3,
crate::rbx::RobloxPartDescription(part_texture_description):crate::rbx::RobloxPartDescription,
)->Result<model::Mesh,Error>{
RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<MeshWithSize,Error>{
const NORMAL_FACES:usize=6;
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
// build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new();
@ -80,6 +84,15 @@ pub fn convert(
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
};
//autoscale to size, idk what roblox is doing with the graphics mesh size
let mut pos_min=glam::Vec3::MAX;
let mut pos_max=glam::Vec3::MIN;
for vertex in &graphics_mesh.vertices{
let p=vertex.pos.into();
pos_min=pos_min.min(p);
pos_max=pos_max.max(p);
}
let graphics_size=pos_max-pos_min;
for [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
@ -89,7 +102,8 @@ pub fn convert(
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{
normal_agreement_checker.check(vertex.normal_id);
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
let pos=glam::Vec3::from_array(vertex.pos)/graphics_size;
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array())?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
let tex_coord=glam::Vec2::from_array(vertex.tex);
let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
@ -126,7 +140,11 @@ pub fn convert(
};
//physics
let physics_convex_meshes=if !roblox_physics_data.is_empty(){
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().map(|faces|
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
);
let polygon_groups:Vec<PolygonGroup>=if !roblox_physics_data.is_empty(){
let physics_data=rbx_mesh::read_physics_data_versioned(
std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?;
@ -140,39 +158,48 @@ pub fn convert(
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
};
physics_convex_meshes
let physics_convex_meshes_it=physics_convex_meshes.into_iter().map(|mesh|{
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let face=[
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
});
polygon_groups_normal_it.chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
}else{
Vec::new()
};
let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces|
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
).chain(physics_convex_meshes.into_iter().map(|mesh|{
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
// generate a unit cube as default physics
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let face=[
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
})).collect::<Result<_,_>>()?;
let normal=mb.acquire_normal_id(vec3::ZERO);
let color=mb.acquire_color_id(glam::Vec4::ONE);
let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
).to_vec()).to_vec()));
polygon_groups_normal_it.chain([Ok(polygon_group)]).collect::<Result<_,_>>()?
};
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)]
}).collect();
Ok(mb.build(
let mesh=mb.build(
polygon_groups,
graphics_groups,
physics_groups,
))
);
Ok(MeshWithSize{
mesh,
size:vec3::ONE,
})
}

@ -1,6 +1,6 @@
[package]
name = "roblox_emulator"
version = "0.4.7"
version = "0.5.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -15,7 +15,7 @@ run-service=[]
glam = "0.30.0"
mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.11.2", features = ["macros"] }
rbx_dom_weak = { version = "3.1.0-sn1", registry = "strafesnet"}
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_types = "2.0.0"

@ -1,94 +1,113 @@
use crate::types::WeakDom;
use rbx_dom_weak::{types::Ref,ustr,InstanceBuilder};
use crate::util::static_ustr;
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)
)
)
#[derive(Debug)]
pub enum ServicesError{
WorkspaceNotFound,
}
impl std::fmt::Display for ServicesError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ServicesError{}
pub struct Services{
pub(crate) game:Ref,
pub(crate) workspace:Ref,
}
#[repr(transparent)]
impl Services{
fn find_services(dom:&WeakDom)->Result<Services,ServicesError>{
Ok(Services{
workspace:*dom.root().children().iter().find(|&&r|
dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
).ok_or(ServicesError::WorkspaceNotFound)?,
game:dom.root_ref(),
})
}
}
pub type LuaAppData=&'static mut WeakDom;
pub struct Context{
pub(crate)dom:WeakDom,
pub(crate)services:Services,
}
impl Context{
pub const fn new(dom:WeakDom)->Self{
Self{dom}
pub fn from_place(dom:WeakDom)->Result<Context,ServicesError>{
let services=Services::find_services(&dom)?;
Ok(Self{dom,services})
}
pub fn script_singleton(source:String)->(Context,crate::runner::instance::InstanceRef,Services){
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance){
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(
let dom=WeakDom::new(
InstanceBuilder::new("DataModel")
.with_child(script)
));
let services=context.convert_into_place();
(context,crate::runner::instance::InstanceRef::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)}
);
let context=Self::from_model(dom);
(context,crate::runner::instance::Instance::new_unchecked(script_ref))
}
/// Creates an iterator over all items of a particular class.
pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
self.dom.descendants().filter(|&instance|
class_is_a(instance.as_ref().class.as_ref(),superclass)
).map(|instance|instance.as_ref().referent())
}
pub fn scripts(&self)->Vec<crate::runner::instance::InstanceRef>{
self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::InstanceRef::new).collect()
}
pub fn find_services(&self)->Option<Services>{
Some(Services{
workspace:*self.dom.root().as_ref().children().iter().find(|&&r|
self.dom.get_by_ref(r).is_some_and(|instance|instance.as_ref().class=="Workspace")
)?,
game:self.dom.root_ref(),
let db=rbx_reflection_database::get();
let Some(superclass)=db.classes.get(superclass)else{
panic!("Invalid class");
};
self.dom.descendants().filter_map(|instance|{
let class=db.classes.get(instance.class.as_str())?;
db.has_superclass(class,superclass).then(||instance.referent())
})
}
pub fn convert_into_place(&mut self)->Services{
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
self.superclass_iter("Script")
.filter_map(|script_ref|{
let script=self.dom.get_by_ref(script_ref)?;
if let None|Some(rbx_dom_weak::types::Variant::Bool(false))=script.properties.get(&static_ustr("Disabled")){
return Some(crate::runner::instance::Instance::new_unchecked(script_ref));
}
None
})
.collect()
}
pub fn from_model(mut dom:WeakDom)->Context{
//snapshot root instances
let children=self.dom.root().as_ref().children().to_owned();
let children=dom.root().children().to_owned();
//insert services
let game=self.dom.root_ref();
let game=dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain");
let workspace=self.dom.insert(game,
let workspace=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().as_mut();
game.properties.insert(ustr("workspace"),rbx_types::Variant::Ref(workspace));
game.properties.insert(ustr("Workspace"),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);
dom.transfer_within(instance,workspace);
}
Services{
game,
workspace,
{
//Lowercase and upper case workspace property!
let game=dom.root_mut();
// TODO: DELETE THIS!
game.properties.insert(static_ustr("workspace"),rbx_types::Variant::Ref(workspace));
game.properties.insert(static_ustr("Workspace"),rbx_types::Variant::Ref(workspace));
}
dom.insert(game,InstanceBuilder::new("Lighting"));
let services=Services{game,workspace};
Self{dom,services}
}
}
pub struct Services{
pub game:Ref,
pub workspace:Ref,
impl AsRef<WeakDom> for Context{
fn as_ref(&self)->&WeakDom{
&self.dom
}
}

@ -1,4 +1,4 @@
pub mod types;
mod util;
pub mod runner;
pub mod context;
#[cfg(feature="run-service")]
@ -6,3 +6,7 @@ pub(crate) mod scheduler;
#[cfg(test)]
mod tests;
pub mod mlua{
pub use mlua::{Result,Error};
}

@ -0,0 +1,59 @@
use super::color3::Color3;
#[derive(Clone,Copy)]
pub struct BrickColor(rbx_types::BrickColor);
impl BrickColor{
pub fn from_name(name:&str)->Option<Self>{
Some(BrickColor(rbx_types::BrickColor::from_name(name)?))
}
pub fn from_number(number:u16)->Option<Self>{
Some(BrickColor(rbx_types::BrickColor::from_number(number)?))
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,r:mlua::Value|
match r{
mlua::Value::String(name)=>Ok(BrickColor::from_name(&*name.to_str()?)),
mlua::Value::Integer(number)=>Ok(BrickColor::from_number(number as u16)),
_=>Err(mlua::Error::runtime("Unsupported arguments"))
}
)?
)?;
macro_rules! brickcolor_constructor{
($fname:expr,$internal:ident)=>{
table.raw_set($fname,
lua.create_function(|_,_:()|
Ok(BrickColor(rbx_types::BrickColor::$internal))
)?
)?;
};
}
brickcolor_constructor!("White",White);
brickcolor_constructor!("Gray",MediumStoneGrey);
brickcolor_constructor!("DarkGray",DarkStoneGrey);
brickcolor_constructor!("Black",Black);
brickcolor_constructor!("Red",BrightRed);
brickcolor_constructor!("Yellow",BrightYellow);
brickcolor_constructor!("Green",DarkGreen);
brickcolor_constructor!("Blue",BrightBlue);
globals.set("BrickColor",table)?;
Ok(())
}
impl mlua::UserData for BrickColor{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Color",|_,BrickColor(this)|{
let rbx_types::Color3uint8{r,g,b}=this.to_color3uint8();
Ok(Color3::from_rgb(r,g,b))
});
}
}
type_from_lua_userdata!(BrickColor);

@ -1,7 +1,10 @@
use mlua::FromLua;
use super::number::Number;
use super::vector3::Vector3;
#[derive(Clone,Copy)]
pub struct CFrame(pub(crate)glam::Affine3A);
pub struct CFrame(glam::Affine3A);
impl CFrame{
pub fn new(
@ -34,14 +37,14 @@ fn vec3_from_glam(v:rbx_types::Vector3)->glam::Vec3A{
glam::vec3a(v.x,v.y,v.z)
}
impl Into<rbx_types::CFrame> for CFrame{
fn into(self)->rbx_types::CFrame{
impl From<CFrame> for rbx_types::CFrame{
fn from(CFrame(cf):CFrame)->rbx_types::CFrame{
rbx_types::CFrame::new(
vec3_to_glam(self.0.translation),
vec3_to_glam(cf.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),
vec3_to_glam(cf.matrix3.x_axis),
vec3_to_glam(cf.matrix3.y_axis),
vec3_to_glam(cf.matrix3.z_axis),
)
)
}
@ -60,16 +63,25 @@ impl From<rbx_types::CFrame> for CFrame{
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let cframe_table=lua.create_table()?;
let table=lua.create_table()?;
//CFrame.new
cframe_table.raw_set("new",
lua.create_function(|_,tuple:(
mlua::Value,mlua::Value,Option<f32>,
Option<f32>,Option<f32>,Option<f32>,
Option<f32>,Option<f32>,Option<f32>,
Option<f32>,Option<f32>,Option<f32>,
table.raw_set("new",
lua.create_function(|lua,tuple:(
mlua::Value,mlua::Value,Option<Number>,
Option<Number>,Option<Number>,Option<Number>,
Option<Number>,Option<Number>,Option<Number>,
Option<Number>,Option<Number>,Option<Number>,
)|match tuple{
//CFrame.new()
(
mlua::Value::Nil,mlua::Value::Nil,None,
None,None,None,
None,None,None,
None,None,None,
)=>{
Ok(CFrame(glam::Affine3A::IDENTITY))
},
//CFrame.new(pos)
(
mlua::Value::UserData(pos),mlua::Value::Nil,None,
@ -77,8 +89,8 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
None,None,None,
None,None,None,
)=>{
let pos:Vector3=pos.take()?;
Ok(CFrame::point(pos.0.x,pos.0.y,pos.0.z))
let Vector3(pos):&Vector3=&*pos.borrow()?;
Ok(CFrame::point(pos.x,pos.y,pos.z))
},
//TODO: CFrame.new(pos,look)
(
@ -87,85 +99,99 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
None,None,None,
None,None,None,
)=>{
let _pos:Vector3=pos.take()?;
let _look:Vector3=look.take()?;
let _pos:&Vector3=&*pos.borrow()?;
let _look:&Vector3=&*look.borrow()?;
Err(mlua::Error::runtime("Not yet implemented"))
},
//CFrame.new(x,y,z)
(
mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
x,y,Some(z),
None,None,None,
None,None,None,
None,None,None,
)=>Ok(CFrame::point(x as f32,y as f32,z)),
)=>Ok(CFrame::point(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into())),
//CFrame.new(x,y,z,xx,yx,zx,xy,yy,zy,xz,yz,zz)
(
mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
x,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,
)=>Ok(CFrame::new(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into(),
xx.into(),yx.into(),zx.into(),
xy.into(),yy.into(),zy.into(),
xz.into(),yz.into(),zz.into(),
)),
_=>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))
)?
let from_euler_angles=lua.create_function(|_,(x,y,z):(Number,Number,Number)|
Ok(CFrame::angles(x.into(),y.into(),z.into()))
)?;
table.raw_set("Angles",from_euler_angles.clone())?;
table.raw_set("fromEulerAnglesXYZ",from_euler_angles.clone())?;
table.raw_set("FromEulerAnglesXYZ",from_euler_angles)?;
globals.set("CFrame",cframe_table)?;
globals.set("CFrame",table)?;
Ok(())
}
impl mlua::UserData for CFrame{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
//CFrame.p
fields.add_field_method_get("p",|_,this|Ok(Vector3(this.0.translation)));
fields.add_field_method_get("p",|_,CFrame(this)|Ok(Vector3(this.translation)));
fields.add_field_method_get("x",|_,CFrame(this)|Ok(this.translation.x));
fields.add_field_method_get("X",|_,CFrame(this)|Ok(this.translation.x));
fields.add_field_method_get("y",|_,CFrame(this)|Ok(this.translation.y));
fields.add_field_method_get("Y",|_,CFrame(this)|Ok(this.translation.y));
fields.add_field_method_get("z",|_,CFrame(this)|Ok(this.translation.z));
fields.add_field_method_get("Z",|_,CFrame(this)|Ok(this.translation.z));
fields.add_field_method_get("rightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
fields.add_field_method_get("RightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
fields.add_field_method_get("upVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
fields.add_field_method_get("UpVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
fields.add_field_method_get("lookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
fields.add_field_method_get("LookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
fields.add_field_method_get("XVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(0))));
fields.add_field_method_get("YVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(1))));
fields.add_field_method_get("ZVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(2))));
}
fn add_methods<M:mlua::UserDataMethods<Self>>(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("components",|_,CFrame(this),()|Ok((
this.translation.x,
this.translation.y,
this.translation.z,
this.matrix3.x_axis.x,
this.matrix3.y_axis.x,
this.matrix3.z_axis.x,
this.matrix3.x_axis.y,
this.matrix3.y_axis.y,
this.matrix3.z_axis.y,
this.matrix3.x_axis.z,
this.matrix3.y_axis.z,
this.matrix3.z_axis.z,
)));
methods.add_method("VectorToWorldSpace",|_,this,v:Vector3|
Ok(Vector3(this.0.transform_vector3a(v.0)))
methods.add_method("VectorToWorldSpace",|_,CFrame(this),Vector3(v):Vector3|
Ok(Vector3(this.transform_vector3a(v)))
);
//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|
methods.add_meta_function(mlua::MetaMethod::Mul,|_,(CFrame(this),CFrame(val)):(Self,Self)|Ok(Self(this*val)));
methods.add_meta_function(mlua::MetaMethod::ToString,|_,CFrame(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,
this.translation.x,
this.translation.y,
this.translation.z,
this.matrix3.x_axis.x,
this.matrix3.y_axis.x,
this.matrix3.z_axis.x,
this.matrix3.x_axis.y,
this.matrix3.y_axis.y,
this.matrix3.z_axis.y,
this.matrix3.x_axis.z,
this.matrix3.y_axis.z,
this.matrix3.z_axis.z,
))
);
}

@ -1,3 +1,5 @@
use super::number::Number;
#[derive(Clone,Copy)]
pub struct Color3{
r:f32,
@ -8,28 +10,37 @@ impl Color3{
pub const fn new(r:f32,g:f32,b:f32)->Self{
Self{r,g,b}
}
pub const fn from_rgb(r:u8,g:u8,b:u8)->Self{
Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0)
}
}
impl Into<rbx_types::Color3> for Color3{
fn into(self)->rbx_types::Color3{
rbx_types::Color3::new(self.r,self.g,self.b)
impl From<rbx_types::Color3> for Color3{
fn from(value:rbx_types::Color3)->Color3{
Color3::new(value.r,value.g,value.b)
}
}
impl From<Color3> for rbx_types::Color3{
fn from(value:Color3)->rbx_types::Color3{
rbx_types::Color3::new(value.r,value.g,value.b)
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let color3_table=lua.create_table()?;
let 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))
table.raw_set("new",
lua.create_function(|_,(r,g,b):(Number,Number,Number)|
Ok(Color3::new(r.into(),g.into(),b.into()))
)?
)?;
globals.set("Color3",color3_table)?;
let from_rgb=lua.create_function(|_,(r,g,b):(u8,u8,u8)|
Ok(Color3::from_rgb(r,g,b))
)?;
table.raw_set("fromRGB",from_rgb.clone())?;
table.raw_set("FromRGB",from_rgb)?;
globals.set("Color3",table)?;
Ok(())
}

@ -1,31 +1,29 @@
#[derive(Clone,Copy)]
pub struct ColorSequence{}
#[derive(Clone)]
pub struct ColorSequence(rbx_types::ColorSequence);
impl ColorSequence{
pub const fn new()->Self{
Self{}
pub const fn new(keypoints:Vec<rbx_types::ColorSequenceKeypoint>)->Self{
Self(rbx_types::ColorSequence{keypoints})
}
}
impl Into<rbx_types::ColorSequence> for ColorSequence{
fn into(self)->rbx_types::ColorSequence{
rbx_types::ColorSequence{
keypoints:Vec::new()
}
impl From<ColorSequence> for rbx_types::ColorSequence{
fn from(ColorSequence(value):ColorSequence)->rbx_types::ColorSequence{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let number_sequence_table=lua.create_table()?;
let table=lua.create_table()?;
number_sequence_table.raw_set("new",
table.raw_set("new",
lua.create_function(|_,_:mlua::MultiValue|
Ok(ColorSequence::new())
Ok(ColorSequence::new(Vec::new()))
)?
)?;
globals.set("ColorSequence",number_sequence_table)?;
globals.set("ColorSequence",table)?;
Ok(())
}
impl mlua::UserData for ColorSequence{}
type_from_lua_userdata!(ColorSequence);
type_from_lua_userdata_clone!(ColorSequence);

@ -1,63 +1,136 @@
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>,
name:Option<&'a str>,
value:u32,
}
impl Into<rbx_types::Enum> for Enum{
fn into(self)->rbx_types::Enum{
rbx_types::Enum::from_u32(self.0)
impl<'a> EnumItem<'a>{
fn known_name((name,&value):(&'a std::borrow::Cow<'a,str>,&u32))->Self{
Self{name:Some(name.as_ref()),value}
}
}
impl<'a> From<rbx_types::Enum> for EnumItem<'a>{
fn from(e:rbx_types::Enum)->Self{
EnumItem{
name:None,
value:e.to_u32(),
}
}
}
impl From<EnumItem<'_>> for rbx_types::Enum{
fn from(e:EnumItem)->rbx_types::Enum{
rbx_types::Enum::from_u32(e.value)
}
}
impl PartialEq for EnumItem<'_>{
fn eq(&self,other:&EnumItem<'_>)->bool{
self.value==other.value&&{
// if both names are known, they must match, otherwise whatever
match (self.name,other.name){
(Some(lhs),Some(rhs))=>lhs==rhs,
_=>true,
}
}
}
}
impl<'a> EnumItem<'a>{
const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{
Self{ed}
#[derive(Clone,Copy)]
pub struct Enums;
impl Enums{
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
let db=rbx_reflection_database::get();
db.enums.get(index).map(|ed|EnumItems{ed})
}
}
#[derive(Clone,Copy)]
pub struct EnumItems<'a>{
ed:&'a rbx_reflection::EnumDescriptor<'a>,
}
impl<'a> EnumItems<'a>{
pub fn from_value(&self,value:u32)->Option<EnumItem<'a>>{
self.ed.items.iter().find(|&(_,&v)|v==value).map(EnumItem::known_name)
}
pub fn from_name(&self,name:&str)->Option<EnumItem<'a>>{
self.ed.items.get_key_value(name).map(EnumItem::known_name)
}
pub fn from_enum(&self,enum_item:EnumItem)->Option<EnumItem<'a>>{
match enum_item.name{
Some(s)=>{
let got=self.from_name(s)?;
(got.value==enum_item.value).then_some(got)
},
None=>self.from_value(enum_item.value)
}
}
}
pub enum CoerceEnum<'a>{
Integer(i32),
String(mlua::String),
Enum(EnumItem<'a>),
}
impl CoerceEnum<'_>{
pub fn coerce_to<'a>(self,enum_items:EnumItems<'a>)->mlua::Result<EnumItem<'a>>{
match self{
CoerceEnum::Integer(int)=>enum_items.from_value(int as u32),
CoerceEnum::String(s)=>enum_items.from_name(&*s.to_str()?),
CoerceEnum::Enum(enum_item)=>enum_items.from_enum(enum_item),
}.ok_or_else(||mlua::Error::runtime(format!("Bad {} EnumItem",enum_items.ed.name)))
}
}
impl mlua::FromLua for CoerceEnum<'_>{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::Integer(int)=>Ok(CoerceEnum::Integer(int)),
mlua::Value::String(s)=>Ok(CoerceEnum::String(s)),
mlua::Value::UserData(ud)=>Ok(CoerceEnum::Enum(*ud.borrow()?)),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Enum),other))),
}
}
}
pub fn set_globals(_lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
globals.set("Enum",EnumItems)
globals.set("Enum",Enums)
}
impl mlua::UserData for EnumItem<'_>{
impl mlua::UserData for EnumItems<'static>{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(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),
}
methods.add_method("FromName",|_,this:&EnumItems,name:mlua::String|Ok(this.from_name(&*name.to_str()?)));
methods.add_method("FromValue",|_,this:&EnumItems,value:u32|Ok(this.from_value(value)));
methods.add_method("GetEnumItems",|_,this:&EnumItems,()|->mlua::Result<Vec<EnumItem>>{
Ok(this.ed.items.iter().map(EnumItem::known_name).collect())
});
methods.add_meta_function(mlua::MetaMethod::Index,|_,(this,val):(EnumItems,mlua::String)|{
let index=&*val.to_str()?;
Ok(this.ed.items.get_key_value(index).map(EnumItem::known_name))
});
}
}
type_from_lua_userdata_lua_lifetime!(EnumItems);
impl mlua::UserData for Enums{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Index,|_,(enums,val):(Self,mlua::String)|{
Ok(enums.get(&*val.to_str()?))
});
}
}
type_from_lua_userdata!(Enums);
impl mlua::UserData for EnumItem<'_>{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Name",|_,this|Ok(this.name));
fields.add_field_method_get("Value",|_,this|Ok(this.value));
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Eq,|_,(lhs,rhs):(EnumItem<'_>,EnumItem<'_>)|{
Ok(lhs==rhs)
});
}
}
type_from_lua_userdata_lua_lifetime!(EnumItem);
impl mlua::UserData for EnumItems{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(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<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(_methods:&mut M){
}
}
type_from_lua_userdata!(Enum);

@ -2,54 +2,51 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref;
use rbx_dom_weak::{ustr,Ustr,InstanceBuilder};
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use crate::runner::vector3::Vector3;
use crate::types::{WeakDom,Instance};
use crate::runner::number::Number;
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store
lua.set_app_data(ClassMethodsStore::default());
let instance_table=lua.create_table()?;
let table=lua.create_table()?;
//Instance.new
instance_table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<InstanceRef>)|{
table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
let class_name_str=&*class_name.to_str()?;
let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{
//TODO: Nil instances
Ok(InstanceRef::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
})
})?
)?;
globals.set("Instance",instance_table)?;
globals.set("Instance",table)?;
Ok(())
}
// LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{
match value{
&mlua::Value::Integer(i)=>Some(i as f32),
&mlua::Value::Number(f)=>Some(f as f32),
_=>None,
}
pub fn class_is_a(class:&str,superclass:&str)->bool{
let db=rbx_reflection_database::get();
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
return false;
};
db.has_superclass(class,superclass)
}
fn get_full_name(dom:&WeakDom,instance:&Instance)->String{
let instance=instance.as_ref();
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){
let parent=parent.as_ref();
full_name.insert(0,'.');
full_name.insert_str(0,parent.name.as_str());
pref=parent.parent();
@ -57,10 +54,10 @@ fn get_full_name(dom:&WeakDom,instance:&Instance)->String{
full_name
}
//helper function for script
pub fn get_name_source(lua:&mlua::Lua,script:InstanceRef)->Result<(String,String),mlua::Error>{
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.as_ref().properties.get(&ustr("Source")){
let source=match instance.properties.get(&static_ustr("Source")){
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
_=>Err(mlua::Error::external("Missing script.Source"))?,
};
@ -68,61 +65,83 @@ pub fn get_name_source(lua:&mlua::Lua,script:InstanceRef)->Result<(String,String
})
}
pub fn find_first_child<'a>(dom:&'a WeakDom,instance:&Instance,name:&str)->Option<&'a Instance>{
instance.as_ref().children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.as_ref().name==name)
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 WeakDom,instance:&Instance,name:&str)->Option<&'a Instance>{
dom.descendants_of(instance.as_ref().referent()).find(|&inst|inst.as_ref().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 WeakDom,instance:&Instance,class:&str)->Option<&'a Instance>{
instance.as_ref().children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.as_ref().class==class)
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 WeakDom,instance:&Instance,class:&str)->Option<&'a Instance>{
dom.descendants_of(instance.as_ref().referent()).find(|&inst|inst.as_ref().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)
}
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
dom.descendants_of(instance.referent()).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
#[derive(Clone,Copy)]
pub struct InstanceRef{
pub struct Instance{
referent:Ref,
}
impl InstanceRef{
pub const fn new(referent:Ref)->Self{
impl Instance{
pub const fn new_unchecked(referent:Ref)->Self{
Self{referent}
}
pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a Instance>{
pub fn new(referent:Ref)->Option<Self>{
referent.is_some().then_some(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 Instance>{
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!(InstanceRef);
type_from_lua_userdata!(Instance);
impl mlua::UserData for InstanceRef{
impl mlua::UserData for Instance{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Parent",|lua,this|{
fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
Ok(InstanceRef::new(instance.parent()))
let instance=this.get(dom)?;
Ok(Instance::new(instance.parent()))
})
});
fields.add_field_method_set("Parent",|lua,this,val:Option<InstanceRef>|{
let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
}
fields.add_field_method_get("parent",get_parent);
fields.add_field_method_get("Parent",get_parent);
fn set_parent(lua:&mlua::Lua,this:&mut Instance,new_parent:Option<Instance>)->mlua::Result<()>{
let parent_ref=new_parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{
dom.transfer_within(this.referent,parent.referent);
dom.transfer_within(this.referent,parent_ref);
Ok(())
})
});
}
fields.add_field_method_set("parent",set_parent);
fields.add_field_method_set("Parent",set_parent);
fields.add_field_method_get("Name",|lua,this|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
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)?.as_mut();
let instance=this.get_mut(dom)?;
//Why does this need to be cloned?
instance.name=val.to_str()?.to_owned();
Ok(())
@ -130,25 +149,41 @@ impl mlua::UserData for InstanceRef{
});
fields.add_field_method_get("ClassName",|lua,this|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
let instance=this.get(dom)?;
Ok(instance.class.to_owned())
})
});
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("GetChildren",|lua,this,_:()|
fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
let instance_ref=dom.clone_within(this.referent);
Ok(Instance::new_unchecked(instance_ref))
})
}
methods.add_method("clone",clone);
methods.add_method("Clone",clone);
fn get_children(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Vec<Instance>>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
let children:Vec<_>=instance
.children()
.iter()
.copied()
.map(InstanceRef::new)
.map(Instance::new_unchecked)
.collect();
Ok(children)
})
}
methods.add_method("children",get_children);
methods.add_method("GetChildren",get_children);
methods.add_method("GetFullName",|lua,this,()|
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(get_full_name(dom,instance))
})
);
fn ffc(lua:&mlua::Lua,this:&InstanceRef,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<InstanceRef>>{
fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
let name_str=&*name.to_str()?;
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
@ -158,7 +193,7 @@ impl mlua::UserData for InstanceRef{
false=>find_first_child(dom,instance,name_str),
}
.map(|instance|
InstanceRef::new(instance.as_ref().referent())
Instance::new_unchecked(instance.referent())
)
)
})
@ -168,13 +203,29 @@ impl mlua::UserData for InstanceRef{
methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{
let inst=this.get(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),
true=>find_first_descendant_of_class(dom,inst,class_str),
false=>find_first_child_of_class(dom,inst,class_str),
}
.map(|instance|
InstanceRef::new(instance.as_ref().referent())
Instance::new_unchecked(instance.referent())
)
)
})
});
methods.add_method("FindFirstChildWhichIsA",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok(
match search_descendants.unwrap_or(false){
true=>find_first_descendant_which_is_a(dom,inst,class_str),
false=>find_first_child_which_is_a(dom,inst,class_str),
}
.map(|instance|
Instance::new_unchecked(instance.referent())
)
)
})
@ -184,58 +235,83 @@ impl mlua::UserData for InstanceRef{
let children:Vec<_>=dom
.descendants_of(this.referent)
.map(|instance|
InstanceRef::new(instance.as_ref().referent())
Instance::new_unchecked(instance.referent())
)
.collect();
Ok(children)
})
);
methods.add_method("IsA",|lua,this,classname:mlua::String|
methods.add_method("IsAncestorOf",|lua,this,descendant:Instance|
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
let instance=descendant.get(dom)?;
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==this.referent))
})
);
methods.add_method("Destroy",|lua,this,()|
methods.add_method("IsDescendantOf",|lua,this,ancestor:Instance|
dom_mut(lua,|dom|{
dom.destroy(this.referent);
let instance=this.get(dom)?;
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
})
);
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(class_is_a(instance.class.as_str(),&*classname.to_str()?))
})
}
methods.add_method("isA",is_a);
methods.add_method("IsA",is_a);
fn destroy(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<()>{
dom_mut(lua,|dom|{
dom.transfer_within(this.referent,Ref::none());
Ok(())
})
);
methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:InstanceRef|{
}
methods.add_method("remove",destroy);
methods.add_method("Remove",destroy);
methods.add_method("destroy",destroy);
methods.add_method("Destroy",destroy);
methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?.as_ref();
let instance=this.get(dom)?;
Ok(instance.name.clone())
})
});
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(InstanceRef,mlua::String)|{
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{
let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
let instance_ref=instance.as_ref();
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get();
let class=db.classes.get(instance_ref.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
//Find existing property
match instance_ref.properties.get(&index_ustr)
.cloned()
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
// Find existing property
// Interestingly, ustr can know ahead of time if
// a property does not exist in any runtime instance
match Ustr::from_existing(index_str)
.and_then(|index_ustr|
instance.properties.get(&index_ustr).cloned()
)
//Find default value
.or_else(||db.find_default_property(class,index_str).cloned())
//Find virtual property
.or_else(||db.superclasses_iter(class).find_map(|class|
find_virtual_property(&instance_ref.properties,class,&index_ustr)
find_virtual_property(&instance.properties,class,index_str)
))
{
Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
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 InstanceRef::new(val).into_lua(lua),
Some(rbx_types::Variant::CFrame(cf))=>return Into::<crate::runner::cframe::CFrame>::into(cf).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).into_lua(lua),
Some(rbx_types::Variant::String(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).into_lua(lua),
Some(rbx_types::Variant::Color3(c))=>return crate::runner::color3::Color3::from(c).into_lua(lua),
Some(rbx_types::Variant::CFrame(cf))=>return crate::runner::cframe::CFrame::from(cf).into_lua(lua),
Some(rbx_types::Variant::Vector2(v))=>return crate::runner::vector2::Vector2::from(v).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return crate::runner::vector3::Vector3::from(v).into_lua(lua),
None=>(),
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance_ref.name))),
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|{
@ -250,23 +326,21 @@ impl mlua::UserData for InstanceRef{
//find or create an associated userdata object
let instance=this.get_mut(dom)?;
if let Some(value)=instance.get_or_create_userdata(lua,index_ustr)?{
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
return value.into_lua(lua);
}
// drop mutable borrow
//find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str)
.map(|instance|InstanceRef::new(instance.as_ref().referent()))
.map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua)
})
});
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(InstanceRef,mlua::String,mlua::Value)|{
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?.as_mut();
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
let instance=this.get_mut(dom)?;
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let property=db.superclasses_iter(class).find_map(|cls|
@ -274,53 +348,91 @@ impl mlua::UserData for InstanceRef{
).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_ustr,rbx_types::Variant::Vector3(typed_value.into()));
},
let value=match &property.data_type{
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_ustr,rbx_types::Variant::Float32(typed_value));
let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
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"))?;
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()?;
let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?;
Ok(e.into())
},
_=>Err(mlua::Error::runtime("Expected Enum")),
}?;
instance.properties.insert(index_ustr,rbx_types::Variant::Enum(typed_value));
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_ustr,rbx_types::Variant::Color3(typed_value.into()));
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_ustr,rbx_types::Variant::Bool(typed_value));
rbx_types::Variant::Bool(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
rbx_types::Variant::Int32(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_ustr,rbx_types::Variant::String(typed_value.to_owned()));
let typed_value=match &value{
mlua::Value::Integer(i)=>i.to_string(),
mlua::Value::Number(n)=>n.to_string(),
mlua::Value::String(s)=>s.to_str()?.to_owned(),
_=>return Err(mlua::Error::runtime("Expected string")),
};
rbx_types::Variant::String(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::UDim2)=>{
let typed_value:&crate::runner::udim2::UDim2=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected UDim2"))?.borrow()?;
rbx_types::Variant::UDim2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberRange)=>{
let typed_value:&crate::runner::number_range::NumberRange=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberRange"))?.borrow()?;
rbx_types::Variant::NumberRange(typed_value.clone().into())
},
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_ustr,rbx_types::Variant::NumberSequence(typed_value.into()));
let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
rbx_types::Variant::NumberSequence(typed_value.clone().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_ustr,rbx_types::Variant::ColorSequence(typed_value.into()));
let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
rbx_types::Variant::ColorSequence(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector2)=>{
let typed_value:crate::runner::vector2::Vector2=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector2"))?.borrow()?;
rbx_types::Variant::Vector2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:crate::runner::vector3::Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector3"))?.borrow()?;
rbx_types::Variant::Vector3(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::CFrame)=>{
let typed_value:crate::runner::cframe::CFrame=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected CFrame"))?.borrow()?;
rbx_types::Variant::CFrame(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
rbx_types::Variant::ContentId(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
// why clone?
let typed_value=Option::<Instance>::from_lua(value.clone(),lua)?;
rbx_types::Variant::Ref(typed_value.map_or(Ref::none(),|instance|instance.referent))
},
other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))),
}
};
// the index is known to be a real property at this point
// allow creating a permanent ustr (memory leak)
let index_ustr=rbx_dom_weak::ustr(index_str);
instance.properties.insert(index_ustr,value);
Ok(())
})
});
@ -331,7 +443,7 @@ impl mlua::UserData for InstanceRef{
macro_rules! cf{
($f:expr)=>{
|lua,mut args|{
let this=InstanceRef::from_lua(args.pop_front().unwrap_or(mlua::Value::Nil),lua)?;
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)
}
};
@ -345,28 +457,53 @@ type CFD=phf::Map<&'static str,// Class name
ClassFunctionPointer
>
>;
const GET_SERVICE:ClassFunctionPointer=cf!(|lua,_this,service:mlua::String|{
dom_mut(lua,|dom|{
//dom.root_ref()==this.referent ?
let service=&*service.to_str()?;
match service{
"Lighting"|"RunService"|"Players"|"Workspace"|"MaterialService"|"TweenService"=>{
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_unchecked(referent))
},
other=>Err(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
});
const GET_PLAYERS:ClassFunctionPointer=cf!(|_lua,_this,()|->mlua::Result<_>{
Ok(Vec::<Instance>::new())
});
const NO_OP:ClassFunctionPointer=cf!(|_lua,_this,_:mlua::MultiValue|->mlua::Result<_>{Ok(())});
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.as_ref().referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(InstanceRef::new(referent))
},
other=>Err::<InstanceRef,_>(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
}),
"service"=>GET_SERVICE,
"GetService"=>GET_SERVICE,
},
"Terrain"=>phf::phf_map!{
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(()))
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
},
"Players"=>phf::phf_map!{
"players"=>GET_PLAYERS,
"GetPlayers"=>GET_PLAYERS,
},
"Sound"=>phf::phf_map!{
"Play"=>NO_OP,
},
"TweenService"=>phf::phf_map!{
"Create"=>cf!(|_lua,_,(instance,tween_info,goal):(Instance,crate::runner::tween_info::TweenInfo,mlua::Table)|->mlua::Result<_>{
Ok(crate::runner::tween::Tween::create(
instance,
tween_info,
goal,
))
}),
},
};
@ -455,15 +592,68 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
fn find_virtual_property(
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor,
index:&Ustr,
index:&str,
)->Option<rbx_types::Variant>{
//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(&ustr(virtual_property.property))?;
let variant=properties.get(&static_ustr(virtual_property.property))?;
//Transform Source property with provided function
(virtual_property.pointer)(variant)
}
// lazy-loaded per-instance userdata values
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
type LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name
CreateUserData
>
>;
fn create_script_signal(lua:&mlua::Lua)->mlua::Result<mlua::AnyUserData>{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
}
static LAZY_USER_DATA:LUD=phf::phf_map!{
"RunService"=>phf::phf_map!{
"Stepped"=>create_script_signal,
"Heartbeat"=>create_script_signal,
"RenderStepped"=>create_script_signal,
},
"Players"=>phf::phf_map!{
"PlayerAdded"=>create_script_signal,
},
"BasePart"=>phf::phf_map!{
"Touched"=>create_script_signal,
"TouchEnded"=>create_script_signal,
},
"Instance"=>phf::phf_map!{
"ChildAdded"=>create_script_signal,
"ChildRemoved"=>create_script_signal,
"DescendantAdded"=>create_script_signal,
"DescendantRemoved"=>create_script_signal,
},
"ClickDetector"=>phf::phf_map!{
"MouseClick"=>create_script_signal,
},
};
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
use std::collections::hash_map::Entry;
let db=rbx_reflection_database::get();
let Some(class)=db.classes.get(instance.class.as_str())else{
return Ok(None)
};
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
// find pair (class,index)
LAZY_USER_DATA.get(&superclass.name)
.and_then(|map|map.get_entry(index))
){
let index_ustr=static_ustr(static_str);
return Ok(Some(match instance.userdata.entry(index_ustr){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
}));
}
Ok(None)
}

@ -1,2 +1,2 @@
pub mod instance;
pub use instance::InstanceRef;
pub use instance::Instance;

@ -10,6 +10,18 @@ macro_rules! type_from_lua_userdata{
}
};
}
macro_rules! type_from_lua_userdata_clone{
($ty:ident)=>{
impl mlua::FromLua for $ty{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::UserData(ud)=>Ok(ud.borrow::<Self>()?.clone()),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
}
}
}
};
}
macro_rules! type_from_lua_userdata_lua_lifetime{
($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{

@ -3,11 +3,20 @@ mod macros;
mod runner;
mod r#enum;
mod task;
mod udim;
mod tween;
mod udim2;
mod color3;
mod cframe;
mod number;
mod vector2;
mod vector3;
mod brickcolor;
mod tween_info;
pub mod instance;
pub mod script_signal;
mod number_range;
mod script_signal;
mod color_sequence;
mod number_sequence;

@ -0,0 +1,43 @@
// the goal of this module is to provide an intermediate type
// that is guaranteed to be some kind of number, and provide
// methods to coerce it into various more specific types.
#[derive(Clone,Copy)]
pub enum Number{
Integer(i32),
Number(f64),
}
macro_rules! impl_ty{
($ident:ident,$ty:ty)=>{
impl Number{
#[inline]
pub fn $ident(self)->$ty{
match self{
Self::Integer(int)=>int as $ty,
Self::Number(num)=>num as $ty,
}
}
}
impl From<Number> for $ty{
fn from(value:Number)->$ty{
value.$ident()
}
}
};
}
impl_ty!(to_u32,u32);
impl_ty!(to_i32,i32);
impl_ty!(to_f32,f32);
impl_ty!(to_u64,u64);
impl_ty!(to_i64,i64);
impl_ty!(to_f64,f64);
impl mlua::FromLua for Number{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::Integer(int)=>Ok(Number::Integer(int)),
mlua::Value::Number(num)=>Ok(Number::Number(num)),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Number),other))),
}
}
}

@ -0,0 +1,34 @@
use super::number::Number;
#[derive(Clone)]
pub struct NumberRange(rbx_types::NumberRange);
impl NumberRange{
pub const fn new(min:f32,max:f32)->Self{
Self(rbx_types::NumberRange{min,max})
}
}
impl From<NumberRange> for rbx_types::NumberRange{
fn from(NumberRange(value):NumberRange)->rbx_types::NumberRange{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(min,max):(Number,Option<Number>)|{
Ok(match max{
Some(max)=>NumberRange::new(min.into(),max.into()),
None=>NumberRange::new(min.into(),min.into()),
})
})?
)?;
globals.set("NumberRange",table)?;
Ok(())
}
impl mlua::UserData for NumberRange{}
type_from_lua_userdata_clone!(NumberRange);

@ -1,31 +1,36 @@
#[derive(Clone,Copy)]
pub struct NumberSequence{}
#[derive(Clone)]
pub struct NumberSequence(rbx_types::NumberSequence);
impl NumberSequence{
pub const fn new()->Self{
Self{}
pub const fn new(keypoints:Vec<rbx_types::NumberSequenceKeypoint>)->Self{
Self(rbx_types::NumberSequence{keypoints})
}
}
impl Into<rbx_types::NumberSequence> for NumberSequence{
fn into(self)->rbx_types::NumberSequence{
rbx_types::NumberSequence{
keypoints:Vec::new()
}
impl From<NumberSequence> for rbx_types::NumberSequence{
fn from(NumberSequence(value):NumberSequence)->rbx_types::NumberSequence{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let number_sequence_table=lua.create_table()?;
let table=lua.create_table()?;
number_sequence_table.raw_set("new",
table.raw_set("new",
lua.create_function(|_,_:mlua::MultiValue|
Ok(NumberSequence::new())
Ok(NumberSequence::new(Vec::new()))
)?
)?;
globals.set("NumberSequence",number_sequence_table)?;
globals.set("NumberSequence",table)?;
Ok(())
}
impl mlua::UserData for NumberSequence{}
type_from_lua_userdata!(NumberSequence);
impl mlua::FromLua for NumberSequence{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::UserData(ud)=>Ok(ud.borrow::<Self>()?.clone()),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(NumberSequence),other))),
}
}
}

@ -1,7 +1,7 @@
use crate::context::Context;
use crate::util::static_ustr;
#[cfg(feature="run-service")]
use crate::scheduler::scheduler_mut;
use crate::types::WeakDom;
pub struct Runner{
lua:mlua::Lua,
@ -13,7 +13,7 @@ pub enum Error{
error:mlua::Error
},
RustLua(mlua::Error),
NoServices,
Services(crate::context::ServicesError),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -32,14 +32,19 @@ fn init(lua:&mlua::Lua)->mlua::Result<()>{
//global environment
let globals=lua.globals();
#[cfg(feature="run-service")]
crate::scheduler::set_globals(lua,&globals)?;
super::task::set_globals(lua,&globals)?;
super::script_signal::set_globals(lua,&globals)?;
super::r#enum::set_globals(lua,&globals)?;
super::udim::set_globals(lua,&globals)?;
super::udim2::set_globals(lua,&globals)?;
super::color3::set_globals(lua,&globals)?;
super::brickcolor::set_globals(lua,&globals)?;
super::vector2::set_globals(lua,&globals)?;
super::vector3::set_globals(lua,&globals)?;
super::cframe::set_globals(lua,&globals)?;
super::instance::instance::set_globals(lua,&globals)?;
super::tween_info::set_globals(lua,&globals)?;
super::number_range::set_globals(lua,&globals)?;
super::number_sequence::set_globals(lua,&globals)?;
super::color_sequence::set_globals(lua,&globals)?;
@ -55,22 +60,21 @@ impl Runner{
Ok(runner)
}
pub fn runnable_context<'a>(self,context:&'a mut Context)->Result<Runnable<'a>,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<Runnable<'a>,Error>{
{
let globals=self.lua.globals();
globals.set("game",super::instance::InstanceRef::new(services.game)).map_err(Error::RustLua)?;
globals.set("workspace",super::instance::InstanceRef::new(services.workspace)).map_err(Error::RustLua)?;
globals.set("game",super::instance::Instance::new_unchecked(context.services.game)).map_err(Error::RustLua)?;
globals.set("workspace",super::instance::Instance::new_unchecked(context.services.workspace)).map_err(Error::RustLua)?;
}
//this makes set_app_data shut up about the lifetime
self.lua.set_app_data::<&'static mut WeakDom>(unsafe{core::mem::transmute(&mut context.dom)});
// SAFETY: This is not a &'static mut WeakDom,
// but as long as Runnable<'a> holds the lifetime of &'a mut Context
// it is a valid unique reference.
let ptr=&mut context.dom as *mut rbx_dom_weak::WeakDom;
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{&mut*ptr});
#[cfg(feature="run-service")]
self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
Ok(Runnable{
lua:self.lua,
_lifetime:&std::marker::PhantomData
_lifetime:std::marker::PhantomData
})
}
}
@ -78,18 +82,18 @@ impl Runner{
//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<()>
_lifetime:std::marker::PhantomData<&'a ()>
}
impl Runnable<'_>{
pub fn drop_context(self)->Runner{
self.lua.remove_app_data::<&'static mut WeakDom>();
self.lua.remove_app_data::<crate::context::LuaAppData>();
#[cfg(feature="run-service")]
self.lua.remove_app_data::<crate::scheduler::Scheduler>();
Runner{
lua:self.lua,
}
}
pub fn run_script(&self,script:super::instance::InstanceRef)->Result<(),Error>{
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())
@ -126,7 +130,7 @@ impl Runnable<'_>{
pub fn run_service_step(&self)->Result<(),mlua::Error>{
let render_stepped_signal=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"))?;
Ok(match run_service.userdata.get(&rbx_dom_weak::ustr("RenderStepped")){
Ok(match run_service.userdata.get(&static_ustr("RenderStepped")){
Some(render_stepped)=>Some(render_stepped.borrow::<super::script_signal::ScriptSignal>()?.clone()),
None=>None
})

@ -98,12 +98,9 @@ impl ScriptConnection{
impl mlua::UserData for ScriptSignal{
fn add_methods<M:mlua::UserDataMethods<Self>>(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))
);
methods.add_method("connect",|_lua,this,f:mlua::Function|Ok(this.connect(f)));
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))
@ -164,6 +161,7 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
.call::<mlua::Function>(())?;
lua.register_userdata_type::<ScriptSignal>(|reg|{
reg.add_field("wait",wait.clone());
reg.add_field("Wait",wait);
mlua::UserData::register(reg);
})?;

@ -0,0 +1,42 @@
#[cfg(not(feature="run-service"))]
fn no_op(_lua:&mlua::Lua,_time:Option<super::number::Number>)->mlua::Result<f64>{
Ok(0.0)
}
fn tick(_lua:&mlua::Lua,_:())->mlua::Result<f64>{
Ok(0.0)
}
// 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::<mlua::Table>("coroutine")?;
#[cfg(feature="run-service")]
let schedule_thread=lua.create_function(crate::scheduler::schedule_thread)?;
#[cfg(not(feature="run-service"))]
let schedule_thread=lua.create_function(no_op)?;
//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::<mlua::Function>(())?;
globals.raw_set("wait",wait)?;
// TODO: move this somewhere it belongs
let tick=lua.create_function(tick)?;
globals.raw_set("tick",tick)?;
Ok(())
}

@ -0,0 +1,35 @@
use super::instance::Instance;
use super::tween_info::TweenInfo;
#[allow(dead_code)]
#[derive(Clone)]
pub struct Tween{
instance:Instance,
tween_info:TweenInfo,
goal:mlua::Table,
playback_state:rbx_types::Enum,
}
impl Tween{
pub fn create(
instance:Instance,
tween_info:TweenInfo,
goal:mlua::Table,
)->Self{
Self{
instance,
tween_info,
goal,
// Enum.PlaybackState.Begin
playback_state:rbx_types::Enum::from_u32(0),
}
}
pub fn play(&mut self){
}
}
impl mlua::UserData for Tween{
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method_mut("Play",|_,this,()|Ok(this.play()))
}
}
type_from_lua_userdata_clone!(Tween);

@ -0,0 +1,45 @@
use super::number::Number;
use super::r#enum::{CoerceEnum,Enums};
#[allow(dead_code)]
#[derive(Clone)]
pub struct TweenInfo{
time:f64,
easing_style:rbx_types::Enum,
easing_direction:rbx_types::Enum,
repeat_count:u32,
reverses:bool,
delay_time:f64,
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(time,easing_style,easing_direction,repeat_count,reverses,delay_time):(Option<Number>,Option<CoerceEnum>,Option<CoerceEnum>,Option<u32>,Option<bool>,Option<Number>)|{
Ok(TweenInfo{
time:time.map_or(1.0,Number::to_f64),
easing_style:match easing_style{
// Enum.EasingStyle.Quad
None=>rbx_types::Enum::from_u32(3),
Some(e)=>e.coerce_to(Enums.get("EasingStyle").unwrap())?.into(),
},
easing_direction:match easing_direction{
// Enum.EasingDirection.Out
None=>rbx_types::Enum::from_u32(1),
Some(e)=>e.coerce_to(Enums.get("EasingDirection").unwrap())?.into(),
},
repeat_count:repeat_count.unwrap_or(0),
reverses:reverses.unwrap_or(false),
delay_time:delay_time.map_or(0.0,Number::to_f64),
})
})?
)?;
globals.set("TweenInfo",table)?;
Ok(())
}
impl mlua::UserData for TweenInfo{}
type_from_lua_userdata_clone!(TweenInfo);

@ -0,0 +1,36 @@
use super::number::Number;
#[derive(Clone,Copy)]
pub struct UDim(rbx_types::UDim);
impl UDim{
pub fn new(scale:f32,offset:i32)->Self{
UDim(rbx_types::UDim::new(scale,offset))
}
}
impl From<rbx_types::UDim> for UDim{
fn from(value:rbx_types::UDim)->Self{
Self(value)
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(scale,offset):(Number,i32)|
Ok(UDim::new(scale.into(),offset))
)?
)?;
globals.set("UDim",table)?;
Ok(())
}
impl mlua::UserData for UDim{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Scale",|_,UDim(this)|Ok(this.scale));
fields.add_field_method_get("Offset",|_,UDim(this)|Ok(this.offset));
}
}
type_from_lua_userdata!(UDim);

@ -0,0 +1,40 @@
use super::udim::UDim;
use super::number::Number;
#[derive(Clone,Copy)]
pub struct UDim2(rbx_types::UDim2);
impl UDim2{
pub fn new(sx:f32,ox:i32,sy:f32,oy:i32)->Self{
UDim2(rbx_types::UDim2::new(
rbx_types::UDim::new(sx,ox),
rbx_types::UDim::new(sy,oy),
))
}
}
impl From<UDim2> for rbx_types::UDim2{
fn from(UDim2(value):UDim2)->rbx_types::UDim2{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(sx,ox,sy,oy):(Number,i32,Number,i32)|
Ok(UDim2::new(sx.into(),ox,sy.into(),oy))
)?
)?;
globals.set("UDim2",table)?;
Ok(())
}
impl mlua::UserData for UDim2{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("X",|_,UDim2(this)|Ok(UDim::from(this.x)));
fields.add_field_method_get("Y",|_,UDim2(this)|Ok(UDim::from(this.y)));
}
}
type_from_lua_userdata!(UDim2);

@ -0,0 +1,93 @@
use mlua::FromLua;
use super::number::Number;
#[derive(Clone,Copy)]
pub struct Vector2(glam::Vec2);
impl Vector2{
pub const fn new(x:f32,y:f32)->Self{
Self(glam::vec2(x,y))
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
//Vector2.new
table.raw_set("new",
lua.create_function(|_,(x,y):(Option<Number>,Option<Number>)|
match (x,y){
(Some(x),Some(y))=>Ok(Vector2::new(x.into(),y.into())),
(None,None)=>Ok(Vector2(glam::Vec2::ZERO)),
_=>Err(mlua::Error::runtime("Unsupported arguments to Vector2.new")),
}
)?
)?;
globals.set("Vector2",table)?;
Ok(())
}
impl From<Vector2> for rbx_types::Vector2{
fn from(Vector2(v):Vector2)->rbx_types::Vector2{
rbx_types::Vector2::new(v.x,v.y)
}
}
impl From<rbx_types::Vector2> for Vector2{
fn from(value:rbx_types::Vector2)->Vector2{
Vector2::new(value.x,value.y)
}
}
impl mlua::UserData for Vector2{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("magnitude",|_,Vector2(this)|Ok(this.length()));
fields.add_field_method_get("Magnitude",|_,Vector2(this)|Ok(this.length()));
fields.add_field_method_get("unit",|_,Vector2(this)|Ok(Vector2(this.normalize())));
fields.add_field_method_get("Unit",|_,Vector2(this)|Ok(Vector2(this.normalize())));
fields.add_field_method_get("x",|_,Vector2(this)|Ok(this.x));
fields.add_field_method_get("X",|_,Vector2(this)|Ok(this.x));
fields.add_field_method_get("y",|_,Vector2(this)|Ok(this.y));
fields.add_field_method_get("Y",|_,Vector2(this)|Ok(this.y));
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
//methods.add_method("area",|_,this,()| Ok(this.length * this.width));
methods.add_meta_function(mlua::MetaMethod::Add,|_,(Vector2(this),Vector2(val)):(Self,Self)|Ok(Self(this+val)));
methods.add_meta_function(mlua::MetaMethod::Sub,|_,(Vector2(this),Vector2(val)):(Self,Self)|Ok(Self(this-val)));
methods.add_meta_function(mlua::MetaMethod::Mul,|lua,(lhs,rhs):(mlua::Value,mlua::Value)|{
match (lhs,rhs){
(mlua::Value::UserData(lhs),mlua::Value::UserData(rhs))=>lhs.borrow_scoped(|Vector2(lhs):&Vector2|rhs.borrow_scoped(|Vector2(rhs):&Vector2|Self(lhs*rhs)))?,
(lhs,mlua::Value::UserData(rhs))=>{
let lhs=Number::from_lua(lhs,lua)?;
rhs.borrow_scoped(|Vector2(rhs):&Vector2|Self(lhs.to_f32()*rhs))
},
(mlua::Value::UserData(lhs),rhs)=>{
let rhs=Number::from_lua(rhs,lua)?;
lhs.borrow_scoped(|Vector2(lhs):&Vector2|Self(lhs*rhs.to_f32()))
},
_=>Err(mlua::Error::runtime(format!("Expected Vector2")))
}
});
methods.add_meta_function(mlua::MetaMethod::Div,|_,(Vector2(this),val):(Self,mlua::Value)|{
match val{
mlua::Value::Integer(n)=>Ok(Self(this/(n as f32))),
mlua::Value::Number(n)=>Ok(Self(this/(n as f32))),
mlua::Value::UserData(ud)=>ud.borrow_scoped(|Vector2(rhs):&Vector2|Self(this/rhs)),
other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector2 by {other:?}"))),
}
});
methods.add_meta_function(mlua::MetaMethod::ToString,|_,Vector2(this):Self|
Ok(format!("Vector2.new({},{})",
this.x,
this.y,
))
);
}
}
type_from_lua_userdata!(Vector2);

@ -1,3 +1,7 @@
use mlua::FromLua;
use super::number::Number;
#[derive(Clone,Copy)]
pub struct Vector3(pub(crate)glam::Vec3A);
@ -8,23 +12,27 @@ impl Vector3{
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let vector3_table=lua.create_table()?;
let 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))
table.raw_set("new",
lua.create_function(|_,(x,y,z):(Option<Number>,Option<Number>,Option<Number>)|
match (x,y,z){
(Some(x),Some(y),Some(z))=>Ok(Vector3::new(x.into(),y.into(),z.into())),
(None,None,None)=>Ok(Vector3(glam::Vec3A::ZERO)),
_=>Err(mlua::Error::runtime("Unsupported arguments to Vector3.new")),
}
)?
)?;
globals.set("Vector3",vector3_table)?;
globals.set("Vector3",table)?;
Ok(())
}
impl Into<rbx_types::Vector3> for Vector3{
fn into(self)->rbx_types::Vector3{
rbx_types::Vector3::new(self.0.x,self.0.y,self.0.z)
impl From<Vector3> for rbx_types::Vector3{
fn from(Vector3(v):Vector3)->rbx_types::Vector3{
rbx_types::Vector3::new(v.x,v.y,v.z)
}
}
@ -36,44 +44,50 @@ impl From<rbx_types::Vector3> for Vector3{
impl mlua::UserData for Vector3{
fn add_fields<F:mlua::UserDataFields<Self>>(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(())
});
fields.add_field_method_get("magnitude",|_,Vector3(this)|Ok(this.length()));
fields.add_field_method_get("Magnitude",|_,Vector3(this)|Ok(this.length()));
fields.add_field_method_get("unit",|_,Vector3(this)|Ok(Vector3(this.normalize())));
fields.add_field_method_get("Unit",|_,Vector3(this)|Ok(Vector3(this.normalize())));
fields.add_field_method_get("x",|_,Vector3(this)|Ok(this.x));
fields.add_field_method_get("X",|_,Vector3(this)|Ok(this.x));
fields.add_field_method_get("y",|_,Vector3(this)|Ok(this.y));
fields.add_field_method_get("Y",|_,Vector3(this)|Ok(this.y));
fields.add_field_method_get("z",|_,Vector3(this)|Ok(this.z));
fields.add_field_method_get("Z",|_,Vector3(this)|Ok(this.z));
}
fn add_methods<M:mlua::UserDataMethods<Self>>(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))
methods.add_meta_function(mlua::MetaMethod::Add,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this+val)));
methods.add_meta_function(mlua::MetaMethod::Sub,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this-val)));
methods.add_meta_function(mlua::MetaMethod::Mul,|lua,(lhs,rhs):(mlua::Value,mlua::Value)|{
match (lhs,rhs){
(mlua::Value::UserData(lhs),mlua::Value::UserData(rhs))=>lhs.borrow_scoped(|Vector3(lhs):&Vector3|rhs.borrow_scoped(|Vector3(rhs):&Vector3|Self(lhs*rhs)))?,
(lhs,mlua::Value::UserData(rhs))=>{
let lhs=Number::from_lua(lhs,lua)?;
rhs.borrow_scoped(|Vector3(rhs):&Vector3|Self(lhs.to_f32()*rhs))
},
(mlua::Value::UserData(lhs),rhs)=>{
let rhs=Number::from_lua(rhs,lua)?;
lhs.borrow_scoped(|Vector3(lhs):&Vector3|Self(lhs*rhs.to_f32()))
},
_=>Err(mlua::Error::runtime(format!("Expected Vector3")))
}
});
methods.add_meta_function(mlua::MetaMethod::Div,|_,(Vector3(this),val):(Self,mlua::Value)|{
match val{
mlua::Value::Integer(n)=>Ok(Self(this/(n as f32))),
mlua::Value::Number(n)=>Ok(Self(this/(n as f32))),
mlua::Value::UserData(ud)=>ud.borrow_scoped(|Vector3(rhs):&Vector3|Self(this/rhs)),
other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector3 by {other:?}"))),
}
});
methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self|
methods.add_meta_function(mlua::MetaMethod::ToString,|_,Vector3(this):Self|
Ok(format!("Vector3.new({},{},{})",
this.0.x,
this.0.y,
this.0.z,
this.x,
this.y,
this.z,
))
);
}

@ -51,7 +51,7 @@ pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::S
f(&mut *scheduler)
}
fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
pub 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)=>{
@ -75,32 +75,3 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
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::<mlua::Table>("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::<mlua::Function>(())?;
globals.raw_set("wait",wait)?;
Ok(())
}

@ -1,57 +0,0 @@
pub struct Instance{
pub(crate) instance:rbx_dom_weak::Instance,
pub(crate) userdata:rbx_dom_weak::UstrMap<mlua::AnyUserData>,
}
impl AsRef<rbx_dom_weak::Instance> for Instance{
fn as_ref(&self)->&rbx_dom_weak::Instance{
&self.instance
}
}
impl AsMut<rbx_dom_weak::Instance> for Instance{
fn as_mut(&mut self)->&mut rbx_dom_weak::Instance{
&mut self.instance
}
}
impl From<Instance> for rbx_dom_weak::Instance{
fn from(instance:Instance)->Self{
instance.instance
}
}
impl From<rbx_dom_weak::Instance> for Instance{
fn from(instance:rbx_dom_weak::Instance)->Self{
Self{
instance,
userdata:Default::default(),
}
}
}
pub type WeakDom=rbx_dom_weak::GenericWeakDom<Instance>;
// lazy-loaded per-instance userdata values
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
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()),
},
};
impl Instance{
pub fn get_or_create_userdata(&mut self,lua:&mlua::Lua,index:rbx_dom_weak::Ustr)->mlua::Result<Option<mlua::AnyUserData>>{
use std::collections::hash_map::Entry;
Ok(match LAZY_USER_DATA.get(self.instance.class.as_str()){
Some(userdata_map)=>match self.userdata.entry(index){
Entry::Occupied(entry)=>Some(entry.get().clone()),
Entry::Vacant(entry)=>match userdata_map.get(index.as_str()){
Some(create_userdata)=>Some(entry.insert(create_userdata(lua)?).clone()),
None=>None,
},
},
None=>None,
})
}
}

@ -0,0 +1,3 @@
pub fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}

@ -56,8 +56,6 @@ impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
}
}
pub type Angle32=i32;
pub type Planar64=i64;
pub type Planar64Vec3=[i64;3];
pub type Planar64Mat3=[i64;9];
pub type Planar64Affine3=[i64;12];

@ -14,10 +14,10 @@ image = "0.25.2"
image_dds = "0.7.1"
lazy-regex = "3.1.0"
rbx_asset = { version = "0.4.4", registry = "strafesnet" }
rbx_binary = { version = "1.1.0-sn3", registry = "strafesnet"}
rbx_dom_weak = { version = "3.1.0-sn1", registry = "strafesnet"}
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet" }
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn3", registry = "strafesnet"}
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.0", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../lib/deferred_loader", registry = "strafesnet" }
@ -25,7 +25,7 @@ strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registr
strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.8.0"
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vmt-parser = "0.2.0"

@ -3,11 +3,16 @@ use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use rbx_dom_weak::{ustr,Instance};
use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)]
@ -102,8 +107,8 @@ SurfaceAppearance.RoughnessMapContent
WrapLayer.ReferenceMeshContent
*/
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&ustr(property))else{
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
@ -117,8 +122,8 @@ fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,
};
content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&ustr(property))else{
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
@ -144,8 +149,8 @@ impl UniqueAssets{
fn collect(&mut self,object:&Instance){
match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent");
@ -418,7 +423,7 @@ async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
std::io::Cursor::new(entire_file)
).map_err(ConvertError::RobloxRead)?;
let mut place=model.into_place();
let mut place=strafesnet_rbx_loader::Place::from(model);
place.run_scripts();
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;

@ -97,7 +97,7 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{
let mut place=model.into_place();
let mut place=strafesnet_rbx_loader::Place::from(model);
place.run_scripts();
Ok(LoadFormat::Map(
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?