Compare commits

...

278 Commits

Author SHA1 Message Date
73e3181d0c roblox_emulator: v0.5.2 2025-11-27 16:42:01 -08:00
19ba8f2445 update deps 2025-11-27 15:50:19 -08:00
0495d07e26 update rbx-dom 2025-11-27 15:48:17 -08:00
0ea353b27d common: fixed_wide: min max 2025-11-24 13:04:44 -08:00
99706079d9 common: fixed_wide: add mul_sign div_sign 2025-11-24 13:04:44 -08:00
730c5fb7dd common: integer: generic zero 2025-11-22 08:47:16 -08:00
d1b61bb997 push_solve: remove epsilon 2025-11-21 10:52:34 -08:00
0343ad19cf MeshQuery::hint_point returns any point inside the mesh 2025-11-20 10:59:08 -08:00
43210b1417 less access to TouchingState private fields 2025-11-19 13:39:07 -08:00
e9d28cf15f document jank 2025-11-19 13:15:31 -08:00
452bac4049 change collision_end_contact & collision_end_intersect fn signatures 2025-11-19 10:57:44 -08:00
48aad78f59 change contact_normal function signature to reduce copies 2025-11-19 10:20:33 -08:00
d45a42f5aa change ContactCollision struct layout
Match TouchingState contacts HashMap K,V layout to try to get lucky with compiler optimization.
2025-11-19 10:20:33 -08:00
c219fec3bc specialize touching member access 2025-11-19 10:08:40 -08:00
2a05d50abb check touching before testing collision 2025-11-19 10:08:40 -08:00
fbb047f8d4 combine call chain 2025-11-19 09:01:51 -08:00
c4d837a552 Fix infinite loop with intersects when allowing 0s collisions 2025-11-19 09:01:51 -08:00
a08bd44789 Generic ConvexMeshId 2025-11-19 09:01:51 -08:00
4ae5359046 rename not_spawn_at to is_not_spawn_at 2025-11-19 09:01:27 -08:00
15ecaf602a deep match 2025-11-18 12:29:46 -08:00
1e0511a7ba remove intermediate allocation 2025-11-18 12:23:05 -08:00
a9e4705d89 remove (some) fixed point implicit conversion
They may be convenient, but they cannot be done at compile-time.
TODO: remove more of them i.e. impl_multiplicative_operator
2025-11-18 11:53:52 -08:00
98069859b5 Gracefully handle 0 acceleration for walking targets 2025-11-18 19:47:04 +00:00
64d3996fa9 use From instead of Into 2025-11-18 11:46:32 -08:00
49c0c16e35 Use a From implementation instead of manual conversion
If the contacts and intersects map ever change in the future to not be 1:1 with gaps but instead something else, this guarantees that this implicit use of the relationship will flag at a compiler level
2025-11-18 19:25:44 +00:00
255bed4803 Ensure the PhysicsData's bvh respects the original model ordering
There's no importance in worrying about the core HashMap ordering since it's not used as an iterator except for outside of this very function for bvh purposes
2025-11-18 19:25:44 +00:00
128e137829 remove redundant code 2025-11-17 13:22:46 -08:00
1e19f804cc custom hex Debug print for Fixed 2025-11-17 12:45:55 -08:00
f6f35c5f54 fix lints 2025-11-17 12:41:34 -08:00
4e7d580918 add lints to workspace 2025-11-16 14:53:23 -08:00
8d5a100a2e update deps 2025-11-09 05:48:31 -08:00
91208db706 drop lazy_regex dep 2025-11-09 05:47:33 -08:00
5a320b514e fix style 2025-11-07 16:52:50 -08:00
661d706a22 common: aabb: area_weight fn 2025-10-17 15:48:59 +01:00
5550d5771e common: bvh: reduce variable scope 2025-10-17 15:03:54 +01:00
c834d1d1ca common: bvh: name constant 2025-10-17 14:57:53 +01:00
ca9d2238a7 common: bvh: tweak code style 2025-10-17 14:57:53 +01:00
f3bb8dd067 update deps 2025-10-01 23:37:07 -07:00
e58f9b9ff2 rbx_loader: silently filter vertices which fail to convert 2025-09-29 19:45:16 -07:00
54c4ed6bad update deps 2025-08-30 15:20:23 -07:00
9aceafa0df roblox_emulator: fix lints 2025-08-30 15:15:23 -07:00
d065bac130 Revert "roblox_emulator: use extended instances"
This reverts commit bb8e131464.
2025-08-30 15:13:28 -07:00
a4d0393556 update rbx-dom 2025-08-30 15:12:22 -07:00
3692d7f79e it: fix bug 3 test 2025-08-29 19:07:57 -07:00
7e49840768 it: set gravity to 0 2025-08-29 19:06:51 -07:00
4ecdd547c6 it: use instruction iter 2025-08-29 18:30:28 -07:00
b0365165e8 physics: create iterator over internal instructions 2025-08-29 18:30:18 -07:00
c2ff52a2ae instruction: iterator 2025-08-29 18:30:18 -07:00
6e778869e8 it: bug 3 test scene 2025-08-29 18:30:18 -07:00
6509bef070 it: add test scene 2025-08-29 16:47:46 -07:00
0fa097a004 aabb: tweak Aabb.contains 2025-08-29 15:40:00 -07:00
55d4b1d264 physics: PhysicsData is immutable after construction 2025-08-28 16:37:48 -07:00
ea28663e95 physics: move code 2025-08-28 16:02:23 -07:00
bac9be9684 physics: add edge case tests 2025-08-28 14:49:36 -07:00
7e76f3309b ignore debugger config 2025-08-26 16:54:36 -07:00
cfd9550566 common: truncate vertex precision to 16 bits in MeshBuilder
The physics algorithm expects vertices to align exactly with faces.  Since the face normal is calculated via the cross product of vertex positions, this allows the face normals to be exact with respect to the vertex positions.
2025-08-26 16:09:15 -07:00
bd16720b5a rbx_loader: refactor mesh convert to use MeshBuilder 2025-08-26 15:55:55 -07:00
633f767d0f snf: disable demo code 2025-08-26 15:55:55 -07:00
602b63e953 fix lifetime lints 2025-08-26 15:55:55 -07:00
6abe622885 fix dead code lints 2025-08-26 15:55:55 -07:00
0ab23dde2b move dev config to strafe-client only 2025-08-26 13:26:14 -07:00
657a2530dc update deps 2025-08-19 21:59:25 -07:00
c4bd034928 update rbx_mesh with CSGMDL5 support 2025-07-23 23:26:36 -07:00
bbcdac8879 rbx_loader: fix dead code lints 2025-07-23 23:26:36 -07:00
38b3f3d7a3 use expect instead of allow 2025-07-19 02:25:55 -07:00
eb80c8b9b5 update deps 2025-06-12 04:56:48 -07:00
63714f190d update snf binrw 2025-05-26 15:36:49 -07:00
f50dfb9399 update rbx_mesh 2025-05-26 15:33:38 -07:00
9db39d2a62 physics: face crawler opti 2025-05-23 14:40:06 -07:00
9e2e1d9d4a it: add physics bug tests, use cargo test -- --ignored 2025-05-22 15:37:10 -07:00
6f1548403a it: split into modules 2025-05-22 13:41:43 -07:00
5f3e998b3d map-tool: v1.7.2 provide download cookie 2025-05-20 16:30:44 -07:00
2d8792be4f tools: add clarion shortcut 2025-05-20 15:52:41 -07:00
15d33eb49d map-tool: cookie arg, now required by roblox 2025-05-20 15:35:03 -07:00
156dacb838 map-tool: v1.7.1 rbx_loader error reports 2025-05-16 16:04:55 -07:00
a7f0e431cb snf: v0.3.1 update common 2025-05-16 15:58:25 -07:00
0ed3cb2adb bsp_loader: v0.3.1 update common 2025-05-16 15:56:19 -07:00
bac43eab66 rbx_loader: v0.7.0 error reporting 2025-05-16 15:55:56 -07:00
2a257236fd deferred_loader: v0.5.1 update common 2025-05-16 15:55:47 -07:00
8fe2c20635 common: v0.7.0 misc 2025-05-16 15:49:12 -07:00
2da7ccce7c fixed_wide: v0.2.1 impl Display for FixedFromFloatError 2025-05-16 15:45:21 -07:00
89e6d11630 update deps 2025-05-16 15:42:19 -07:00
6abb40b6d2 roblox_emulator: v0.5.1 2025-05-16 15:35:54 -07:00
4dcf06c44c rbx_loader: support Seat, VehicleSeat, SpawnLocation 2025-05-16 15:26:59 -07:00
0171a711d9 rbx_loader: skip terrain 2025-05-16 15:08:48 -07:00
3eb702eaea roblox_emulator: give terrain BasePart properties to dodge error 2025-05-16 15:08:48 -07:00
92878a4ae8 strafe-client: print error report 2025-05-16 15:08:48 -07:00
88dcf40d77 map-tool: print error report 2025-05-16 15:08:48 -07:00
df9fcb5b02 rbx_loader: impl Display for RecoverableErrors 2025-05-16 15:08:48 -07:00
b07064cc9d common: impl Display for ModeId 2025-05-16 15:08:43 -07:00
6e435e46ac fixed_wide: impl Display for FixedFromFloatError 2025-05-16 14:21:32 -07:00
ce0368590d roblox_emulator: unused error variant 2025-05-16 14:21:32 -07:00
f896e6cfff rbx_loader: report script errors 2025-05-16 14:21:32 -07:00
dcc91db6f7 rbx_loader: refactor to make RecoverableError report 2025-05-16 14:21:32 -07:00
b6d6878137 physics: body double clone/copy fixups 2025-05-14 18:15:26 -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
a3d39a81d0 update rbx-dom 2025-04-17 13:35:04 -07:00
cb31c847d5 map-tool: code clean ups 2025-04-14 21:23:23 -07:00
f579370841 update deps 2025-04-14 21:23:23 -07:00
2a542e0026 remove pointless PhantomData 2025-04-13 21:23:21 -07:00
ca51256a16 deferred_loader: remove PhantomData using GATs 2025-04-13 20:11:47 -07:00
65e17da386 rbx_loader: deconstruct instead of index tuple (with vengeance) 2025-03-21 13:03:55 -07:00
1cf21be185 rbx_loader: rename primitive functions to reflect what it does 2025-03-21 12:55:04 -07:00
413e176741 todo 2025-03-21 12:53:29 -07:00
39b2397d5e rbx_loader: implement roblox cylinder 2025-03-21 12:51:53 -07:00
2ed01dffbd physics: TODO 2025-03-20 18:28:15 -07:00
d0ee597416 rbx_loader: refactor primitive types 2025-03-20 18:28:15 -07:00
be85530000 rbx_loader: primitives: code cleanup 2025-03-20 18:28:15 -07:00
55ff199269 physics: [BUG 6] update move state on collision end with non-walk contact while walking 2025-03-20 13:32:52 -07:00
02615af1fd common: integer: angle constructor 2025-03-19 16:34:42 -07:00
6594ffa87e common: integer: more zero constants 2025-03-19 16:34:38 -07:00
6295dc68ef physics: fix lint 2025-03-19 12:52:54 -07:00
8fb0d30e1d graphics: move get_instances_buffer_data away from the cool render code 2025-03-14 15:38:35 -07:00
0fb0230cb1 tools: create dev profile 2025-03-14 15:24:47 -07:00
bdb1090664 physics: implement precise width conversion 2025-03-13 17:06:23 -07:00
3ce7746489 snf: implement precise width conversion 2025-03-13 17:06:23 -07:00
7978050f8a rbx_loader: implement precise width conversion 2025-03-13 17:06:23 -07:00
77ee36fa72 bsp_loader: implement precise width conversion 2025-03-13 17:06:23 -07:00
0fc1ec3086 common: implement precise width conversion 2025-03-13 16:57:36 -07:00
f60c03ac56 common: update deps 2025-03-13 16:57:36 -07:00
b570d8809d linear_ops: v0.1.1 unwrap vector of results 2025-03-13 16:57:36 -07:00
3f268ec034 linear_ops: allow unwraping vector of results 2025-03-13 16:57:36 -07:00
ba161ef6d1 fixed_wide: v0.2.0 precise width conversion 2025-03-13 16:57:36 -07:00
0755d4413c fixed_wide: expand upon width conversion 2025-03-13 16:13:03 -07:00
459cb13957 physics: monomorphise MeshQuery over AsRef 2025-03-12 19:34:45 -07:00
a5ad7d4d6e it: cli 2025-03-12 17:23:21 -07:00
3d5e76f078 bsp_loader: fix utf8 brush model 2025-03-11 18:13:39 -07:00
78c53ae429 physics: set style on reset and spawn 2025-03-11 17:09:29 -07:00
e794b2649c bsp_loader: implement ladders and water 2025-03-11 16:41:46 -07:00
50f6fe5bd8 common: fix source styles 2025-03-11 15:26:45 -07:00
fc3681f41f it: read_entire_file use built in 2025-03-11 14:07:24 -07:00
a2369b4211 rbx_loader: never do &Vec + deconstruct Face2 (style) 2025-03-09 23:47:23 -07:00
bfea49ffae it: measure simulate speed 2025-03-08 17:39:47 -08:00
8c7063d340 common: aabb: make function inline-able 2025-03-08 14:34:54 -08:00
651150760c common: aabb: use midpoint for center 2025-03-08 14:34:54 -08:00
3d203e2da9 fixed_wide: add midpoint 2025-03-08 14:34:54 -08:00
3798f438cf use as_bits_mut to optimize sqrt 2025-03-08 13:58:56 -08:00
4ace8317fe update deps 2025-03-08 13:55:14 -08:00
ab4a9bb922 bsp_loader: generate prop matrix with euler angles 2025-03-02 18:23:47 -08:00
a9ef07ce78 update vbsp 2025-03-02 18:23:47 -08:00
561e41c760 common: delete unused ModesUpdate 2025-02-28 13:15:55 -08:00
9896a09576 common: delete updatable trait 2025-02-28 13:15:55 -08:00
82840fa6cb refactor type safety for modes data normalization 2025-02-28 13:15:55 -08:00
2c87cb71df common: gameplay_modes: rename internal field on ModeBuilder 2025-02-28 12:37:49 -08:00
709f622547 common: don't use .0 for newtypes 2025-02-28 12:10:16 -08:00
b20264b382 common: move ModesBuilder to common 2025-02-28 11:08:38 -08:00
17cce2d702 silence lints 2025-02-28 10:50:53 -08:00
a2b2ddc9ce bsp_loader: use texture_info function 2025-02-26 15:29:23 -08:00
cb57d893e0 update vbsp fork 2025-02-26 15:26:56 -08:00
9e8d66cec1 map-tool: gimme_them_textures iterate over a shorter list of prop model paths 2025-02-26 12:00:51 -08:00
39fe833198 map-tool: grab brush models 2025-02-26 12:00:51 -08:00
e7688a95fd bsp_loader: generate physics from brushes 2025-02-26 12:00:51 -08:00
d2cc98b04d deps: fork vbsp 2025-02-26 11:59:59 -08:00
9e887580af bsp_loader: valve_transform_{dist|normal} 2025-02-21 13:18:20 -08:00
92feac572e bsp_loader: include missing model path in error 2025-02-21 13:18:20 -08:00
fd02a40783 common: CollisionAttributes::intersect_default 2025-02-21 13:18:20 -08:00
7c787a0e0f linear_ops: repr(transparent) to ward off UB 2025-02-21 13:18:20 -08:00
6a7c076203 Planar64Affine3::IDENTITY 2025-02-21 13:18:20 -08:00
af3abbcb4d Planar64Affine3::from_translation 2025-02-21 13:14:57 -08:00
4859c37780 common: BvhNode::sample_ray 2025-02-21 12:54:13 -08:00
19e65802f6 common: Aabb::contains(point) 2025-02-21 12:54:13 -08:00
2cf1547423 common: Time is Ord 2025-02-21 12:54:13 -08:00
63305f91c7 common: Ray module 2025-02-21 12:54:13 -08:00
e875826250 TransformedMesh::faces 2025-02-21 12:54:13 -08:00
0a44c1630f ratio_ops v0.1.1 fix Ord 2025-02-21 12:54:13 -08:00
5cffc03ef6 ratio_ops: fix Ord for Ratio 2025-02-21 12:53:14 -08:00
113 changed files with 6886 additions and 3664 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
.zed

2389
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,3 +24,14 @@ resolver = "2"
#lto = true
strip = true
codegen-units = 1
[workspace.lints.rust]
# unsafe_code = "forbid"
# missing_docs = "warn"
# missing_debug_implementations = "warn"
single_use_lifetimes = "warn"
trivial_casts = "warn"
unused_lifetimes = "warn"
unused_qualifications = "warn"
# variant_size_differences = "warn"
unexpected_cfgs = "warn"

View File

@@ -11,4 +11,7 @@ id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "24.0.0"
wgpu = "27.0.0"
[lints]
workspace = true

View File

@@ -94,7 +94,7 @@ impl GraphicsCamera{
raw
}
}
impl std::default::Default for GraphicsCamera{
impl Default for GraphicsCamera{
fn default()->Self{
Self{
screen_size:glam::UVec2::ONE,
@@ -103,6 +103,26 @@ impl std::default::Default for GraphicsCamera{
}
}
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}
pub struct GraphicsState{
pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups,
@@ -147,7 +167,7 @@ impl GraphicsState{
}
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
//generate texture view per texture
let texture_views:HashMap<strafesnet_common::model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
let texture_views:HashMap<model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
let texture_id=model::TextureId::new(texture_id as u32);
let image=match ddsfile::Dds::read(std::io::Cursor::new(texture_data)){
Ok(image)=>image,
@@ -783,7 +803,7 @@ impl GraphicsState{
module:&shader,
entry_point:Some("vs_entity_texture"),
buffers:&[wgpu::VertexBufferLayout{
array_stride:std::mem::size_of::<GraphicsVertex>() as wgpu::BufferAddress,
array_stride:size_of::<GraphicsVertex>() as wgpu::BufferAddress,
step_mode:wgpu::VertexStepMode::Vertex,
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4],
}],
@@ -933,6 +953,7 @@ impl GraphicsState{
}),
store:wgpu::StoreOp::Store,
},
depth_slice:None,
})],
depth_stencil_attachment:Some(wgpu::RenderPassDepthStencilAttachment{
view:&self.depth_view,
@@ -967,22 +988,3 @@ impl GraphicsState{
self.staging_belt.recall();
}
}
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}

View File

@@ -8,3 +8,6 @@ arrayvec = "0.7.6"
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -18,11 +18,22 @@ impl<T> std::ops::Neg for Body<T>{
}
}
}
impl<T:Copy> std::ops::Neg for &Body<T>{
type Output=Body<T>;
fn neg(self)->Self::Output{
Body{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
time:-self.time,
}
}
}
impl<T> Body<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const ZERO:Self=Self::new(vec3::zero(),vec3::zero(),vec3::zero(),Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
@@ -42,12 +53,12 @@ impl<T> Body<T>
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.position
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
+(self.velocity*dt).map(|elem|elem.divide().clamp_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_1())
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
}
pub fn advance_time(&mut self,time:Time<T>){
self.position=self.extrapolated_position(time);
@@ -71,12 +82,12 @@ impl<T> Body<T>
D2:Copy,
Planar64:core::ops::Mul<D2,Output=N4>,
N4:integer::Divide<D2,Output=T1>,
T1:integer::Fix<Planar64>,
T1:integer::Clamp<Planar64>,
{
// a*dt^2/2 + v*dt + p
// (a*dt/2+v)*dt+p
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
.map(|elem|elem.divide().fix())+self.position
.map(|elem|elem.divide().clamp())+self.position
}
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
@@ -85,10 +96,10 @@ impl<T> Body<T>
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
T1:integer::Clamp<Planar64>,
{
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt);
@@ -96,8 +107,8 @@ impl<T> Body<T>
self.time+=dt.into();
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
if self.velocity==vec3::zero(){
if self.acceleration==vec3::zero(){
None
}else{
Some(self.acceleration)

View File

@@ -1,7 +1,9 @@
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;
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<M>,GigaTime),
@@ -27,6 +29,47 @@ 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
@@ -35,9 +78,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
M::Vert:Copy,
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;
@@ -50,22 +93,23 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//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;
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::Hit(face_id,dt);
break;
}
}
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.face_edges(face_id).iter(){
for &directed_edge_id in mesh.face_edges(face_id).as_ref(){
let edge_n=mesh.directed_edge_n(directed_edge_id);
let n=n.cross(edge_n);
let verts=mesh.edge_verts(directed_edge_id.as_undirected());
let &[v0,v1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
//WARNING: d is moved out of the *2 block because of adding two vertices!
//WARNING: precision is swept under the rug!
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_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;
//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 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;
}
@@ -75,30 +119,33 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
},
&FEV::Edge(edge_id)=>{
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
let delta_pos=body.position*2-(mesh.vert(edge_verts[0])+mesh.vert(edge_verts[1]));
for (i,&edge_face_id) in mesh.edge_faces(edge_id).iter().enumerate(){
let &[ev0,ev1]=edge_verts.as_ref();
let (v0,v1)=(mesh.vert(ev0),mesh.vert(ev1));
let edge_n=v1-v0;
let delta_pos=body.position*2-(v0+v1);
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
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);
//WARNING yada yada d *2
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_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;
//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 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::Face(edge_face_id),dt);
break;
}
}
}
//test each vertex collision time, ignoring roots with zero or conflicting derivative
for (i,&vert_id) in edge_verts.iter().enumerate(){
for (i,&vert_id) in edge_verts.as_ref().iter().enumerate(){
//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(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
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());
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
}
@@ -108,13 +155,13 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
},
&FEV::Vert(vert_id)=>{
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.vert_edges(vert_id).iter(){
for &directed_edge_id in mesh.vert_edges(vert_id).as_ref(){
//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(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
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());
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
@@ -125,19 +172,13 @@ 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.fix_4(),r.den.fix_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_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));
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
match self.next_transition(mesh,relative_body,lower_bound,upper_bound){
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),
}
}

View File

@@ -1,6 +1,5 @@
use std::borrow::{Borrow,Cow};
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};
@@ -8,6 +7,13 @@ use strafesnet_common::physics::Time;
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
struct AsRefHelper<T>(T);
impl<T> AsRef<T> for AsRefHelper<T>{
fn as_ref(&self)->&T{
&self.0
}
}
pub trait UndirectedEdge{
type DirectedEdge:Copy+DirectedEdge;
fn as_directed(&self,parity:bool)->Self::DirectedEdge;
@@ -62,47 +68,54 @@ 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:Clone;
type Edge:Clone+DirectedEdge;
type Vert:Clone;
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
let verts=self.edge_verts(edge_id);
self.vert(verts[1].clone())-self.vert(verts[0].clone())
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
let verts=self.edge_verts(directed_edge_id.as_undirected());
(self.vert(verts[1].clone())-self.vert(verts[0].clone()))*((directed_edge_id.parity() as i64)*2-1)
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
/// This must return a point inside the mesh.
fn hint_point(&self)->Planar64Vec3;
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->Cow<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>;
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Vert;2]>;
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
@@ -112,6 +125,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
@@ -137,10 +151,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{
@@ -204,19 +220,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(),
@@ -224,21 +245,16 @@ 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{
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView<'_>{
PhysicsMeshView{
data:&self.data,
topology:&self.submeshes()[submesh_id.get() as usize],
}
}
pub fn submesh_views(&self)->impl Iterator<Item=PhysicsMeshView>{
pub fn submesh_views(&self)->impl Iterator<Item=PhysicsMeshView<'_>>{
self.submeshes().iter().map(|topology|PhysicsMeshView{
data:&self.data,
topology,
@@ -307,14 +323,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();
@@ -372,18 +394,14 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}
//assume face hash is stable, and there are no flush faces...
let face=Face{
normal:(normal/len as i64).divide().fix_1(),
dot:(dot/(len*len) as i64).fix_1(),
};
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
}
normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
};
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));
}
@@ -391,16 +409,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(),
}
@@ -410,12 +428,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,
@@ -430,28 +449,33 @@ impl MeshQuery for PhysicsMeshView<'_>{
let face_idx=self.topology.faces[face_id.get() as usize].get() as usize;
(self.data.faces[face_idx].normal,self.data.faces[face_idx].dot)
}
fn hint_point(&self)->Planar64Vec3{
// invariant: meshes always encompass the origin
vec3::zero()
}
//ideally I never calculate the vertex position, but I have to for the graphical meshes...
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
self.data.verts[vert_idx].0
}
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
}
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].faces)
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].faces)
}
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].verts)
}
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.vert_topology[vert_id.get() as usize].edges.as_slice()
}
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.topology.vert_topology[vert_id.get() as usize].faces.as_slice()
}
}
#[derive(Debug)]
pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
@@ -467,6 +491,7 @@ impl PhysicsMeshTransform{
}
}
#[derive(Debug)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
@@ -481,7 +506,7 @@ impl TransformedMesh<'_>{
transform,
}
}
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
pub fn verts<'a>(&'a self)->impl Iterator<Item=Vector3<Fixed<2,64>>>+'a{
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
@@ -510,26 +535,30 @@ impl MeshQuery for TransformedMesh<'_>{
(transformed_n,transformed_d)
}
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
// wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
}
fn hint_point(&self)->Planar64Vec3{
self.transform.vertex.translation
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
}
#[inline]
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
self.view.edge_faces(edge_id)
}
#[inline]
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
self.view.edge_verts(edge_id)
}
#[inline]
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.vert_edges(vert_id)
}
#[inline]
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.view.vert_faces(vert_id)
}
}
@@ -588,6 +617,7 @@ pub enum MinkowskiFace{
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
@@ -605,6 +635,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>{
@@ -618,12 +652,12 @@ impl MinkowskiMesh<'_>{
}
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{
let mut best_transition=Transition::Done;
for &directed_edge_id in self.vert_edges(vert_id).iter(){
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
let edge_verts=self.edge_verts(directed_edge_id.as_undirected());
//select opposite vertex
let test_vert_id=edge_verts[directed_edge_id.parity() as usize];
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
//test if it's closer
let diff=point-self.vert(test_vert_id);
if edge_n.dot(infinity_dir).is_zero(){
@@ -639,7 +673,7 @@ impl MinkowskiMesh<'_>{
fn final_ev(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
let mut best_transition=EV::Vert(vert_id);
let diff=point-self.vert(vert_id);
for &directed_edge_id in self.vert_edges(vert_id).iter(){
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
//check if time of collision is outside Time::MIN..Time::MAX
@@ -650,7 +684,8 @@ impl MinkowskiMesh<'_>{
if !d.is_negative()&&d<=edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
(c.dot(c)/edge_nn).divide().fix_2()
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<=*best_distance_squared{
best_transition=EV::Edge(directed_edge_id.as_undirected());
@@ -674,7 +709,7 @@ impl MinkowskiMesh<'_>{
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh<'_>>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
@@ -686,10 +721,10 @@ impl MinkowskiMesh<'_>{
let edge_n=self.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
let &[v0,v1]=self.edge_verts(edge_id).borrow();
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v0)+self.vert(v1)
};
for (i,&face_id) in self.edge_faces(edge_id).iter().enumerate(){
for (i,&face_id) in self.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=self.face_nd(face_id).0;
//edge-face boundary nd, n facing out of the face towards the edge
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
@@ -719,70 +754,69 @@ 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;
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)>{
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{
//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
let infinity_body=-relative_body.clone();
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
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;
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();
Ratio::new(r.num,r.den)
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
let start_time=range.start_bound().map(|&t|(t-relative_body.time).to_ratio());
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).iter(){
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//f x e points in
let n=face_n.cross(edge_n);
let verts=self.edge_verts(directed_edge_id.as_undirected());
let d=n.dot(self.vert(verts[0])+self.vert(verts[1]));
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
let d=n.dot(self.vert(v0)+self.vert(v1));
//WARNING! d outside of *2
//WARNING: truncated precision
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_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);
//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 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);
let infinity_body=Body::new(point,vec3::Y,vec3::zero(),Time::ZERO);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
@@ -808,12 +842,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let edge0_n=self.mesh0.edge_n(e0);
let edge1_n=self.mesh1.edge_n(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).borrow();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).borrow();
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
let n=edge0_n.cross(edge1_n);
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
((n*(parity as i64*4-2)).fix_3(),((e0d-e1d)*(parity as i64*2-1)).fix_4())
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
},
MinkowskiFace::FaceVert(f0,v1)=>{
let (n,d)=self.mesh0.face_nd(f0);
@@ -828,44 +862,48 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
fn hint_point(&self)->Planar64Vec3{
self.mesh1.transform.vertex.translation-
self.mesh0.transform.vertex.translation
}
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
self.mesh1.face_edges(f1).as_ref().iter().map(|&edge_id1|
MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse())
}).collect())
).collect()
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let e0v=self.mesh0.edge_verts(e0);
let e1v=self.mesh1.edge_verts(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
//could sort this if ordered edges are needed
//probably just need to reverse this list according to parity
Cow::Owned(vec![
MinkowskiDirectedEdge::VertEdge(e0v[0],e1.as_directed(parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v[0]),
MinkowskiDirectedEdge::VertEdge(e0v[1],e1.as_directed(!parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v[1]),
])
vec![
MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0),
MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1),
]
},
MinkowskiFace::FaceVert(f0,v1)=>{
Cow::Owned(self.mesh0.face_edges(f0).iter().map(|&edge_id0|{
self.mesh0.face_edges(f0).as_ref().iter().map(|&edge_id0|
MinkowskiDirectedEdge::EdgeVert(edge_id0,v1)
}).collect())
).collect()
},
}
}
fn edge_faces(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiFace;2]>{
fn edge_faces(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiFace;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
//faces are listed backwards from the minkowski mesh
let v0e=self.mesh0.vert_edges(v0);
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).borrow();
Cow::Owned([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
for &directed_edge_id0 in v0e.iter(){
for &directed_edge_id0 in v0e.as_ref(){
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
//must be behind other face.
let d=edge_face1_n.dot(edge0_n);
@@ -889,13 +927,13 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiEdge::EdgeVert(e0,v1)=>{
//tracking index with an external variable because .enumerate() is not available
let v1e=self.mesh1.vert_edges(v1);
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).borrow();
Cow::Owned([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in v1e.iter(){
for &directed_edge_id1 in v1e.as_ref(){
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
let d=edge_face0_n.dot(edge1_n);
if d.is_negative(){
@@ -915,68 +953,73 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiVert;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
Cow::Owned(self.mesh1.edge_verts(e1).map(|vert_id1|{
MinkowskiVert::VertVert(v0,vert_id1)
}))
},
MinkowskiEdge::EdgeVert(e0,v1)=>{
Cow::Owned(self.mesh0.edge_verts(e0).map(|vert_id0|{
MinkowskiVert::VertVert(vert_id0,v1)
}))
},
}
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|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
}
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
fn vert_edges(&self,vert_id:MinkowskiVert)->impl AsRef<[MinkowskiDirectedEdge]>{
match vert_id{
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_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();
let the_len=v0f.len()+v1f.len();
for &directed_edge_id in self.mesh0.vert_edges(v0).iter(){
// 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
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
//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));
}
}
for &directed_edge_id in self.mesh1.vert_edges(v1).iter(){
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{
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
//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));
}
}
Cow::Owned(edges)
edges
},
}
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
unimplemented!()
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
unimplemented!();
#[expect(unreachable_code)]
Vec::new()
}
}
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{
@@ -1002,6 +1045,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
#[test]
fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec()));
assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_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()]));
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
use strafesnet_common::integer::vec3::{self,Vector3};
use strafesnet_common::integer::{Fixed,Planar64Vec3,Ratio};
use strafesnet_common::ray::Ray;
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
@@ -12,24 +14,6 @@ type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
// hack to allow comparing ratios to zero
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
struct Ray{
origin:Planar64Vec3,
direction:Planar64Vec3,
}
impl Ray{
fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
}
}
/// Information about a contact restriction
pub struct Contact{
pub position:Planar64Vec3,
@@ -55,20 +39,18 @@ impl Contact{
//note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
let det=c0.normal.dot(c0.velocity);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det)
}
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
let u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
@@ -76,10 +58,9 @@ fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,1
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
}
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
@@ -165,21 +146,22 @@ fn is_space_enclosed_4(
}
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::ZERO}
Ray{origin:point,direction:vec3::zero()}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
let direction=solve1(c0)?.divide().fix_1();
//wrap for speed
let direction=solve1(c0)?.divide().wrap_1();
let [s0]=decompose1(direction,c0.velocity)?;
if s0.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve1(
&c0.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let direction=solve2(c0,c1)?.divide().fix_1();
let direction=solve2(c0,c1)?.divide().wrap_1();
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
return None;
@@ -187,11 +169,11 @@ fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let origin=point+solve2(
&c0.relative_to(point),
&c1.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
let direction=solve3(c0,c1,c2)?.divide().fix_1();
let direction=solve3(c0,c1,c2)?.divide().wrap_1();
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
return None;
@@ -200,14 +182,14 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
&c0.relative_to(point),
&c1.relative_to(point),
&c2.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const())
}
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts<'_>)>{
get_push_ray_1(point,c0)
.map(|ray|(ray,Conts::from_iter([c0])))
}
@@ -336,13 +318,13 @@ mod tests{
fn test_push_solve(){
let contacts=vec![
Contact{
position:vec3::ZERO,
position:vec3::zero(),
velocity:vec3::Y,
normal:vec3::Y,
}
];
assert_eq!(
vec3::ZERO,
vec3::zero(),
push_solve(&contacts,vec3::NEG_Y)
);
}

View File

@@ -10,3 +10,6 @@ strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -31,7 +31,7 @@ pub enum SessionInputInstruction{
Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction),
Misc(strafesnet_common::physics::MiscInstruction),
Misc(MiscInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
@@ -152,10 +152,10 @@ enum ViewState{
pub struct Session{
directories:Directories,
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
mouse_interpolator:MouseInterpolator,
view_state:ViewState,
//gui:GuiState
geometry_shared:physics::PhysicsData,
geometry_shared:PhysicsData,
simulation:Simulation,
// below fields not included in lite session
recording:Recording,
@@ -172,7 +172,7 @@ impl Session{
user_settings,
directories,
mouse_interpolator:MouseInterpolator::new(),
geometry_shared:Default::default(),
geometry_shared:PhysicsData::empty(),
simulation,
view_state:ViewState::Play,
recording:Default::default(),
@@ -184,7 +184,7 @@ impl Session{
}
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.clear();
self.geometry_shared.generate_models(map);
self.geometry_shared=PhysicsData::new(map);
}
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
match &self.view_state{

View File

@@ -8,3 +8,6 @@ configparser = "3.0.2"
directories = "6.0.0"
glam = "0.30.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
[lints]
workspace = true

1
integration-testing/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/test_files

View File

@@ -4,6 +4,12 @@ version = "0.1.0"
edition = "2024"
[dependencies]
glam = "0.30.0"
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }
# this is just for the primitive constructor
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -0,0 +1,28 @@
#[expect(dead_code)]
#[derive(Debug)]
pub enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}

View File

@@ -1,47 +1,25 @@
mod error;
mod util;
use std::{io::{Cursor,Read},path::Path};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_scenes;
use std::time::Instant;
use error::ReplayError;
use util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
test_determinism().unwrap();
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
let arg=std::env::args().skip(1).next();
match arg.as_deref(){
Some("determinism")|None=>test_determinism().unwrap(),
Some("replay")=>run_replay().unwrap(),
_=>println!("invalid argument"),
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}
fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{
println!("loading map file..");
@@ -53,9 +31,8 @@ fn run_replay()->Result<(),ReplayError>{
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
@@ -72,7 +49,12 @@ enum DeterminismResult{
Deterministic,
NonDeterministic,
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
struct SegmentResult{
determinism:DeterminismResult,
ticks:u64,
nanoseconds:u64,
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->SegmentResult{
// create default physics state
let mut physics_deterministic=PhysicsState::default();
// create a second physics state
@@ -82,6 +64,9 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
println!("simulating...");
let mut non_idle_count=0;
let instruction_count=bot.instructions.len();
let start=Instant::now();
for (i,ins) in bot.instructions.into_iter().enumerate(){
let state_deterministic=physics_deterministic.clone();
@@ -97,6 +82,7 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
let b0=physics_deterministic.camera_body();
let b1=physics_filtered.camera_body();
if b0.position!=b1.position{
let nanoseconds=start.elapsed().as_nanos() as u64;
println!("desync at instruction #{}",i);
println!("non idle instructions completed={non_idle_count}");
println!("instruction #{i}={:?}",other);
@@ -104,11 +90,16 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
println!("filtered state0:\n{state_filtered:?}");
println!("deterministic state1:\n{:?}",physics_deterministic);
println!("filtered state1:\n{:?}",physics_filtered);
return DeterminismResult::NonDeterministic;
return SegmentResult{
determinism:DeterminismResult::NonDeterministic,
ticks:1+i as u64+non_idle_count,
nanoseconds,
};
}
},
}
}
let nanoseconds=start.elapsed().as_nanos() as u64;
match physics_deterministic.get_finish_time(){
Some(time)=>println!("[with idle] finish time:{}",time),
None=>println!("[with idle] simulation did not end in finished state"),
@@ -117,9 +108,14 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
Some(time)=>println!("[filtered] finish time:{}",time),
None=>println!("[filtered] simulation did not end in finished state"),
}
DeterminismResult::Deterministic
SegmentResult{
determinism:DeterminismResult::Deterministic,
ticks:instruction_count as u64+non_idle_count,
nanoseconds,
}
}
type ThreadResult=Result<Option<DeterminismResult>,ReplayError>;
type ThreadResult=Result<Option<SegmentResult>,ReplayError>;
fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{
let data=read_entire_file(file_path.as_path())?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
@@ -144,9 +140,8 @@ fn test_determinism()->Result<(),ReplayError>{
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
let (send,recv)=std::sync::mpsc::channel();
@@ -198,10 +193,18 @@ fn test_determinism()->Result<(),ReplayError>{
invalid:u32,
error:u32,
}
let mut nanoseconds=0;
let mut ticks=0;
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
match result{
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
Ok(Some(segment_result))=>{
ticks+=segment_result.ticks;
nanoseconds+=segment_result.nanoseconds;
match segment_result.determinism{
DeterminismResult::Deterministic=>totals.deterministic+=1,
DeterminismResult::NonDeterministic=>totals.nondeterministic+=1,
}
},
Ok(None)=>totals.invalid+=1,
Err(_)=>totals.error+=1,
}
@@ -212,6 +215,7 @@ fn test_determinism()->Result<(),ReplayError>{
println!("nondeterministic={nondeterministic}");
println!("invalid={invalid}");
println!("error={error}");
println!("average ticks/s per core: {}",ticks*1_000_000_000/nanoseconds);
assert!(nondeterministic==0);
assert!(invalid==0);

View File

@@ -0,0 +1,117 @@
use strafesnet_physics::physics::{InternalInstruction,PhysicsData,PhysicsState,PhysicsContext};
use strafesnet_common::gameplay_modes::NormalizedModes;
use strafesnet_common::gameplay_attributes::{CollisionAttributes,CollisionAttributesId};
use strafesnet_common::integer::{vec3,mat3,Planar64Affine3,Time};
use strafesnet_common::model::{Mesh,Model,MeshId,ModelId,RenderConfigId};
use strafesnet_common::map::CompleteMap;
use strafesnet_rbx_loader::primitives::{unit_cube,CubeFaceDescription};
struct TestSceneBuilder{
meshes:Vec<Mesh>,
models:Vec<Model>,
}
impl TestSceneBuilder{
fn new()->Self{
Self{
meshes:Vec::new(),
models:Vec::new(),
}
}
fn push_mesh(&mut self,mesh:Mesh)->MeshId{
let mesh_id=self.meshes.len();
self.meshes.push(mesh);
MeshId::new(mesh_id as u32)
}
fn push_mesh_instance(&mut self,mesh:MeshId,transform:Planar64Affine3)->ModelId{
let model=Model{
mesh,
attributes:CollisionAttributesId::new(0),
color:glam::Vec4::ONE,
transform,
};
let model_id=self.models.len();
self.models.push(model);
ModelId::new(model_id as u32)
}
fn build(self)->PhysicsData{
let modes=NormalizedModes::new(Vec::new());
let attributes=vec![CollisionAttributes::contact_default()];
let meshes=self.meshes;
let models=self.models;
let textures=Vec::new();
let render_configs=Vec::new();
PhysicsData::new(&CompleteMap{
modes,
attributes,
meshes,
models,
textures,
render_configs,
})
}
}
fn test_scene()->PhysicsData{
let mut builder=TestSceneBuilder::new();
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
let mesh=builder.push_mesh(unit_cube(cube_face_description));
// place two 5x5x5 cubes.
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(0,0,0)
));
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(5,-5,0)
));
builder.build()
}
#[test]
fn simultaneous_collision(){
let physics_data=test_scene();
let body=strafesnet_physics::physics::Body::new(
(vec3::int(5+2,0,0)>>1)+vec3::int(1,1,0),
vec3::int(-1,-1,0),
vec3::int(0,0,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(2))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// the order that they hit does matter, but we aren't currently worrying about that.
// See multi-collision branch
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
assert!(phys_iter.next().is_none());
let body=physics.body();
assert_eq!(body.position,vec3::int(5,0,0));
assert_eq!(body.velocity,vec3::int(0,0,0));
assert_eq!(body.acceleration,vec3::int(0,0,0));
assert_eq!(body.time,Time::from_secs(1));
}
#[test]
fn bug_3(){
let physics_data=test_scene();
let body=strafesnet_physics::physics::Body::new(
(vec3::int(5+2,0,0)>>1)+vec3::int(1,2,0),
vec3::int(-1,-1,0),
vec3::int(0,0,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(3))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// touch side of part at 0,0,0
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
// touch top of part at 5,-5,0
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(2));
assert!(phys_iter.next().is_none());
let body=physics.body();
assert_eq!(body.position,vec3::int(5+2,0,0)>>1);
assert_eq!(body.velocity,vec3::int(0,0,0));
assert_eq!(body.acceleration,vec3::int(0,0,0));
assert_eq!(body.time,Time::from_secs(2));
}

View File

@@ -0,0 +1,76 @@
use crate::error::ReplayError;
use crate::util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
#[test]
#[ignore]
fn physics_bug_2()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("test_files/bhop_monster_jam.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
println!("generating models..");
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
// body pos = Vector { array: [Fixed { bits: 554895163352 }, Fixed { bits: 1485633089990 }, Fixed { bits: 1279601007173 }] }
// after the fix it's still happening, possibly for a different reason, new position to evince:
// body pos = Vector { array: [Fixed { bits: 555690659654 }, Fixed { bits: 1490485868773 }, Fixed { bits: 1277783839382 }] }
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
vec3::raw_xyz(555690659654,1490485868773,1277783839382),
vec3::int(0,0,0),
vec3::int(0,-100,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}
#[test]
#[ignore]
fn physics_bug_3()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692152916.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
println!("generating models..");
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
// bhop_toc corner position after wall hits
// vec3::raw_xyz(-1401734815424,3315081280280,-2466057177493),
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// alternate room center position
// vec3::raw_xyz(-1129043783837,3324870327882,-2014012350212),
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// corner setup before wall hits
vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
vec3::raw_xyz(0,-429496729600,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}

View File

@@ -0,0 +1,7 @@
use std::io::Cursor;
use std::path::Path;
pub fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let data=std::fs::read(path)?;
Ok(Cursor::new(data))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_bsp_loader"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -11,8 +11,12 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
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.6.0"
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vpk = "0.2.0"
vpk = "0.3.0"
[lints]
workspace = true

343
lib/bsp_loader/src/brush.rs Normal file
View File

@@ -0,0 +1,343 @@
use strafesnet_common::integer::Planar64;
use strafesnet_common::{model,integer};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use crate::{valve_transform_normal,valve_transform_dist};
#[derive(Hash,Eq,PartialEq)]
struct Face{
normal:integer::Planar64Vec3,
dot:Planar64,
}
#[derive(Debug)]
struct Faces{
faces:Vec<Vec<integer::Planar64Vec3>>,
}
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs().is_zero(){
return None;
}
Some((
c1.normal.cross(c2.normal)*c0.dot
+c2.normal.cross(c0.normal)*c1.dot
+c0.normal.cross(c1.normal)*c2.dot
)/det)
}
#[derive(Debug)]
pub enum PlanesToFacesError{
InitFace1,
InitFace2,
InitIntersection,
FindNewIntersection,
// Narrow(strafesnet_common::integer::NarrowError),
EmptyFaces,
InfiniteLoop1,
InfiniteLoop2,
}
impl std::fmt::Display for PlanesToFacesError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl core::error::Error for PlanesToFacesError{}
fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,PlanesToFacesError>{
let mut faces=Vec::new();
// for each face, determine one edge at a time until you complete the face
'face: for face0 in &face_list{
// 1. find first edge
// 2. follow edges around face
// === finding first edge ===
// 1. pick the most perpendicular set of 3 faces
// 2. check if any faces occlude the intersection
// 3. use this test to replace left and right alternating until they are not occluded
// find the most perpendicular face to face0
let mut face1=face_list.iter().min_by_key(|&p|{
face0.normal.dot(p.normal).abs()
}).ok_or(PlanesToFacesError::InitFace1)?;
// direction of edge formed by face0 x face1
let edge_dir=face0.normal.cross(face1.normal);
// find the most perpendicular face to both face0 and face1
let mut face2=face_list.iter().max_by_key(|&p|{
// find the best *oriented* face (no .abs())
edge_dir.dot(p.normal)
}).ok_or(PlanesToFacesError::InitFace2)?;
let mut detect_loop=200u8;
let mut intersection=solve3(face0,face1,face2).ok_or(PlanesToFacesError::InitIntersection)?;
// repeatedly update face1, face2 until all faces form part of the convex solid
'find: loop{
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop1)?;
// test if any *other* faces occlude the intersection
for new_face in &face_list{
// new face occludes intersection point
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// replace one of the faces with the new face
// dont' try to replace face0 because we are exploring that face in particular
if let Some(new_intersection)=solve3(face0,new_face,face2){
// face1 does not occlude (or intersect) the new intersection
if (face1.dot.widen_2()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
face1=new_face;
intersection=new_intersection;
continue 'find;
}
}
if let Some(new_intersection)=solve3(face0,face1,new_face){
// face2 does not occlude (or intersect) the new intersection
if (face2.dot.widen_2()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
face2=new_face;
intersection=new_intersection;
continue 'find;
}
}
}
}
// we have found a set of faces for which the intersection is on the convex solid
break 'find;
}
// check if face0 must go, meaning it is a degenerate face and does not contribute anything to the convex solid
for new_face in &face_list{
if core::ptr::eq(face0,new_face){
continue;
}
if core::ptr::eq(face1,new_face){
continue;
}
if core::ptr::eq(face2,new_face){
continue;
}
// new_face occludes intersection meaning intersection is not on convex solid and face0 is degenrate
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// abort! reject face0 entirely
continue 'face;
}
}
// === follow edges around face ===
// Note that we chose face2 such that the 3 faces create a particular winding order.
// If we choose a consistent face to follow (face1, face2) it will always wind with a consistent chirality
let mut detect_loop=200u8;
// keep looping until we meet this face again
let face1=face1;
let mut face=Vec::new();
loop{
// push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().narrow_1().unwrap());
// we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){
break;
}
// the measure
let edge_dir=face0.normal.cross(face2.normal);
// the dot product to beat
let d_intersection=edge_dir.dot(intersection.num)/intersection.den;
// find the next face moving clockwise around face0
let (new_face,new_intersection,_)=face_list.iter().filter_map(|new_face|{
// ignore faces that are part of the current edge
if core::ptr::eq(face0,new_face)
|core::ptr::eq(face2,new_face){
return None;
}
let new_intersection=solve3(face0,face2,new_face)?;
// the d value must be larger
let d_new_intersection=edge_dir.dot(new_intersection.num)/new_intersection.den;
if d_new_intersection.le_ratio(d_intersection){
return None;
}
Some((new_face,new_intersection,d_new_intersection))
}).min_by_key(|&(_,_,d)|d).ok_or(PlanesToFacesError::FindNewIntersection)?;
face2=new_face;
intersection=new_intersection;
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop2)?;
}
faces.push(face);
}
if faces.is_empty(){
Err(PlanesToFacesError::EmptyFaces)
}else{
Ok(Faces{
faces,
})
}
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum BrushToMeshError{
SliceBrushSides,
MissingPlane,
InvalidFaceCount{
count:usize,
},
InvalidPlanes(PlanesToFacesError),
}
impl std::fmt::Display for BrushToMeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl core::error::Error for BrushToMeshError{}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// generate the mesh
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::zero());
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{
let pos=mb.acquire_pos_id(pos);
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}).collect()
}).collect();
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];
let physics_groups=vec![model::IndexedPhysicsGroup{
groups:vec![model::PolygonGroupId::new(0)],
}];
let graphics_groups=vec![];
mb.build(polygon_groups,graphics_groups,physics_groups)
}
pub fn brush_to_mesh(bsp:&vbsp::Bsp,brush:&vbsp::Brush)->Result<model::Mesh,BrushToMeshError>{
let brush_start_idx=brush.brush_side as usize;
let sides_range=brush_start_idx..brush_start_idx+brush.num_brush_sides as usize;
let sides=bsp.brush_sides.get(sides_range).ok_or(BrushToMeshError::SliceBrushSides)?;
let face_list=sides.iter().map(|side|{
// The so-called tumor brushes have TRIGGER bit set
// but also ignore visleaf hint and skip sides
const TUMOR:vbsp::TextureFlags=vbsp::TextureFlags::HINT.union(vbsp::TextureFlags::SKIP).union(vbsp::TextureFlags::TRIGGER);
if let Some(texture_info)=bsp.texture_info(side.texture_info as usize){
if texture_info.flags.intersects(TUMOR){
return None;
}
}
let plane=bsp.plane(side.plane as usize)?;
Some(Face{
normal:valve_transform_normal(plane.normal.into()),
dot:valve_transform_dist(plane.dist.into()),
})
}).collect::<Option<std::collections::HashSet<_>>>().ok_or(BrushToMeshError::MissingPlane)?;
if face_list.len()<4{
return Err(BrushToMeshError::InvalidFaceCount{count:face_list.len()});
}
let faces=planes_to_faces(face_list).map_err(BrushToMeshError::InvalidPlanes)?;
let mesh=faces_to_mesh(faces.faces);
Ok(mesh)
}
pub fn unit_cube()->model::Mesh{
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
let mesh=faces_to_mesh(faces.faces);
mesh
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn test_cube(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),6);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),6);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face2(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:-Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),5);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face3(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),7);
dbg!(faces);
}
}

View File

@@ -1,9 +1,12 @@
use std::borrow::Cow;
use strafesnet_common::{map,model,integer,gameplay_attributes};
use vbsp_entities_css::Entity;
use strafesnet_common::{map,model,integer,gameplay_attributes as attr};
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
use strafesnet_common::gameplay_modes::{NormalizedMode,NormalizedModes,Mode,Stage};
use crate::valve_transform;
@@ -32,6 +35,47 @@ fn ingest_vertex(
})
}
fn add_brush<'a>(
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
world_models:&mut Vec<model::Model>,
prop_models:&mut Vec<model::Model>,
model:&'a str,
origin:vbsp::Vector,
rendercolor:vbsp::Color,
attributes:attr::CollisionAttributesId,
){
let transform=integer::Planar64Affine3::from_translation(
valve_transform(origin.into())
);
let color=(glam::Vec3::from_array([
rendercolor.r as f32,
rendercolor.g as f32,
rendercolor.b as f32
])/255.0).extend(1.0);
match model.chars().next(){
// The first character of brush.model is '*'
Some('*')=>match model[1..].parse(){
Ok(mesh_id)=>{
let mesh=model::MeshId::new(mesh_id);
world_models.push(
model::Model{mesh,attributes,transform,color}
);
},
Err(e)=>{
println!("Brush model int parse error: {e} model={model}");
return;
},
},
_=>{
let mesh=mesh_deferred_loader.acquire_mesh_id(model);
prop_models.push(
model::Model{mesh,attributes,transform,color}
);
}
}
}
pub fn convert<'a>(
bsp:&'a crate::Bsp,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>,
@@ -39,25 +83,60 @@ pub fn convert<'a>(
)->PartialMap1{
let bsp=bsp.as_ref();
//figure out real attributes later
let mut unique_attributes=Vec::new();
unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
const TEMP_TOUCH_ME_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
let unique_attributes=vec![
attr::CollisionAttributes::Decoration,
attr::CollisionAttributes::contact_default(),
attr::CollisionAttributes::intersect_default(),
// ladder
attr::CollisionAttributes::Contact(
attr::ContactAttributes{
contacting:attr::ContactingAttributes{
contact_behaviour:Some(attr::ContactingBehaviour::Ladder(
attr::ContactingLadder{sticky:true}
))
},
general:attr::GeneralAttributes::default(),
}
),
// water
attr::CollisionAttributes::Intersect(
attr::IntersectAttributes{
intersecting:attr::IntersectingAttributes{
water:Some(attr::IntersectingWater{
viscosity:integer::Planar64::ONE,
density:integer::Planar64::ONE,
velocity:integer::vec3::zero(),
}),
},
general:attr::GeneralAttributes::default(),
}
),
];
const ATTRIBUTE_DECORATION:attr::CollisionAttributesId=attr::CollisionAttributesId::new(0);
const ATTRIBUTE_CONTACT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(1);
const ATTRIBUTE_INTERSECT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(2);
const ATTRIBUTE_LADDER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(3);
const ATTRIBUTE_WATER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(4);
//declare all prop models to Loader
let prop_models=bsp.static_props().map(|prop|{
let mut prop_models=bsp.static_props().map(|prop|{
const DEG_TO_RAD:f32=std::f32::consts::TAU/360.0;
//get or create mesh_id
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
let placement=prop.as_prop_placement();
model::Model{
mesh:mesh_id,
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
attributes:ATTRIBUTE_DECORATION,
transform:integer::Planar64Affine3::new(
integer::mat3::try_from_f32_array_2d((
glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale))
integer::mat3::try_from_f32_array_2d(
//TODO: figure this out
*glam::Mat3A::from_quat(glam::Quat::from_array(placement.rotation.into()))
).to_cols_array_2d()).unwrap(),
valve_transform(placement.origin.into()),
glam::Mat3A::from_euler(
glam::EulerRot::XYZ,
prop.angles.pitch*DEG_TO_RAD,
prop.angles.yaw*DEG_TO_RAD,
prop.angles.roll*DEG_TO_RAD
).to_cols_array_2d()
).unwrap(),
valve_transform(prop.origin.into()),
),
color:glam::Vec4::ONE,
}
@@ -67,7 +146,7 @@ pub fn convert<'a>(
//the generated MeshIds in here will collide with the Loader Mesh Ids
//but I can't think of a good workaround other than just remapping one later.
let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
let mut world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
@@ -118,64 +197,159 @@ pub fn convert<'a>(
mb.build(polygon_groups,graphics_groups,vec![])
}).collect();
let world_models:Vec<model::Model>=
//one instance of the main world mesh
std::iter::once((
//world_model
model::MeshId::new(0),
//model_origin
vbsp::Vector::from([0.0,0.0,0.0]),
//model_color
vbsp::Color{r:255,g:255,b:255},
)).chain(
//entities sprinkle instances of the other meshes around
bsp.entities.iter()
.flat_map(|ent|ent.parse())//ignore entity parsing errors
.filter_map(|ent|match ent{
vbsp::Entity::Brush(brush)=>Some(brush),
vbsp::Entity::BrushIllusionary(brush)=>Some(brush),
vbsp::Entity::BrushWall(brush)=>Some(brush),
vbsp::Entity::BrushWallToggle(brush)=>Some(brush),
_=>None,
}).flat_map(|brush|
//The first character of brush.model is '*'
brush.model[1..].parse().map(|mesh_id|//ignore parse int errors
(model::MeshId::new(mesh_id),brush.origin,brush.color)
)
)
).map(|(mesh_id,model_origin,vbsp::Color{r,g,b})|{
model::Model{
mesh:mesh_id,
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
valve_transform(model_origin.into())
),
color:(glam::Vec3::from_array([r as f32,g as f32,b as f32])/255.0).extend(1.0),
let mut found_spawn=None;
let mut world_models=Vec::new();
// the one and only world model 0
world_models.push(model::Model{
mesh:model::MeshId::new(0),
attributes:ATTRIBUTE_DECORATION,
transform:integer::Planar64Affine3::IDENTITY,
color:glam::Vec4::W,
});
const WHITE:vbsp::Color=vbsp::Color{r:255,g:255,b:255};
for raw_ent in &bsp.entities{
match raw_ent.parse(){
Ok(Entity::Cycler(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::EnvSprite(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncBreakable(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncBrush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncDoor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncIllusionary(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncMonitor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncMovelinear(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncPhysbox(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncPhysboxMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncRotButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::FuncRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTracktrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWall(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWallToggle(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWaterAnalog(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ATTRIBUTE_DECORATION),
Ok(Entity::PropDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamic(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamicOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysics(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropRagdoll(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerGravity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerHurt(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerLook(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerMultiple(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerOnce(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerProximity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerPush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerSoundscape(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerTeleport(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerVphysicsMotion(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerWind(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::InfoPlayerCounterterrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Ok(Entity::InfoPlayerTerrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Err(e)=>{
println!("Bsp Entity parse error: {e}");
},
_=>(),
}
}).collect();
}
// physics models
for brush in &bsp.brushes{
const RELEVANT:vbsp::BrushFlags=
vbsp::BrushFlags::SOLID
.union(vbsp::BrushFlags::PLAYERCLIP)
.union(vbsp::BrushFlags::WATER)
.union(vbsp::BrushFlags::MOVEABLE)
.union(vbsp::BrushFlags::LADDER);
if !brush.flags.intersects(RELEVANT){
continue;
}
let is_ladder=brush.flags.contains(vbsp::BrushFlags::LADDER);
let is_water=brush.flags.contains(vbsp::BrushFlags::WATER);
let attributes=match (is_ladder,is_water){
(true,false)=>ATTRIBUTE_LADDER_DEFAULT,
(false,true)=>ATTRIBUTE_WATER_DEFAULT,
(false,false)=>ATTRIBUTE_CONTACT_DEFAULT,
(true,true)=>{
// water ladder? wtf
println!("brush is a water ladder o_o defaulting to ladder");
ATTRIBUTE_LADDER_DEFAULT
}
};
let mesh_result=crate::brush::brush_to_mesh(bsp,brush);
match mesh_result{
Ok(mesh)=>{
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(mesh);
world_models.push(model::Model{
mesh:mesh_id,
attributes,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
integer::vec3::zero(),
),
color:glam::Vec4::ONE,
});
},
Err(e)=>println!("Brush mesh error: {e}"),
}
}
let mut modes_list=Vec::new();
if let Some(spawn_point)=found_spawn{
// create a new mesh
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(crate::brush::unit_cube());
// create a new model
let model_id=model::ModelId::new(world_models.len() as u32);
world_models.push(model::Model{
mesh:mesh_id,
attributes:ATTRIBUTE_INTERSECT_DEFAULT,
transform:integer::Planar64Affine3::from_translation(spawn_point),
color:glam::Vec4::W,
});
let first_stage=Stage::empty(model_id);
let main_mode=Mode::new(
strafesnet_common::gameplay_style::StyleModifiers::source_bhop(),
model_id,
std::collections::HashMap::new(),
vec![first_stage],
std::collections::HashMap::new(),
);
modes_list.push(NormalizedMode::new(main_mode));
}
PartialMap1{
attributes:unique_attributes,
world_meshes,
prop_models,
world_models,
modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
modes:NormalizedModes::new(modes_list),
}
}
//partially constructed map types
pub struct PartialMap1{
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
attributes:Vec<attr::CollisionAttributes>,
prop_models:Vec<model::Model>,
world_meshes:Vec<model::Mesh>,
world_models:Vec<model::Model>,
modes:strafesnet_common::gameplay_modes::Modes,
modes:NormalizedModes,
}
impl PartialMap1{
pub fn add_prop_meshes<'a>(
pub fn add_prop_meshes(
self,
prop_meshes:Meshes,
prop_meshes:Meshes<model::Mesh>,
)->PartialMap2{
PartialMap2{
attributes:self.attributes,
@@ -188,12 +362,12 @@ impl PartialMap1{
}
}
pub struct PartialMap2{
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
attributes:Vec<attr::CollisionAttributes>,
prop_meshes:Vec<(model::MeshId,model::Mesh)>,
prop_models:Vec<model::Model>,
world_meshes:Vec<model::Mesh>,
world_models:Vec<model::Model>,
modes:strafesnet_common::gameplay_modes::Modes,
modes:NormalizedModes,
}
impl PartialMap2{
pub fn add_render_configs_and_textures(

View File

@@ -2,9 +2,16 @@ use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLo
mod bsp;
mod mesh;
mod brush;
pub mod loader;
const VALVE_SCALE:f32=1.0/16.0;
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
(d*VALVE_SCALE).try_into().unwrap()
}
pub(crate) fn valve_transform_normal([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x,z,-y]).unwrap()
}
pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
}

View File

@@ -5,7 +5,6 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk};
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -22,17 +21,17 @@ impl From<std::io::Error> for TextureError{
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub struct TextureLoader;
impl TextureLoader{
pub fn new()->Self{
Self(std::marker::PhantomData)
Self
}
}
impl<'a> Loader for TextureLoader<'a>{
impl Loader for TextureLoader{
type Error=TextureError;
type Index=Cow<'a,str>;
type Index<'a>=Cow<'a,str>;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
@@ -41,13 +40,12 @@ impl<'a> Loader for TextureLoader<'a>{
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
VMDL(vmdl::ModelError),
VBSP(vbsp::BspError),
MissingMdl,
MissingMdl(String),
MissingVtx,
MissingVvd,
}
@@ -100,30 +98,24 @@ impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
}
}
pub struct ModelLoader<'bsp,'vpk,'a>{
pub struct ModelLoader<'bsp,'vpk>{
finder:BspFinder<'bsp,'vpk>,
life:core::marker::PhantomData<&'a ()>,
}
impl ModelLoader<'_,'_,'_>{
impl ModelLoader<'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'a>(
pub const fn new<'bsp,'vpk>(
finder:BspFinder<'bsp,'vpk>,
)->ModelLoader<'bsp,'vpk,'a>{
)->ModelLoader<'bsp,'vpk>{
ModelLoader{
finder,
life:core::marker::PhantomData,
}
}
}
impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
where
'bsp:'a,
'vpk:'a,
{
impl Loader for ModelLoader<'_,'_>{
type Error=MeshError;
type Index=&'a str;
type Index<'a>=&'a str where Self:'a;
type Resource=vmdl::Model;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let mdl_path_lower=index.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mdl_path_lower.as_str());
@@ -132,7 +124,7 @@ impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
// TODO: search more packs, possibly using an index of multiple packs
let mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?;
let mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl(mdl_path_lower))?;
let vtx=self.finder.find(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
let vvd=self.finder.find(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
Ok(vmdl::Model::from_parts(
@@ -143,31 +135,27 @@ impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
}
}
pub struct MeshLoader<'bsp,'vpk,'load,'a>{
pub struct MeshLoader<'bsp,'vpk,'load,'str>{
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
}
impl MeshLoader<'_,'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'load,'a>(
pub const fn new<'bsp,'vpk,'load,'str>(
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
)->MeshLoader<'bsp,'vpk,'load,'a>{
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
)->MeshLoader<'bsp,'vpk,'load,'str>{
MeshLoader{
finder,
deferred_loader
}
}
}
impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a>
where
'bsp:'a,
'vpk:'a,
{
impl Loader for MeshLoader<'_,'_,'_,'_>{
type Error=MeshError;
type Index=&'a str;
type Index<'a>=&'a str where Self:'a;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh)

View File

@@ -61,7 +61,7 @@ pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredL
_=>None,
}
})
}).flat_map(|[v1,v2,v3]|{
}).filter_map(|[v1,v2,v3]|{
// this should probably be a fatal error :D
let v1=model_vertices.get(v1)?;
let v2=model_vertices.get(v2)?;

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_common"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -12,8 +12,11 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
arrayvec = "0.7.4"
bitflags = "2.6.0"
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.0", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -2,53 +2,70 @@ use crate::integer::{vec3,Planar64Vec3};
#[derive(Clone)]
pub struct Aabb{
// min is inclusive
min:Planar64Vec3,
// max is not inclusive
max:Planar64Vec3,
}
impl Default for Aabb{
#[inline]
fn default()->Self{
Self{min:vec3::MAX,max:vec3::MIN}
}
}
impl Aabb{
#[inline]
pub const fn new(min:Planar64Vec3,max:Planar64Vec3)->Self{
Self{min,max}
}
#[inline]
pub const fn max(&self)->Planar64Vec3{
self.max
}
#[inline]
pub const fn min(&self)->Planar64Vec3{
self.min
}
#[inline]
pub fn grow(&mut self,point:Planar64Vec3){
self.min=self.min.min(point);
self.max=self.max.max(point);
}
#[inline]
pub fn join(&mut self,aabb:&Aabb){
self.min=self.min.min(aabb.min);
self.max=self.max.max(aabb.max);
}
#[inline]
pub fn inflate(&mut self,hs:Planar64Vec3){
self.min-=hs;
self.max+=hs;
}
#[inline]
pub fn contains(&self,point:Planar64Vec3)->bool{
let bvec=self.min.le(point)&point.lt(self.max);
bvec.all()
}
#[inline]
pub fn intersects(&self,aabb:&Aabb)->bool{
let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max);
bvec.all()
}
#[inline]
pub fn size(&self)->Planar64Vec3{
self.max-self.min
}
#[inline]
pub fn center(&self)->Planar64Vec3{
self.min+(self.max-self.min)>>1
self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
}
#[inline]
pub fn area_weight(&self)->fixed_wide::fixed::Fixed<2,64>{
let d=self.max-self.min;
d.x*d.y+d.y*d.z+d.z*d.x
}
//probably use floats for area & volume because we don't care about precision
// pub fn area_weight(&self)->f32{
// let d=self.max-self.min;
// d.x*d.y+d.y*d.z+d.z*d.x
// }
// pub fn volume(&self)->f32{
// let d=self.max-self.min;
// d.x*d.y*d.z

View File

@@ -1,4 +1,10 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;
use crate::aabb::Aabb;
use crate::ray::Ray;
use crate::integer::{Ratio,Planar64};
use crate::instruction::{InstructionCollector,TimedInstruction};
//da algaritum
//lista boxens
@@ -10,6 +16,96 @@ use crate::aabb::Aabb;
//sort the centerpoints on each axis (3 lists)
//bv is put into octant based on whether it is upper or lower in each list
pub fn intersect_aabb(ray:&Ray,aabb:&Aabb)->Option<Ratio<Planar64,Planar64>>{
// n.(o+d*t)==n.p
// n.o + n.d * t == n.p
// t == (n.p - n.o)/n.d
let mut hit=None;
match ray.direction.x.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_max.x*ray.direction.y;
let dz=rel_max.x*ray.direction.z;
// x is negative, so inequalities are flipped
if rel_min.y*ray.direction.x>dy&&dy>rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x>dz&&dz>rel_max.z*ray.direction.x{
let t=rel_max.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_min.x*ray.direction.y;
let dz=rel_min.x*ray.direction.z;
// x is positive, so inequalities are normal
if rel_min.y*ray.direction.x<dy&&dy<rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x<dz&&dz<rel_max.z*ray.direction.x{
let t=rel_min.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.z.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_max.z*ray.direction.x;
let dy=rel_max.z*ray.direction.y;
// z is negative, so inequalities are flipped
if rel_min.x*ray.direction.z>dx&&dx>rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z>dy&&dy>rel_max.y*ray.direction.z{
let t=rel_max.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_min.z*ray.direction.x;
let dy=rel_min.z*ray.direction.y;
// z is positive, so inequalities are normal
if rel_min.x*ray.direction.z<dx&&dx<rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z<dy&&dy<rel_max.y*ray.direction.z{
let t=rel_min.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.y.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_max.y*ray.direction.z;
let dx=rel_max.y*ray.direction.x;
// y is negative, so inequalities are flipped
if rel_min.z*ray.direction.y>dz&&dz>rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y>dx&&dx>rel_max.x*ray.direction.y{
let t=rel_max.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_min.y*ray.direction.z;
let dx=rel_min.y*ray.direction.x;
// y is positive, so inequalities are normal
if rel_min.z*ray.direction.y<dz&&dz<rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y<dx&&dx<rel_max.x*ray.direction.y{
let t=rel_min.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
hit
}
pub enum RecursiveContent<N,L>{
Branch(Vec<N>),
Leaf(L),
@@ -44,6 +140,92 @@ impl<L> BvhNode<L>{
},
}
}
fn populate_nodes<'a,T,F>(
&'a self,
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>,
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
ray:&Ray,
start_time:Ratio<Planar64,Planar64>,
f:&F,
)
where
T:Ord+Copy,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
match &self.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
RecursiveContent::Branch(children)=>for child in children{
if child.aabb.contains(ray.origin){
child.populate_nodes(collector,nodes,ray,start_time,f);
}else{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){
nodes.insert(t,child);
}
}
}
},
}
}
pub fn sample_ray<T,F>(
&self,
ray:&Ray,
start_time:T,
time_limit:T,
f:F,
)->Option<(T,&L)>
where
T:Ord+Copy,
T:From<Ratio<Planar64,Planar64>>,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
// source of nondeterminism when Aabb boundaries are coplanar
let mut nodes=BTreeMap::new();
let start_time=start_time.into();
let time_limit=time_limit.into();
let mut collector=InstructionCollector::new(time_limit);
// break open all nodes that contain ray.origin and populate nodes with future intersection times
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
// swim through nodes one at a time
while let Some((t,node))=nodes.pop_first(){
if collector.time()<t{
break;
}
match &node.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
// this lower bound can also be omitted
// but it causes type inference errors lol
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
// break open the node and predict collisions with the child nodes
RecursiveContent::Branch(children)=>for child in children{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
// we don't need to check the lower bound
// because child aabbs are guaranteed to be within the parent bounds.
if t<collector.time(){
nodes.insert(t,child);
}
}
},
}
}
collector.take().map(|TimedInstruction{time,instruction:leaf}|(time.into(),leaf))
}
pub fn into_inner(self)->(RecursiveContent<BvhNode<L>,L>,Aabb){
(self.content,self.aabb)
}
@@ -63,18 +245,19 @@ pub fn generate_bvh<T>(boxen:Vec<(T,Aabb)>)->BvhNode<T>{
fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
let n=boxen.len();
if force||n<20{
let mut aabb=Aabb::default();
let nodes=boxen.into_iter().map(|b|{
aabb.join(&b.1);
const MAX_TERMINAL_BRANCH_LEAF_NODES:usize=20;
if force||n<MAX_TERMINAL_BRANCH_LEAF_NODES{
let mut aabb_outer=Aabb::default();
let nodes=boxen.into_iter().map(|(data,aabb)|{
aabb_outer.join(&aabb);
BvhNode{
content:RecursiveContent::Leaf(b.0),
aabb:b.1,
content:RecursiveContent::Leaf(data),
aabb,
}
}).collect();
BvhNode{
content:RecursiveContent::Branch(nodes),
aabb,
aabb:aabb_outer,
}
}else{
let mut sort_x=Vec::with_capacity(n);
@@ -90,9 +273,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
sort_y.sort_by_key(|&(_,c)|c);
sort_z.sort_by_key(|&(_,c)|c);
let h=n/2;
let median_x=sort_x[h].1;
let median_y=sort_y[h].1;
let median_z=sort_z[h].1;
let (_,median_x)=sort_x[h];
let (_,median_y)=sort_y[h];
let (_,median_z)=sort_z[h];
//locate a run of values equal to the median
//partition point gives the first index for which the predicate evaluates to false
let first_index_eq_median_x=sort_x.partition_point(|&(_,x)|x<median_x);
@@ -131,10 +314,10 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
};
list_list[list_id].push((data,aabb));
}
let mut aabb=Aabb::default();
if list_list.len()==1{
generate_bvh_node(list_list.remove(0),true)
}else{
let mut aabb=Aabb::default();
BvhNode{
content:RecursiveContent::Branch(
list_list.into_iter().map(|b|{

View File

@@ -171,4 +171,7 @@ impl CollisionAttributes{
pub fn contact_default()->Self{
Self::Contact(ContactAttributes::default())
}
pub fn intersect_default()->Self{
Self::Intersect(IntersectAttributes::default())
}
}

View File

@@ -1,7 +1,6 @@
use std::collections::{HashSet,HashMap};
use crate::model::ModelId;
use crate::gameplay_style;
use crate::updatable::Updatable;
#[derive(Clone)]
pub struct StageElement{
@@ -128,18 +127,6 @@ impl Stage{
self.unordered_checkpoints.contains(&model_id)
}
}
#[derive(Default)]
pub struct StageUpdate{
//other behaviour models of this stage can have
ordered_checkpoints:HashMap<CheckpointId,ModelId>,
unordered_checkpoints:HashSet<ModelId>,
}
impl Updatable<StageUpdate> for Stage{
fn update(&mut self,update:StageUpdate){
self.ordered_checkpoints.extend(update.ordered_checkpoints);
self.unordered_checkpoints.extend(update.unordered_checkpoints);
}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub enum Zone{
@@ -153,6 +140,15 @@ impl ModeId{
pub const MAIN:Self=Self(0);
pub const BONUS:Self=Self(1);
}
impl core::fmt::Display for ModeId{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->core::fmt::Result{
match self{
&Self::MAIN=>write!(f,"Main"),
&Self::BONUS=>write!(f,"Bonus"),
&Self(mode_id)=>write!(f,"Bonus{mode_id}"),
}
}
}
#[derive(Clone)]
pub struct Mode{
style:gameplay_style::StyleModifiers,
@@ -211,34 +207,47 @@ impl Mode{
pub fn push_stage(&mut self,stage:Stage){
self.stages.push(stage)
}
pub fn get_stage_mut(&mut self,stage:StageId)->Option<&mut Stage>{
self.stages.get_mut(stage.0 as usize)
pub fn get_stage_mut(&mut self,StageId(stage_id):StageId)->Option<&mut Stage>{
self.stages.get_mut(stage_id as usize)
}
pub fn get_spawn_model_id(&self,stage:StageId)->Option<ModelId>{
self.stages.get(stage.0 as usize).map(|s|s.spawn)
pub fn get_spawn_model_id(&self,StageId(stage_id):StageId)->Option<ModelId>{
self.stages.get(stage_id as usize).map(|s|s.spawn)
}
pub fn get_zone(&self,model_id:ModelId)->Option<&Zone>{
self.zones.get(&model_id)
}
pub fn get_stage(&self,stage_id:StageId)->Option<&Stage>{
self.stages.get(stage_id.0 as usize)
pub fn get_stage(&self,StageId(stage_id):StageId)->Option<&Stage>{
self.stages.get(stage_id as usize)
}
pub fn get_element(&self,model_id:ModelId)->Option<&StageElement>{
self.elements.get(&model_id)
}
}
#[derive(Clone)]
pub struct NormalizedMode(Mode);
impl NormalizedMode{
pub fn new(mode:Mode)->Self{
Self(mode)
}
pub fn into_inner(self)->Mode{
let Self(mode)=self;
mode
}
//TODO: put this in the SNF
pub fn denormalize_data(&mut self){
pub fn denormalize(self)->Mode{
let NormalizedMode(mut mode)=self;
//expand and index normalized data
self.zones.insert(self.start,Zone::Start);
for (stage_id,stage) in self.stages.iter().enumerate(){
self.elements.insert(stage.spawn,StageElement{
mode.zones.insert(mode.start,Zone::Start);
for (stage_id,stage) in mode.stages.iter().enumerate(){
mode.elements.insert(stage.spawn,StageElement{
stage_id:StageId(stage_id as u32),
force:false,
behaviour:StageElementBehaviour::SpawnAt,
jump_limit:None,
});
for (_,&model) in &stage.ordered_checkpoints{
self.elements.insert(model,StageElement{
mode.elements.insert(model,StageElement{
stage_id:StageId(stage_id as u32),
force:false,
behaviour:StageElementBehaviour::Checkpoint,
@@ -246,7 +255,7 @@ impl Mode{
});
}
for &model in &stage.unordered_checkpoints{
self.elements.insert(model,StageElement{
mode.elements.insert(model,StageElement{
stage_id:StageId(stage_id as u32),
force:false,
behaviour:StageElementBehaviour::Checkpoint,
@@ -254,53 +263,13 @@ impl Mode{
});
}
}
}
}
//this would be nice as a macro
#[derive(Default)]
pub struct ModeUpdate{
zones:HashMap<ModelId,Zone>,
stages:HashMap<StageId,StageUpdate>,
//mutually exlusive stage element behaviour
elements:HashMap<ModelId,StageElement>,
}
impl Updatable<ModeUpdate> for Mode{
fn update(&mut self,update:ModeUpdate){
self.zones.extend(update.zones);
for (stage,stage_update) in update.stages{
if let Some(stage)=self.stages.get_mut(stage.0 as usize){
stage.update(stage_update);
}
}
self.elements.extend(update.elements);
}
}
impl ModeUpdate{
pub fn zone(model_id:ModelId,zone:Zone)->Self{
let mut mu=Self::default();
mu.zones.insert(model_id,zone);
mu
}
pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
let mut mu=Self::default();
mu.stages.insert(stage_id,stage_update);
mu
}
pub fn element(model_id:ModelId,element:StageElement)->Self{
let mut mu=Self::default();
mu.elements.insert(model_id,element);
mu
}
pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
for (_,stage_element) in self.elements.iter_mut(){
stage_element.stage_id=f(stage_element.stage_id);
}
mode
}
}
#[derive(Default,Clone)]
pub struct Modes{
pub modes:Vec<Mode>,
modes:Vec<Mode>,
}
impl Modes{
pub const fn new(modes:Vec<Mode>)->Self{
@@ -314,19 +283,191 @@ impl Modes{
pub fn push_mode(&mut self,mode:Mode){
self.modes.push(mode)
}
pub fn get_mode(&self,mode:ModeId)->Option<&Mode>{
self.modes.get(mode.0 as usize)
pub fn get_mode(&self,ModeId(mode_id):ModeId)->Option<&Mode>{
self.modes.get(mode_id as usize)
}
}
pub struct ModesUpdate{
modes:HashMap<ModeId,ModeUpdate>,
#[derive(Clone)]
pub struct NormalizedModes{
modes:Vec<NormalizedMode>,
}
impl Updatable<ModesUpdate> for Modes{
fn update(&mut self,update:ModesUpdate){
for (mode,mode_update) in update.modes{
if let Some(mode)=self.modes.get_mut(mode.0 as usize){
mode.update(mode_update);
}
impl NormalizedModes{
pub fn new(modes:Vec<NormalizedMode>)->Self{
Self{modes}
}
pub fn len(&self)->usize{
self.modes.len()
}
pub fn denormalize(self)->Modes{
Modes{
modes:self.modes.into_iter().map(NormalizedMode::denormalize).collect(),
}
}
}
impl IntoIterator for NormalizedModes{
type Item=<Vec<NormalizedMode> as IntoIterator>::Item;
type IntoIter=<Vec<NormalizedMode> as IntoIterator>::IntoIter;
fn into_iter(self)->Self::IntoIter{
self.modes.into_iter()
}
}
#[derive(Default)]
pub struct StageUpdate{
//other behaviour models of this stage can have
ordered_checkpoints:HashMap<CheckpointId,ModelId>,
unordered_checkpoints:HashSet<ModelId>,
}
impl StageUpdate{
fn apply_to(self,stage:&mut Stage){
stage.ordered_checkpoints.extend(self.ordered_checkpoints);
stage.unordered_checkpoints.extend(self.unordered_checkpoints);
}
}
//this would be nice as a macro
#[derive(Default)]
pub struct ModeUpdate{
zones:Option<(ModelId,Zone)>,
stages:Option<(StageId,StageUpdate)>,
//mutually exlusive stage element behaviour
elements:Option<(ModelId,StageElement)>,
}
impl ModeUpdate{
fn apply_to(self,mode:&mut Mode){
mode.zones.extend(self.zones);
if let Some((StageId(stage_id),stage_update))=self.stages{
if let Some(stage)=mode.stages.get_mut(stage_id as usize){
stage_update.apply_to(stage);
}
}
mode.elements.extend(self.elements);
}
}
impl ModeUpdate{
pub fn zone(model_id:ModelId,zone:Zone)->Self{
Self{
zones:Some((model_id,zone)),
stages:None,
elements:None,
}
}
pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
Self{
zones:None,
stages:Some((stage_id,stage_update)),
elements:None,
}
}
pub fn element(model_id:ModelId,element:StageElement)->Self{
Self{
zones:None,
stages:None,
elements:Some((model_id,element)),
}
}
pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
for (_,stage_element) in self.elements.iter_mut(){
stage_element.stage_id=f(stage_element.stage_id);
}
}
}
struct ModeBuilder{
mode:Mode,
stage_id_map:HashMap<StageId,StageId>,
}
#[derive(Default)]
pub struct ModesBuilder{
modes:HashMap<ModeId,Mode>,
stages:HashMap<ModeId,HashMap<StageId,Stage>>,
mode_updates:Vec<(ModeId,ModeUpdate)>,
stage_updates:Vec<(ModeId,StageId,StageUpdate)>,
}
impl ModesBuilder{
pub fn build_normalized(mut self)->NormalizedModes{
//collect modes and stages into contiguous arrays
let mut unique_modes:Vec<(ModeId,Mode)>
=self.modes.into_iter().collect();
unique_modes.sort_by_key(|&(mode_id,_)|mode_id);
let (mut modes,mode_id_map):(Vec<ModeBuilder>,HashMap<ModeId,ModeId>)
=unique_modes.into_iter().enumerate()
.map(|(final_mode_id,(builder_mode_id,mut mode))|{
(
ModeBuilder{
stage_id_map:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{
let mut unique_stages:Vec<(StageId,Stage)>
=stages.into_iter().collect();
unique_stages.sort_by_key(|&(StageId(stage_id),_)|stage_id);
unique_stages.into_iter().enumerate()
.map(|(final_stage_id,(builder_stage_id,stage))|{
mode.push_stage(stage);
(builder_stage_id,StageId::new(final_stage_id as u32))
}).collect()
}),
mode,
},
(
builder_mode_id,
ModeId::new(final_mode_id as u32)
)
)
}).unzip();
//TODO: failure messages or errors or something
//push stage updates
for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{
if let Some(final_mode_id)=mode_id_map.get(&builder_mode_id){
if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
if let Some(&final_stage_id)=mode.stage_id_map.get(&builder_stage_id){
if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){
stage_update.apply_to(stage);
}
}
}
}
}
//push mode updates
for (builder_mode_id,mut mode_update) in self.mode_updates{
if let Some(final_mode_id)=mode_id_map.get(&builder_mode_id){
if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
//map stage id on stage elements
mode_update.map_stage_element_ids(|stage_id|
//walk down one stage id at a time until a stage is found
//TODO use better logic like BTreeMap::upper_bound instead of walking
// final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id))
// .value().copied().unwrap_or(StageId::FIRST)
(0..=stage_id.get()).rev().find_map(|builder_stage_id|
//map the stage element to that stage
mode.stage_id_map.get(&StageId::new(builder_stage_id)).copied()
).unwrap_or(StageId::FIRST)
);
mode_update.apply_to(&mut mode.mode);
}
}
}
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
}
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)->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));
}
// fn push_stage_update(&mut self,mode_id:ModeId,stage_id:StageId,stage_update:StageUpdate){
// 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(())
}
}

View File

@@ -34,7 +34,7 @@ pub struct StyleModifiers{
//unused
pub mass:Planar64,
}
impl std::default::Default for StyleModifiers{
impl Default for StyleModifiers{
fn default()->Self{
Self::roblox_bhop()
}
@@ -63,22 +63,22 @@ impl JumpImpulse{
velocity:Planar64Vec3,
jump_dir:Planar64Vec3,
gravity:&Planar64Vec3,
mass:Planar64,
_mass:Planar64,
)->Planar64Vec3{
match self{
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().fix_1()),
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_1()),
&JumpImpulse::Height(height)=>{
//height==-v.y*v.y/(2*g.y);
//use energy to determine max height
let gg=gravity.length_squared();
let g=gg.sqrt().fix_1();
let g=gg.sqrt();
let v_g=gravity.dot(velocity);
//do it backwards
let radicand=v_g*v_g+(g*height*2).fix_4();
velocity-(*gravity*(radicand.sqrt().fix_2()+v_g)/gg).divide().fix_1()
let radicand=v_g*v_g+(g*height*2).widen_4();
velocity-(*gravity*(radicand.sqrt().wrap_2()+v_g)/gg).divide().clamp_1()
},
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().fix_1(),
&JumpImpulse::Energy(energy)=>{
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_1(),
&JumpImpulse::Energy(_energy)=>{
//calculate energy
//let e=gravity.dot(velocity);
//add
@@ -91,10 +91,10 @@ impl JumpImpulse{
pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
match self{
&JumpImpulse::Time(time)=>(gravity.length().fix_1()*time/2).divide().fix_1(),
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().fix_1(),
&JumpImpulse::Time(time)=>(gravity.length().wrap_1()*time/2).divide().clamp_1(),
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_1(),
&JumpImpulse::Linear(deltav)=>deltav,
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().fix_1(),
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_1(),
}
}
}
@@ -126,10 +126,10 @@ impl JumpSettings{
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
let js=jump_speed.fix_2();
let js=jump_speed.widen_2();
if j<js{
//weak booster: just do a regular jump
boost_vel+jump_dir.with_length(js-j).divide().fix_1()
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
}else{
//activate booster normally, jump does nothing
boost_vel
@@ -142,13 +142,13 @@ impl JumpSettings{
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
let js=jump_speed.fix_2();
let js=jump_speed.widen_2();
if j<js{
//speed in direction of jump cannot be lower than amount
boost_vel+jump_dir.with_length(js-j).divide().fix_1()
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
}else{
//boost and jump add together
boost_vel+jump_dir.with_length(js).divide().fix_1()
boost_vel+jump_dir.with_length(js).divide().wrap_1()
}
}
(false,JumpCalculation::Max)=>{
@@ -159,10 +159,10 @@ impl JumpSettings{
None=>rel_velocity,
};
let boost_dot=boost_vel.dot(jump_dir);
let js=jump_speed.fix_2();
let js=jump_speed.widen_2();
if boost_dot<js{
//weak boost is extended to jump speed
boost_vel+jump_dir.with_length(js-boost_dot).divide().fix_1()
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_1()
}else{
//activate booster normally, jump does nothing
boost_vel
@@ -174,7 +174,7 @@ impl JumpSettings{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
boost_vel+jump_dir.with_length(jump_speed).divide().fix_1()
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_1()
},
}
}
@@ -267,9 +267,9 @@ pub struct StrafeSettings{
impl StrafeSettings{
pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
let d=velocity.dot(control_dir);
let mv=self.mv.fix_2();
let mv=self.mv.widen_2();
match d<mv{
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.fix_2().min(mv-d))).fix_1()),
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_2().min(mv-d))).wrap_1()),
false=>None,
}
}
@@ -290,7 +290,7 @@ pub struct PropulsionSettings{
}
impl PropulsionSettings{
pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
(control_dir*self.magnitude).fix_1()
(control_dir*self.magnitude).clamp_1()
}
}
@@ -310,16 +310,16 @@ pub struct WalkSettings{
impl WalkSettings{
pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
//TODO: fallible walk accel
let diff_len=target_diff.length().fix_1();
let diff_len=target_diff.length().wrap_1();
let friction=if diff_len<self.accelerate.topspeed{
self.static_friction
}else{
self.kinetic_friction
};
self.accelerate.accel.min((-gravity.y*friction).fix_1())
self.accelerate.accel.min((-gravity.y*friction).clamp_1())
}
pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
if control_dir==crate::integer::vec3::ZERO{
if control_dir==crate::integer::vec3::zero(){
return control_dir;
}
let nn=normal.length_squared();
@@ -329,19 +329,19 @@ impl WalkSettings{
let dd=d*d;
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
}
}else{
crate::integer::vec3::ZERO
crate::integer::vec3::zero()
}
}
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
//normal is not guaranteed to be unit length
let ny=normal.dot(up);
let h=normal.length().fix_1();
let h=normal.length().wrap_1();
//remember this is a normal vector
ny.is_positive()&&h*self.surf_dot<ny
}
@@ -355,12 +355,12 @@ pub struct LadderSettings{
pub dot:Planar64,
}
impl LadderSettings{
pub const fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
pub const fn accel(&self,_target_diff:Planar64Vec3,_gravity:Planar64Vec3)->Planar64{
//TODO: fallible ladder accel
self.accelerate.accel
}
pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
if control_dir==crate::integer::vec3::ZERO{
if control_dir==crate::integer::vec3::zero(){
return control_dir;
}
let nn=normal.length_squared();
@@ -368,13 +368,13 @@ impl LadderSettings{
let nnmm=nn*mm;
let d=normal.dot(control_dir);
let mut dd=d*d;
if (self.dot*self.dot*nnmm).fix_4()<dd{
if (self.dot*self.dot*nnmm).clamp_4()<dd{
if d.is_negative(){
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.fix_1(),Planar64::ZERO]);
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_1(),Planar64::ZERO]);
}else{
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.fix_1(),Planar64::ZERO]);
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_1(),Planar64::ZERO]);
}
dd=(normal.y*normal.y).fix_4();
dd=(normal.y*normal.y).widen_4();
}
//n=d if you are standing on top of a ladder and press E.
//two fixes:
@@ -382,13 +382,13 @@ impl LadderSettings{
//- fix the underlying issue
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
}
}else{
crate::integer::vec3::ZERO
crate::integer::vec3::zero()
}
}
}
@@ -417,7 +417,7 @@ impl Hitbox{
}
pub fn source()->Self{
Self{
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).fix_1(),
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_1().unwrap(),
mesh:HitboxMesh::Box,
}
}
@@ -529,20 +529,20 @@ impl StyleModifiers{
pub fn source_bhop()->Self{
Self{
controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
controls_mask:Controls::all(),
controls_mask_state:Controls::all(),
strafe:Some(StrafeSettings{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some(Planar64::raw(150<<28)*100),
mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
air_accel_limit:Some(Planar64::raw((150<<28)*100)),
mv:Planar64::raw(30<<28),
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
mass:int(1),
rocket:None,
walk:Some(WalkSettings{
@@ -565,25 +565,25 @@ impl StyleModifiers{
magnitude:int(12),//?
}),
hitbox:Hitbox::source(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
}
}
pub fn source_surf()->Self{
Self{
controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
controls_mask:Controls::all(),
controls_mask_state:Controls::all(),
strafe:Some(StrafeSettings{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
mv:(int(30)*VALVE_SCALE).fix_1(),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_1().unwrap()),
mv:Planar64::raw(30<<28),
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
mass:int(1),
rocket:None,
walk:Some(WalkSettings{
@@ -606,7 +606,7 @@ impl StyleModifiers{
magnitude:int(12),//?
}),
hitbox:Hitbox::source(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
}
}
}

View File

@@ -34,12 +34,41 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsu
self.process_instruction(instruction);
}
}
#[inline]
fn into_iter(self,time_limit:T)->InstructionIter<I,T,Self>
where
Self:Sized
{
InstructionIter{
time_limit,
feedback:self,
_phantom:core::marker::PhantomData,
}
}
}
impl<I,T,X> InstructionFeedback<I,T> for X
impl<I,T,F> InstructionFeedback<I,T> for F
where
T:Copy,
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
F:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
{}
pub struct InstructionIter<I,T:Copy,F:InstructionFeedback<I,T>>{
time_limit:T,
feedback:F,
_phantom:core::marker::PhantomData<I>,
}
impl<I,T,F> Iterator for InstructionIter<I,T,F>
where
I:Clone,
T:Clone+Copy,
F:InstructionFeedback<I,T>,
{
type Item=TimedInstruction<I,T>;
fn next(&mut self)->Option<Self::Item>{
let instruction=self.feedback.next_instruction(self.time_limit)?;
self.feedback.process_instruction(instruction.clone());
Some(instruction)
}
}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{

View File

@@ -1,14 +1,14 @@
pub use fixed_wide::fixed::{Fixed,Fix};
pub use ratio_ops::ratio::{Ratio,Divide};
pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide,Parity};
//integer units
/// specific example of a "default" time type
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type AbsoluteTime=Time<TimeInner>;
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub struct Time<T>(i64,core::marker::PhantomData<T>);
impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN);
@@ -60,7 +60,7 @@ impl<T> Time<T>{
impl<T> From<Planar64> for Time<T>{
#[inline]
fn from(value:Planar64)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_1().to_raw())
}
}
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
@@ -73,11 +73,11 @@ impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T>
where
Num:core::ops::Mul<Planar64,Output=N1>,
N1:Divide<Den,Output=T1>,
T1:Fix<Planar64>,
T1:Clamp<Planar64>,
{
#[inline]
fn from(value:Ratio<Num,Den>)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).divide().clamp().to_raw())
}
}
impl<T> std::fmt::Display for Time<T>{
@@ -86,7 +86,7 @@ impl<T> std::fmt::Display for Time<T>{
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
}
}
impl<T> std::default::Default for Time<T>{
impl<T> Default for Time<T>{
fn default()->Self{
Self::raw(0)
}
@@ -126,26 +126,27 @@ impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
impl<T> std::ops::Mul for Time<T>{
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
type Output=Ratio<Fixed<2,64>,Fixed<2,64>>;
#[inline]
fn mul(self,rhs:Self)->Self::Output{
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{
@@ -155,7 +156,7 @@ impl<T> core::ops::Mul<Time<T>> for Planar64{
#[cfg(test)]
mod test_time{
use super::*;
type Time=super::AbsoluteTime;
type Time=AbsoluteTime;
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
@@ -401,6 +402,10 @@ impl Angle32{
pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
pub const PI:Self=Self(-1<<31);
#[inline]
pub const fn raw(num:i32)->Self{
Self(num)
}
#[inline]
pub const fn wrap_from_i64(theta:i64)->Self{
//take lower bits
//note: this was checked on compiler explorer and compiles to 1 instruction!
@@ -515,8 +520,8 @@ fn angle_sin_cos(){
println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
let (fs,fc)=f.sin_cos();
println!("float s={} c={}",fs,fc);
assert!(close_enough((c/h).divide().fix_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().fix_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
assert!(close_enough((c/h).divide().wrap_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().wrap_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
}
test_angle(1.0);
test_angle(std::f64::consts::PI/4.0);
@@ -547,7 +552,7 @@ impl TryFrom<[f32;3]> for Unit32Vec3{
}
*/
pub type Planar64TryFromFloatError=fixed_wide::fixed::FixedFromFloatError;
pub type Planar64TryFromFloatError=FixedFromFloatError;
pub type Planar64=fixed_wide::types::I32F32;
pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>;
pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>;
@@ -556,8 +561,6 @@ pub mod vec3{
pub use linear_ops::types::Vector3;
pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
@@ -566,6 +569,10 @@ pub mod vec3{
pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]);
pub const NEG_Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::NEG_ONE]);
pub const NEG_ONE:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::NEG_ONE,Planar64::NEG_ONE]);
// TODO: use #![feature(generic_const_items)] when stabilized https://github.com/rust-lang/rust/issues/113521
pub const fn zero<const N:usize,const F:usize>()->Vector3<Fixed<N,F>>{
Vector3::new([Fixed::ZERO;3])
}
#[inline]
pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{
Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
@@ -625,8 +632,8 @@ pub mod mat3{
let (yc,ys)=y.cos_sin();
Planar64Mat3::from_cols([
Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
Planar64Vec3::new([(xs*ys).fix_1(),yc,(xc*ys).fix_1()]),
Planar64Vec3::new([(xs*yc).fix_1(),-ys,(xc*yc).fix_1()]),
Planar64Vec3::new([(xs*ys).wrap_1(),yc,(xc*ys).wrap_1()]),
Planar64Vec3::new([(xs*yc).wrap_1(),-ys,(xc*yc).wrap_1()]),
])
}
#[inline]
@@ -648,19 +655,27 @@ 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,
}
impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::zero());
#[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation}
}
#[inline]
pub const fn from_translation(translation:Planar64Vec3)->Self{
Self{
matrix3:mat3::identity(),
translation,
}
}
#[inline]
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
self.translation.fix_2()+self.matrix3*point
self.translation.widen_2()+self.matrix3*point
}
}
impl Into<glam::Mat4> for Planar64Affine3{

View File

@@ -1,5 +1,6 @@
pub mod bvh;
pub mod map;
pub mod ray;
pub mod run;
pub mod aabb;
pub mod model;
@@ -8,7 +9,6 @@ pub mod timer;
pub mod integer;
pub mod physics;
pub mod session;
pub mod updatable;
pub mod instruction;
pub mod gameplay_attributes;
pub mod gameplay_modes;

View File

@@ -4,7 +4,7 @@ use crate::gameplay_attributes;
//this is a temporary struct to try to get the code running again
//TODO: use snf::map::Region to update the data in physics and graphics instead of this
pub struct CompleteMap{
pub modes:gameplay_modes::Modes,
pub modes:gameplay_modes::NormalizedModes,
pub attributes:Vec<gameplay_attributes::CollisionAttributes>,
pub meshes:Vec<model::Mesh>,
pub models:Vec<model::Model>,

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::integer::{Planar64Vec3,Planar64Affine3};
use crate::integer::{Planar64,Planar64Vec3,Planar64Affine3};
use crate::gameplay_attributes;
pub type TextureCoordinate=glam::Vec2;
@@ -168,6 +168,11 @@ impl MeshBuilder{
}
}
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{
// Truncate the 16 most precise bits of the vertex positions.
// This allows the normal vectors to exactly represent the face.
// Remove this in Mesh V2
const MASK:Planar64=Planar64::raw(!((1<<16)-1));
let pos=pos.map(|c|c&MASK);
*self.pos_id_from.entry(pos).or_insert_with(||{
let pos_id=PositionId::new(self.unique_pos.len() as u32);
self.unique_pos.push(pos);

View File

@@ -1,7 +1,7 @@
use crate::mouse::MouseState;
use crate::gameplay_modes::{ModeId,StageId};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;

20
lib/common/src/ray.rs Normal file
View File

@@ -0,0 +1,20 @@
use ratio_ops::ratio::Ratio;
use crate::integer::{self,Planar64,Planar64Vec3};
pub struct Ray{
pub origin:Planar64Vec3,
pub direction:Planar64Vec3,
}
impl Ray{
pub fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Clamp<Planar64>,
{
self.origin+self.direction.map(|elem|(t*elem).divide().clamp())
}
}

View File

@@ -2,7 +2,7 @@ use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;

View File

@@ -1,3 +1,3 @@
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;

View File

@@ -1,57 +0,0 @@
// This whole thing should be a drive macro
pub trait Updatable<Updater>{
fn update(&mut self,update:Updater);
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
struct InnerId(u32);
#[derive(Clone)]
struct Inner{
id:InnerId,
enabled:bool,
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
struct OuterId(u32);
struct Outer{
id:OuterId,
inners:std::collections::HashMap<InnerId,Inner>,
}
enum Update<I,U>{
Insert(I),
Update(U),
Remove
}
struct InnerUpdate{
//#[updatable(Update)]
enabled:Option<bool>,
}
struct OuterUpdate{
//#[updatable(Insert,Update,Remove)]
inners:std::collections::HashMap<InnerId,Update<Inner,InnerUpdate>>,
//#[updatable(Update)]
//inners:std::collections::HashMap<InnerId,InnerUpdate>,
}
impl Updatable<InnerUpdate> for Inner{
fn update(&mut self,update:InnerUpdate){
if let Some(enabled)=update.enabled{
self.enabled=enabled;
}
}
}
impl Updatable<OuterUpdate> for Outer{
fn update(&mut self,update:OuterUpdate){
for (id,up) in update.inners{
match up{
Update::Insert(new_inner)=>self.inners.insert(id,new_inner),
Update::Update(inner_update)=>self.inners.get_mut(&id).map(|inner|{
let old=inner.clone();
inner.update(inner_update);
old
}),
Update::Remove=>self.inners.remove(&id),
};
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.5.0"
version = "0.5.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -10,4 +10,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -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{
@@ -45,7 +45,7 @@ impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
pub fn into_indices(self)->impl Iterator<Item=H>{
self.render_config_id_from_asset_id.into_keys().flatten()
}
pub fn into_render_configs<L:Loader<Resource=Texture,Index=H>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
pub fn into_render_configs<'a,L:Loader<Resource=Texture,Index<'a>=H>+'a>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
let mut sorted_textures=vec![None;self.texture_count as usize];
for (index_option,render_config_id) in self.render_config_id_from_asset_id{
let render_config=&mut self.render_configs[render_config_id.get() as usize];
@@ -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<L:Loader<Resource=Mesh,Index=H>>(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);

View File

@@ -2,7 +2,7 @@ use std::error::Error;
pub trait Loader{
type Error:Error;
type Index;
type Index<'a> where Self:'a;
type Resource;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>;
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>;
}

View File

@@ -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))
)

View File

@@ -1,6 +1,6 @@
[package]
name = "fixed_wide"
version = "0.1.2"
version = "0.2.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -14,7 +14,10 @@ wide-mul=[]
zeroes=["dep:arrayvec"]
[dependencies]
bnum = "0.12.0"
bnum = "0.13.0"
arrayvec = { version = "0.7.6", optional = true }
paste = "1.0.15"
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
[lints]
workspace = true

View File

@@ -1,6 +1,8 @@
use bnum::{BInt,cast::As};
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord)]
const BNUM_DIGIT_WIDTH:usize=64;
#[derive(Clone,Copy,Default,Hash,PartialEq,Eq,PartialOrd,Ord)]
/// A Fixed point number for which multiply operations widen the bits in the output. (when the wide-mul feature is enabled)
/// N is the number of u64s to use
/// F is the number of fractional bits (always N*32 lol)
@@ -33,6 +35,14 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
self.bits
}
#[inline]
pub const fn as_bits(&self)->&BInt<N>{
&self.bits
}
#[inline]
pub const fn as_bits_mut(&mut self)->&mut BInt<N>{
&mut self.bits
}
#[inline]
pub const fn raw_digit(value:i64)->Self{
let mut digits=[0u64;N];
digits[0]=value.abs() as u64;
@@ -56,6 +66,38 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
pub const fn abs(self)->Self{
Self::from_bits(self.bits.abs())
}
#[inline]
pub const fn midpoint(self,other:Self)->Self{
Self::from_bits(self.bits.midpoint(other.bits))
}
#[inline]
pub const fn min(self,other:Self)->Self{
Self::from_bits(self.bits.min(other.bits))
}
#[inline]
pub const fn max(self,other:Self)->Self{
Self::from_bits(self.bits.max(other.bits))
}
/// return the result of self*sign(other)
#[inline]
pub const fn mul_sign<const N1:usize,const F1:usize>(self,other:Fixed<N1,F1>)->Self{
if other.is_negative(){
Self::from_bits(self.bits.neg())
}else if other.is_zero(){
Fixed::ZERO
}else{
self
}
}
/// return the result of self/sign(other) (divide by zero does not change the sign)
#[inline]
pub const fn div_sign<const N1:usize,const F1:usize>(self,other:Fixed<N1,F1>)->Self{
if other.is_negative(){
Self::from_bits(self.bits.neg())
}else{
self
}
}
}
impl<const F:usize> Fixed<1,F>{
/// My old code called this function everywhere so let's provide it
@@ -87,28 +129,6 @@ impl_from!(
i8,i16,i32,i64,i128,isize
);
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
where
T:Copy,
BInt::<N>:From<T>,
{
#[inline]
fn eq(&self,&other:&T)->bool{
self.bits.eq(&other.into())
}
}
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
where
T:Copy,
BInt::<N>:From<T>,
{
#[inline]
fn partial_cmp(&self,&other:&T)->Option<std::cmp::Ordering>{
self.bits.partial_cmp(&other.into())
}
}
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self;
#[inline]
@@ -221,6 +241,11 @@ impl FixedFromFloatError{
}
}
}
impl core::fmt::Display for FixedFromFloatError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
macro_rules! impl_from_float {
( $decode:ident, $input: ty, $mantissa_bits:expr ) => {
impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{
@@ -269,6 +294,23 @@ macro_rules! impl_from_float {
impl_from_float!(integer_decode_f32,f32,24);
impl_from_float!(integer_decode_f64,f64,53);
impl<const N:usize,const F:usize> core::fmt::Debug for Fixed<N,F>{
#[inline]
fn fmt(&self,f:&mut core::fmt::Formatter)->Result<(),core::fmt::Error>{
let integral=self.as_bits().unsigned_abs()>>F;
let fractional=self.as_bits().unsigned_abs()&((bnum::BUint::<N>::ONE<<F)-bnum::BUint::<N>::ONE);
let leading_zeroes=(fractional.leading_zeros() as usize).saturating_sub(N*BNUM_DIGIT_WIDTH-F)>>2;
if self.is_negative(){
core::write!(f,"-")?;
}
if fractional.is_zero(){
core::write!(f,"{integral:x}.{}","0".repeat(leading_zeroes))
}else{
core::write!(f,"{integral:x}.{}{fractional:x}","0".repeat(leading_zeroes))
}
}
}
impl<const N:usize,const F:usize> core::fmt::Display for Fixed<N,F>{
#[inline]
fn fmt(&self,f:&mut core::fmt::Formatter)->Result<(),core::fmt::Error>{
@@ -292,16 +334,6 @@ macro_rules! impl_additive_operator {
self.$method(other)
}
}
impl<const N:usize,const F:usize,U> core::ops::$trait<U> for $struct<N,F>
where
BInt::<N>:From<U>,
{
type Output = $output;
#[inline]
fn $method(self, other: U) -> Self::Output {
Self::from_bits(self.bits.$method(BInt::<N>::from(other).shl(F as u32)))
}
}
};
}
macro_rules! impl_additive_assign_operator {
@@ -312,15 +344,6 @@ macro_rules! impl_additive_assign_operator {
self.bits.$method(other.bits);
}
}
impl<const N:usize,const F:usize,U> core::ops::$trait<U> for $struct<N,F>
where
BInt::<N>:From<U>,
{
#[inline]
fn $method(&mut self, other: U) {
self.bits.$method(BInt::<N>::from(other).shl(F as u32));
}
}
};
}
@@ -651,74 +674,94 @@ macro_repeated!(
1,2,3,4,5,6,7,8
);
pub trait Fix<Out>{
fn fix(self)->Out;
#[derive(Debug,Eq,PartialEq)]
pub enum NarrowError{
Overflow,
Underflow,
}
macro_rules! impl_fix_rhs_lt_lhs_not_const_generic{
pub trait Wrap<Output>{
fn wrap(self)->Output;
}
pub trait Clamp<Output>{
fn clamp(self)->Output;
}
impl<const N:usize,const F:usize> Clamp<Fixed<N,F>> for Result<Fixed<N,F>,NarrowError>{
fn clamp(self)->Fixed<N,F>{
match self{
Ok(fixed)=>fixed,
Err(NarrowError::Overflow)=>Fixed::MAX,
Err(NarrowError::Underflow)=>Fixed::MIN,
}
}
}
macro_rules! impl_narrow_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
paste::item!{
impl Fixed<$lhs,{$lhs*32}>
{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
pub fn [<wrap_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits.shr(($lhs-$rhs)*32)))
}
#[inline]
pub fn [<narrow_ $rhs>](self)->Result<Fixed<$rhs,{$rhs*32}>,NarrowError>{
if Fixed::<$rhs,{$rhs*32}>::MAX.[<widen_ $lhs>]().bits<self.bits{
return Err(NarrowError::Overflow);
}
if self.bits<Fixed::<$rhs,{$rhs*32}>::MIN.[<widen_ $lhs>]().bits{
return Err(NarrowError::Underflow);
}
Ok(self.[<wrap_ $rhs>]())
}
#[inline]
pub fn [<clamp_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
self.[<narrow_ $rhs>]().clamp()
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
impl Wrap<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
fn wrap(self)->Fixed<$rhs,{$rhs*32}>{
self.[<wrap_ $rhs>]()
}
}
impl TryInto<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
type Error=NarrowError;
#[inline]
fn try_into(self)->Result<Fixed<$rhs,{$rhs*32}>,Self::Error>{
self.[<narrow_ $rhs>]()
}
}
impl Clamp<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
fn clamp(self)->Fixed<$rhs,{$rhs*32}>{
self.[<clamp_ $rhs>]()
}
}
}
}
}
macro_rules! impl_fix_lhs_lt_rhs_not_const_generic{
macro_rules! impl_widen_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
paste::item!{
impl Fixed<$lhs,{$lhs*32}>
{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
pub fn [<widen_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits).shl(($rhs-$lhs)*32))
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
}
}
}
}
}
macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
impl Into<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
self
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
fn into(self)->Fixed<$rhs,{$rhs*32}>{
self.[<widen_ $rhs>]()
}
}
}
@@ -728,7 +771,7 @@ macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
macro_repeated!(
impl_fix_rhs_lt_lhs_not_const_generic,(),
impl_narrow_not_const_generic,(),
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
@@ -746,7 +789,7 @@ macro_repeated!(
(16,15)
);
macro_repeated!(
impl_fix_lhs_lt_rhs_not_const_generic,(),
impl_widen_not_const_generic,(),
(1,2),
(1,3),(2,3),
(1,4),(2,4),(3,4),
@@ -761,11 +804,8 @@ macro_repeated!(
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16)
);
macro_repeated!(
impl_fix_lhs_eq_rhs_not_const_generic,(),
(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10),(11,11),(12,12),(13,13),(14,14),(15,15),(16,16)
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
(1,17)
);
macro_rules! impl_not_const_generic{
@@ -785,16 +825,13 @@ macro_rules! impl_not_const_generic{
let mut result=Self::ZERO;
//resize self to match the wide mul output
let wide_self=self.[<fix_ $_2n>]();
let wide_self=self.[<widen_ $_2n>]();
//descend down the bits and check if flipping each bit would push the square over the input value
for shift in (0..=max_shift).rev(){
let new_result={
let mut bits=result.to_bits().to_bits();
bits.set_bit(shift,true);
Self::from_bits(BInt::from_bits(bits))
};
if new_result.[<wide_mul_ $n _ $n>](new_result)<=wide_self{
result=new_result;
result.as_bits_mut().as_bits_mut().set_bit(shift,true);
if wide_self<result.[<wide_mul_ $n _ $n>](result){
// put it back lol
result.as_bits_mut().as_bits_mut().set_bit(shift,false);
}
}
result

View File

@@ -61,7 +61,7 @@ fn from_f32(){
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
//16 is within the 24 bits of float precision
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.fix_2()).try_into();
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.widen_2()).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
let b:Result<I32F32,_>=f32::MIN_POSITIVE.try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Underflow));
@@ -136,11 +136,24 @@ fn test_bint(){
}
#[test]
fn test_fix(){
assert_eq!(I32F32::ONE.fix_8(),I256F256::ONE);
assert_eq!(I32F32::ONE,I256F256::ONE.fix_1());
assert_eq!(I32F32::NEG_ONE.fix_8(),I256F256::NEG_ONE);
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.fix_1());
fn test_wrap(){
assert_eq!(I32F32::ONE,I256F256::ONE.wrap_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.wrap_1());
}
#[test]
fn test_narrow(){
assert_eq!(Ok(I32F32::ONE),I256F256::ONE.narrow_1());
assert_eq!(Ok(I32F32::NEG_ONE),I256F256::NEG_ONE.narrow_1());
}
#[test]
fn test_widen(){
assert_eq!(I32F32::ONE.widen_8(),I256F256::ONE);
assert_eq!(I32F32::NEG_ONE.widen_8(),I256F256::NEG_ONE);
}
#[test]
fn test_clamp(){
assert_eq!(I32F32::ONE,I256F256::ONE.clamp_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.clamp_1());
}
#[test]
fn test_sqrt(){
@@ -216,3 +229,16 @@ fn test_zeroes_deferred_division(){
])
);
}
#[test]
fn test_debug(){
assert_eq!(format!("{:?}",I32F32::EPSILON),"0.00000001");
assert_eq!(format!("{:?}",I32F32::ONE),"1.00000000");
assert_eq!(format!("{:?}",I32F32::TWO),"2.00000000");
assert_eq!(format!("{:?}",I32F32::MAX),"7fffffff.ffffffff");
assert_eq!(format!("{:?}",I32F32::try_from(core::f64::consts::PI).unwrap()),"3.243f6a88");
assert_eq!(format!("{:?}",I32F32::NEG_EPSILON),"-0.00000001");
assert_eq!(format!("{:?}",I32F32::NEG_ONE),"-1.00000000");
assert_eq!(format!("{:?}",I32F32::NEG_TWO),"-2.00000000");
assert_eq!(format!("{:?}",I32F32::MIN),"-80000000.00000000");
}

View File

@@ -15,8 +15,10 @@ macro_rules! impl_zeroes{
let radicand=a1*a1-a2*a0*4;
match radicand.cmp(&<Self as core::ops::Mul>::Output::ZERO){
Ordering::Greater=>{
// using wrap because sqrt always halves the number of leading digits.
// clamp would be more defensive, but is slower.
paste::item!{
let planar_radicand=radicand.sqrt().[<fix_ $n>]();
let planar_radicand=radicand.sqrt().[<wrap_ $n>]();
}
//sort roots ascending and avoid taking the difference of large numbers
let zeroes=match (a2pos,Self::ZERO<a1){

View File

@@ -1,6 +1,6 @@
[package]
name = "linear_ops"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -15,8 +15,11 @@ deferred-division=["dep:ratio_ops"]
[dependencies]
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", optional = true }
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", optional = true }
paste = { version = "1.0.15", optional = true }
[dev-dependencies]
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["wide-mul"] }
[lints]
workspace = true

View File

@@ -38,40 +38,95 @@ macro_rules! impl_fixed_wide_vector {
$crate::macro_4!(impl_fixed_wide_vector_not_const_generic,());
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
$crate::macro_repeated!(
impl_fix_not_const_generic,(),
(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),
(1,2),(2,2),(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
(1,3),(2,3),(3,3),(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
(1,4),(2,4),(3,4),(4,4),(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
(1,5),(2,5),(3,5),(4,5),(5,5),(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
(1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),(7,7),(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),(8,8),(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),(9,9),(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),(10,10),(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),(11,11),(12,11),(13,11),(14,11),(15,11),(16,11),
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),(12,12),(13,12),(14,12),(15,12),(16,12),
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),(13,13),(14,13),(15,13),(16,13),
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),(14,14),(15,14),(16,14),
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),(15,15),(16,15),
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),(16,16)
impl_narrow_not_const_generic,(),
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
(12,11),(13,11),(14,11),(15,11),(16,11),
(13,12),(14,12),(15,12),(16,12),
(14,13),(15,13),(16,13),
(15,14),(16,14),
(16,15)
);
$crate::macro_repeated!(
impl_widen_not_const_generic,(),
(1,2),
(1,3),(2,3),
(1,4),(2,4),(3,4),
(1,5),(2,5),(3,5),(4,5),
(1,6),(2,6),(3,6),(4,6),(5,6),
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
(1,17)
);
impl<const N:usize,T:fixed_wide::fixed::Wrap<U>,U> fixed_wide::fixed::Wrap<Vector<N,U>> for Vector<N,T>
{
#[inline]
fn wrap(self)->Vector<N,U>{
self.map(|t|t.wrap())
}
}
impl<const N:usize,T:fixed_wide::fixed::Clamp<U>,U> fixed_wide::fixed::Clamp<Vector<N,U>> for Vector<N,T>
{
#[inline]
fn clamp(self)->Vector<N,U>{
self.map(|t|t.clamp())
}
}
};
}
#[doc(hidden)]
#[macro_export(local_inner_macros)]
macro_rules! impl_fix_not_const_generic{
macro_rules! impl_narrow_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>
{
paste::item!{
paste::item!{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
#[inline]
pub fn [<fix_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
self.map(|t|t.[<fix_ $rhs>]())
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
self.map(|t|t.[<wrap_ $rhs>]())
}
#[inline]
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>,fixed_wide::fixed::NarrowError>>{
self.map(|t|t.[<narrow_ $rhs>]())
}
#[inline]
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
self.map(|t|t.[<clamp_ $rhs>]())
}
}
}
}
}
#[doc(hidden)]
#[macro_export(local_inner_macros)]
macro_rules! impl_widen_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
paste::item!{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
#[inline]
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
self.map(|t|t.[<widen_ $rhs>]())
}
}
}

View File

@@ -204,13 +204,21 @@ 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:*const [[T;$size_inner];$size_outer]=&self.array;
let ptr=ptr 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 [[T;$size_inner];$size_outer]=&mut self.array;
let ptr=ptr as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}
}
}

View File

@@ -58,6 +58,15 @@ macro_rules! impl_vector {
}
}
impl<const N:usize,T,E:std::fmt::Debug> Vector<N,Result<T,E>>{
#[inline]
pub fn unwrap(self)->Vector<N,T>{
Vector{
array:self.array.map(Result::unwrap)
}
}
}
impl<const N:usize,T:Ord> Vector<N,T>{
#[inline]
pub fn min(self,rhs:Self)->Self{
@@ -321,13 +330,21 @@ 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:*const [T;$size]=&self.array;
let ptr=ptr 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 [T;$size]=&mut self.array;
let ptr=ptr as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::vector::Vector;
#[repr(transparent)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct Matrix<const X:usize,const Y:usize,T>{
pub(crate) array:[[T;Y];X],

View File

@@ -3,6 +3,7 @@
/// v.x += v.z;
/// println!("v.x={}",v.x);
#[repr(transparent)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct Vector<const N:usize,T>{
pub(crate) array:[T;N],

View File

@@ -1,6 +1,6 @@
[package]
name = "ratio_ops"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -8,3 +8,6 @@ description = "Ratio operations using trait bounds for avoiding division like th
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
[lints]
workspace = true

View File

@@ -268,30 +268,35 @@ impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialEq<Ratio<RhsNum,RhsDen>> for Ratio<
}
impl<Num,Den> Eq for Ratio<Num,Den> where Self:PartialEq{}
// Wow! These were both completely wrong!
// Idea: use a 'signed' trait instead of parity and float the sign to the numerator.
impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialOrd<Ratio<RhsNum,RhsDen>> for Ratio<LhsNum,LhsDen>
where
LhsNum:Copy,
LhsDen:Copy,
LhsDen:Copy+Parity,
RhsNum:Copy,
RhsDen:Copy,
RhsDen:Copy+Parity,
LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
RhsNum:core::ops::Mul<LhsDen,Output=U>,
T:PartialOrd<U>,
RhsDen:core::ops::Mul<LhsNum,Output=U>,
T:PartialOrd<U>+Ord,
{
#[inline]
fn partial_cmp(&self,other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{
(self.num*other.den).partial_cmp(&(other.num*self.den))
fn partial_cmp(&self,&other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{
self.partial_cmp_ratio(other)
}
}
impl<Num,Den,T> Ord for Ratio<Num,Den>
where
Num:Copy,
Den:Copy,
Den:Copy+Parity,
Num:core::ops::Mul<Den,Output=T>,
Den:core::ops::Mul<Num,Output=T>,
T:Ord,
{
#[inline]
fn cmp(&self,other:&Self)->std::cmp::Ordering{
(self.num*other.den).cmp(&(other.num*self.den))
fn cmp(&self,&other:&Self)->std::cmp::Ordering{
self.cmp_ratio(other)
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_rbx_loader"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -12,13 +12,17 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
bytemuck = "1.14.3"
glam = "0.30.0"
lazy-regex = "3.1.0"
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.3.1"
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
regex = { version = "1.11.3", default-features = false }
rbx_mesh = "0.5.0"
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
rbx_binary = "2.0.1"
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_xml = "2.0.1"
[lints]
workspace = true

245
lib/rbx_loader/src/error.rs Normal file
View File

@@ -0,0 +1,245 @@
use std::collections::HashSet;
use std::num::ParseIntError;
use strafesnet_common::gameplay_modes::{StageId,ModeId};
use strafesnet_common::integer::{FixedFromFloatError,Planar64TryFromFloatError};
/// A collection of errors which can be ignored at your peril
#[derive(Debug,Default)]
pub struct RecoverableErrors{
/// A basepart has an invalid / missing property.
pub basepart_property:Vec<InstancePath>,
/// A part has an unconvertable CFrame.
pub basepart_cframe:Vec<CFrameError>,
/// A part has an unconvertable Velocity.
pub basepart_velocity:Vec<Planar64ConvertError>,
/// A part has an invalid / missing property.
pub part_property:Vec<InstancePath>,
/// A part has an invalid shape.
pub part_shape:Vec<ShapeError>,
/// A meshpart has an invalid / missing property.
pub meshpart_property:Vec<InstancePath>,
/// A meshpart has no mesh.
pub meshpart_content:Vec<InstancePath>,
/// A basepart has an unsupported subclass.
pub unsupported_class:HashSet<String>,
/// A decal has an invalid / missing property.
pub decal_property:Vec<InstancePath>,
/// A decal has an invalid normal_id.
pub normal_id:Vec<NormalIdError>,
/// A texture has an invalid / missing property.
pub texture_property:Vec<InstancePath>,
/// A mode_id failed to parse.
pub mode_id_parse_int:Vec<ParseIntContext>,
/// There is a duplicate mode.
pub duplicate_mode:HashSet<ModeId>,
/// A mode_id failed to parse.
pub stage_id_parse_int:Vec<ParseIntContext>,
/// A Stage was duplicated leading to undefined behaviour.
pub duplicate_stage:HashSet<DuplicateStageError>,
/// A WormholeOut id failed to parse.
pub wormhole_out_id_parse_int:Vec<ParseIntContext>,
/// A WormholeOut was duplicated leading to undefined behaviour.
pub duplicate_wormhole_out:HashSet<u32>,
/// A WormholeIn id failed to parse.
pub wormhole_in_id_parse_int:Vec<ParseIntContext>,
/// A jump limit failed to parse.
pub jump_limit_parse_int:Vec<ParseIntContext>,
}
impl RecoverableErrors{
pub fn count(&self)->usize{
self.basepart_property.len()+
self.basepart_cframe.len()+
self.basepart_velocity.len()+
self.part_property.len()+
self.part_shape.len()+
self.meshpart_property.len()+
self.meshpart_content.len()+
self.unsupported_class.len()+
self.decal_property.len()+
self.normal_id.len()+
self.texture_property.len()+
self.mode_id_parse_int.len()+
self.duplicate_mode.len()+
self.stage_id_parse_int.len()+
self.duplicate_stage.len()+
self.wormhole_out_id_parse_int.len()+
self.duplicate_wormhole_out.len()+
self.wormhole_in_id_parse_int.len()+
self.jump_limit_parse_int.len()
}
}
fn write_comma_separated<T>(
f:&mut std::fmt::Formatter<'_>,
mut it:impl Iterator<Item=T>,
custom_write:impl Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result
)->std::fmt::Result{
if let Some(t)=it.next(){
custom_write(f,t)?;
for t in it{
write!(f,", ")?;
custom_write(f,t)?;
}
}
Ok(())
}
macro_rules! write_instance_path_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} {}: ",$problem)?;
write_comma_separated($f,$self.$field.iter(),|f,InstancePath(path)|
write!(f,"{path}")
)?;
writeln!($f)?;
}
};
}
macro_rules! write_duplicate_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} duplicates: ")?;
write_comma_separated($f,$self.$field.iter(),|f,id|
write!(f,"{id}")
)?;
writeln!($f)?;
}
};
}
macro_rules! write_bespoke_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal,$path_field:ident,$error_field:ident)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} {}: ",$problem)?;
write_comma_separated($f,$self.$field.iter(),|f,context|
write!(f,"{} ({})",context.$path_field,context.$error_field)
)?;
writeln!($f)?;
}
};
}
impl core::fmt::Display for RecoverableErrors{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write_instance_path_error!(f,self,basepart_property,"BasePart is","BaseParts are","missing a property");
write_bespoke_error!(f,self,basepart_cframe,"BasePart","BaseParts","CFrame float convert failed",path,error);
write_bespoke_error!(f,self,basepart_velocity,"BasePart","BaseParts","Velocity float convert failed",path,error);
write_instance_path_error!(f,self,part_property,"Part is","Parts are","missing a property");
write_bespoke_error!(f,self,part_shape,"Part","Parts","Shape is invalid",path,shape);
write_instance_path_error!(f,self,meshpart_property,"MeshPart is","MeshParts are","missing a property");
write_instance_path_error!(f,self,meshpart_content,"MeshPart has","MeshParts have","no mesh");
{
let len=self.unsupported_class.len();
if len!=0{
let plural=if len==1{"Class is"}else{"Classes are"};
write!(f,"The following {plural} not supported: ")?;
write_comma_separated(f,self.unsupported_class.iter(),|f,classname|write!(f,"{classname}"))?;
writeln!(f)?;
}
}
write_instance_path_error!(f,self,decal_property,"Decal is","Decals are","missing a property");
write_bespoke_error!(f,self,normal_id,"Decal","Decals","NormalId is invalid",path,normal_id);
write_instance_path_error!(f,self,texture_property,"Texture is","Textures are","missing a property");
write_bespoke_error!(f,self,mode_id_parse_int,"ModeId","ModeIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_mode,"ModeId has","ModeIds have");
write_bespoke_error!(f,self,stage_id_parse_int,"StageId","StageIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_stage,"StageId has","StageIds have");
write_bespoke_error!(f,self,wormhole_out_id_parse_int,"WormholeOutId","WormholeOutIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_wormhole_out,"WormholeOutId has","WormholeOutIds have");
write_bespoke_error!(f,self,wormhole_in_id_parse_int,"WormholeInId","WormholeInIds","failed to parse",context,error);
write_bespoke_error!(f,self,jump_limit_parse_int,"jump limit","jump limits","failed to parse",context,error);
Ok(())
}
}
/// A Decal was missing required properties
#[derive(Debug)]
pub struct InstancePath(pub String);
impl core::fmt::Display for InstancePath{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
self.0.fmt(f)
}
}
impl InstancePath{
pub fn new(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->InstancePath{
let mut names:Vec<_>=core::iter::successors(
Some(instance),
|i|dom.get_by_ref(i.parent())
).map(
|i|i.name.as_str()
).collect();
// discard the name of the root object
names.pop();
names.reverse();
InstancePath(names.join("."))
}
}
#[derive(Debug)]
pub struct ParseIntContext{
pub context:String,
pub error:ParseIntError,
}
impl ParseIntContext{
pub fn parse<T:core::str::FromStr<Err=ParseIntError>>(input:&str)->Result<T,Self>{
input.parse().map_err(|error|ParseIntContext{
context:input.to_owned(),
error,
})
}
}
#[derive(Debug)]
pub struct NormalIdError{
pub path:InstancePath,
pub normal_id:u32,
}
#[derive(Debug)]
pub struct ShapeError{
pub path:InstancePath,
pub shape:u32,
}
#[derive(Debug)]
pub enum CFrameErrorType{
ZeroDeterminant,
Convert(FixedFromFloatError),
}
impl core::fmt::Display for CFrameErrorType{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
#[derive(Debug)]
pub struct CFrameError{
pub path:InstancePath,
pub error:CFrameErrorType,
}
#[derive(Debug)]
pub struct Planar64ConvertError{
pub path:InstancePath,
pub error:Planar64TryFromFloatError,
}
#[derive(Debug,Hash,Eq,PartialEq)]
pub struct DuplicateStageError{
pub mode_id:ModeId,
pub stage_id:StageId,
}
impl core::fmt::Display for DuplicateStageError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{}-Spawn{}",self.mode_id,self.stage_id.get())
}
}

View File

@@ -1,12 +1,18 @@
use std::io::Read;
use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_common::map::CompleteMap;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use error::RecoverableErrors;
pub use roblox_emulator::runner::Error as RunnerError;
mod rbx;
mod mesh;
mod error;
mod union;
pub mod loader;
mod primitives;
pub mod primitives;
pub mod data{
pub struct RobloxMeshBytes(Vec<u8>);
@@ -27,13 +33,7 @@ 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>{
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
to_snf(self,failure_mode)
}
}
@@ -44,36 +44,43 @@ 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 runner=roblox_emulator::runner::Runner::new().unwrap();
let context=roblox_emulator::context::Context::from_mut(dom);
pub fn run_scripts(&mut self)->Result<Vec<RunnerError>,RunnerError>{
let Place{context}=self;
let runner=roblox_emulator::runner::Runner::new()?;
let scripts=context.scripts();
let runnable=runner.runnable_context_with_services(context,services).unwrap();
let runnable=runner.runnable_context(context)?;
let mut errors=Vec::new();
for script in scripts{
if let Err(e)=runnable.run_script(script){
println!("runner error: {e}");
errors.push(e);
}
}
Ok(errors)
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
to_snf(self,failure_mode)
}
}
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 +101,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(buf).map(Model::new).map_err(ReadError::RbxBinary),
b"<roblox "=>rbx_xml::from_reader_default(buf).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),
}
}
@@ -123,7 +130,7 @@ impl From<loader::MeshError> for LoadError{
}
}
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
let dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
@@ -143,7 +150,5 @@ fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesn
let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
Ok(map_step2.add_render_configs_and_textures(render_configs))
}

View File

@@ -4,7 +4,12 @@ use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxFaceTextureDescription;
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)?;
@@ -13,7 +18,6 @@ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::E
Ok(data)
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -36,17 +40,17 @@ impl From<RobloxAssetIdParseErr> for TextureError{
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub struct TextureLoader;
impl TextureLoader{
pub fn new()->Self{
Self(std::marker::PhantomData)
Self
}
}
impl<'a> Loader for TextureLoader<'a>{
impl Loader for TextureLoader{
type Error=TextureError;
type Index=&'a str;
type Index<'a>=&'a str;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?;
@@ -54,7 +58,6 @@ impl<'a> Loader for TextureLoader<'a>{
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
@@ -104,7 +107,7 @@ pub enum MeshType<'a>{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size_float_bits:[u32;3],
part_texture_description:[Option<RobloxFaceTextureDescription>;6],
part_texture_description:RobloxPartDescription,
},
}
#[derive(Hash,Eq,PartialEq)]
@@ -113,7 +116,7 @@ pub struct MeshIndex<'a>{
content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex{
pub fn file_mesh(content:&str)->MeshIndex<'_>{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
@@ -124,7 +127,7 @@ impl MeshIndex<'_>{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3,
part_texture_description:crate::rbx::RobloxPartDescription,
part_texture_description:RobloxPartDescription,
)->MeshIndex<'a>{
MeshIndex{
mesh_type:MeshType::Union{
@@ -138,17 +141,23 @@ impl MeshIndex<'_>{
}
}
pub struct MeshLoader<'a>(std::marker::PhantomData<&'a ()>);
impl MeshLoader<'_>{
#[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{
Self(std::marker::PhantomData)
Self
}
}
impl<'a> Loader for MeshLoader<'a>{
impl Loader for MeshLoader{
type Error=MeshError;
type Index=MeshIndex<'a>;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
type Index<'a>=MeshIndex<'a>;
type Resource=MeshWithSize;
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?;
@@ -171,12 +180,12 @@ impl<'a> Loader for MeshLoader<'a>{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("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("MeshData"){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
}

View File

@@ -1,12 +1,14 @@
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,PolygonGroup,PolygonList,RenderConfigId,VertexId};
use crate::loader::MeshWithSize;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RbxMesh(rbx_mesh::mesh::Error)
}
impl std::fmt::Display for Error{
@@ -16,202 +18,130 @@ impl std::fmt::Display for Error{
}
impl std::error::Error for Error{}
fn ingest_vertices2<
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireColorId,
AcquireVertexId,
>(
fn ingest_vertices2(
vertices:Vec<Vertex2>,
acquire_pos_id:&mut AcquirePosId,
acquire_tex_id:&mut AcquireTexId,
acquire_normal_id:&mut AcquireNormalId,
acquire_color_id:&mut AcquireColorId,
acquire_vertex_id:&mut AcquireVertexId,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>
where
AcquirePosId:FnMut([f32;3])->Result<PositionId,Error>,
AcquireTexId:FnMut([f32;2])->TextureCoordinateId,
AcquireNormalId:FnMut([f32;3])->Result<NormalId,Error>,
AcquireColorId:FnMut([f32;4])->ColorId,
AcquireVertexId:FnMut(IndexedVertex)->VertexId,
{
mb:&mut model::MeshBuilder,
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
// vertex positions that fail to convert are DROPPED
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id(vertex.tex),
normal:acquire_normal_id(vertex.norm)?,
color:acquire_color_id(vertex.color.map(|f|f as f32/255.0f32))
}),
))).collect::<Result<_,_>>()?)
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32))),
};
mb.acquire_vertex_id(vertex)
}
))).collect()
}
fn ingest_vertices_truncated2<
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireVertexId,
>(
fn ingest_vertices_truncated2(
vertices:Vec<Vertex2Truncated>,
acquire_pos_id:&mut AcquirePosId,
acquire_tex_id:&mut AcquireTexId,
acquire_normal_id:&mut AcquireNormalId,
mb:&mut model::MeshBuilder,
static_color_id:ColorId,//pick one color and fill everything with it
acquire_vertex_id:&mut AcquireVertexId,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>
where
AcquirePosId:FnMut([f32;3])->Result<PositionId,Error>,
AcquireTexId:FnMut([f32;2])->TextureCoordinateId,
AcquireNormalId:FnMut([f32;3])->Result<NormalId,Error>,
AcquireVertexId:FnMut(IndexedVertex)->VertexId,
{
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
// vertex positions that fail to convert are DROPPED
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id(vertex.tex),
normal:acquire_normal_id(vertex.norm)?,
color:static_color_id
}),
))).collect::<Result<_,_>>()?)
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:static_color_id,
};
mb.acquire_vertex_id(vertex)
}
))).collect()
}
fn ingest_faces2_lods3(
polygon_groups:&mut Vec<PolygonGroup>,
vertex_id_map:&HashMap<rbx_mesh::mesh::VertexId2,VertexId>,
faces:&Vec<rbx_mesh::mesh::Face2>,
lods:&Vec<rbx_mesh::mesh::Lod3>
faces:&[rbx_mesh::mesh::Face2],
lods:&[rbx_mesh::mesh::Lod3],
){
//faces have to be split into polygon groups based on lod
polygon_groups.extend(lods.windows(2).map(|lod_pair|
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().filter_map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
Some(vec![*vertex_id_map.get(&v0)?,*vertex_id_map.get(&v1)?,*vertex_id_map.get(&v2)?])
).collect()))
))
}
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();
let mut unique_tex=Vec::new();
let mut tex_id_from=HashMap::new();
let mut unique_normal=Vec::new();
let mut normal_id_from=HashMap::new();
let mut unique_color=Vec::new();
let mut color_id_from=HashMap::new();
let mut unique_vertices=Vec::new();
let mut vertex_id_from=HashMap::new();
let mut polygon_groups=Vec::new();
let mut acquire_pos_id=|pos|{
let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?;
Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=unique_pos.len();
unique_pos.push(p);
pos_id
}) as u32))
};
let mut acquire_tex_id=|tex|{
let h=bytemuck::cast::<[f32;2],[u32;2]>(tex);
TextureCoordinateId::new(*tex_id_from.entry(h).or_insert_with(||{
let tex_id=unique_tex.len();
unique_tex.push(glam::Vec2::from_array(tex));
tex_id
}) as u32)
};
let mut acquire_normal_id=|normal|{
let n=vec3::try_from_f32_array(normal).map_err(Error::Planar64Vec3)?;
Ok(NormalId::new(*normal_id_from.entry(n).or_insert_with(||{
let normal_id=unique_normal.len();
unique_normal.push(n);
normal_id
}) as u32))
};
let mut acquire_color_id=|color|{
let h=bytemuck::cast::<[f32;4],[u32;4]>(color);
ColorId::new(*color_id_from.entry(h).or_insert_with(||{
let color_id=unique_color.len();
unique_color.push(glam::Vec4::from_array(color));
color_id
}) as u32)
};
let mut acquire_vertex_id=|vertex:IndexedVertex|{
VertexId::new(*vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=unique_vertices.len();
unique_vertices.push(vertex);
vertex_id
}) as u32)
};
let mut mb=model::MeshBuilder::new();
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
rbx_mesh::mesh::VersionedMesh::Version1(mesh)=>{
let color_id=acquire_color_id([1.0f32;4]);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|Ok(acquire_vertex_id(IndexedVertex{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id([vertex.tex[0],vertex.tex[1]]),
normal:acquire_normal_id(vertex.norm)?,
color:color_id,
}));
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect::<Result<_,_>>()?)));
rbx_mesh::mesh::Mesh::V1(mesh)=>{
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).filter_map(|trip|{
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::vec2(vertex.tex[0],vertex.tex[1])),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:color_id,
};
Some(mb.acquire_vertex_id(vertex))
};
Some(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect())));
},
rbx_mesh::mesh::VersionedMesh::Version2(mesh)=>{
rbx_mesh::mesh::Mesh::V2(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
//pick white and make all the vertices white
let color_id=acquire_color_id([1.0f32;4]);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id)
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id),
}?;
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
};
//one big happy group for all the faces
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().filter_map(|face|
Some(vec![*vertex_id_map.get(&face.0)?,*vertex_id_map.get(&face.1)?,*vertex_id_map.get(&face.2)?])
).collect())));
},
rbx_mesh::mesh::VersionedMesh::Version3(mesh)=>{
rbx_mesh::mesh::Mesh::V3(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
let color_id=acquire_color_id([1.0f32;4]);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id)
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id),
}?;
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
};
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::VersionedMesh::Version4(mesh)=>{
let vertex_id_map=ingest_vertices2(
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?;
rbx_mesh::mesh::Mesh::V4(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::VersionedMesh::Version5(mesh)=>{
let vertex_id_map=ingest_vertices2(
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?;
rbx_mesh::mesh::Mesh::V5(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
}
Ok(model::Mesh{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
let mesh=mb.build(
polygon_groups,
//these should probably be moved to the model...
//but what if models want to use the same texture
graphics_groups:vec![model::IndexedGraphicsGroup{
vec![model::IndexedGraphicsGroup{
render:RenderConfigId::new(0),
//the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)]
}],
//disable physics
physics_groups:Vec::new(),
})
Vec::new(),
);
let mut aabb=Aabb::default();
for &point in &mesh.unique_pos{
aabb.grow(point);
}
Ok(MeshWithSize{mesh,size:aabb.size()})
}

View File

@@ -1,5 +1,6 @@
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::integer::{vec3,Planar64Vec3};
use crate::rbx::{RobloxPartDescription,RobloxWedgeDescription,RobloxCornerWedgeDescription};
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,MeshBuilder,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::integer::{vec3,Planar64,Planar64Vec3};
#[derive(Debug)]
pub enum Primitives{
@@ -9,7 +10,22 @@ pub enum Primitives{
Wedge,
CornerWedge,
}
#[derive(Hash,PartialEq,Eq)]
#[derive(Debug)]
pub struct PrimitivesError;
impl TryFrom<u32> for Primitives{
type Error=PrimitivesError;
fn try_from(value:u32)->Result<Self,Self::Error>{
match value{
0=>Ok(Primitives::Sphere),
1=>Ok(Primitives::Cube),
2=>Ok(Primitives::Cylinder),
3=>Ok(Primitives::Wedge),
4=>Ok(Primitives::CornerWedge),
_=>Err(PrimitivesError),
}
}
}
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
pub enum CubeFace{
Right,
Top,
@@ -18,13 +34,29 @@ pub enum CubeFace{
Bottom,
Front,
}
#[derive(Debug)]
pub struct CubeFaceError;
impl TryFrom<u32> for CubeFace{
type Error=CubeFaceError;
fn try_from(value:u32)->Result<Self,Self::Error>{
match value{
0=>Ok(CubeFace::Right),
1=>Ok(CubeFace::Top),
2=>Ok(CubeFace::Back),
3=>Ok(CubeFace::Left),
4=>Ok(CubeFace::Bottom),
5=>Ok(CubeFace::Front),
_=>Err(CubeFaceError),
}
}
}
const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[
TextureCoordinate::new(0.0,0.0),
TextureCoordinate::new(1.0,0.0),
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
@@ -34,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
@@ -43,103 +75,36 @@ const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 0, 0,-1),//CubeFace::Front
];
#[derive(Hash,PartialEq,Eq)]
pub enum WedgeFace{
Right,
TopFront,
Back,
Left,
Bottom,
}
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//Wedge::Right
vec3::int( 0, 1,-1),//Wedge::TopFront
vec3::int( 0, 0, 1),//Wedge::Back
vec3::int(-1, 0, 0),//Wedge::Left
vec3::int( 0,-1, 0),//Wedge::Bottom
];
/*
local cornerWedgeVerticies = {
Vector3.new(-1/2,-1/2,-1/2),7
Vector3.new(-1/2,-1/2, 1/2),0
Vector3.new( 1/2,-1/2,-1/2),6
Vector3.new( 1/2,-1/2, 1/2),1
Vector3.new( 1/2, 1/2,-1/2),5
}
*/
#[derive(Hash,PartialEq,Eq)]
pub enum CornerWedgeFace{
Right,
TopBack,
TopLeft,
Bottom,
Front,
}
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//CornerWedge::Right
vec3::int( 0, 1, 1),//CornerWedge::BackTop
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front
];
#[derive(Default)]
pub struct CubeFaceDescription([Option<FaceDescription>;6]);
pub struct CubeFaceDescription([FaceDescription;Self::FACES]);
impl CubeFaceDescription{
pub fn insert(&mut self,index:CubeFace,value:FaceDescription){
self.0[index as usize]=Some(value);
}
pub fn pairs(self)->impl Iterator<Item=(usize,FaceDescription)>{
self.0.into_iter().enumerate().filter_map(|(i,v)|v.map(|u|(i,u)))
pub const FACES:usize=6;
pub fn new(RobloxPartDescription(part_description):RobloxPartDescription,textureless_render_id:RenderConfigId)->Self{
Self(part_description.map(|face_description|match face_description{
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
None=>FaceDescription::new_with_render_id(textureless_render_id),
}))
}
}
pub fn unit_cube(render:RenderConfigId)->Mesh{
let mut t=CubeFaceDescription::default();
t.insert(CubeFace::Right,FaceDescription::new_with_render_id(render));
t.insert(CubeFace::Top,FaceDescription::new_with_render_id(render));
t.insert(CubeFace::Back,FaceDescription::new_with_render_id(render));
t.insert(CubeFace::Left,FaceDescription::new_with_render_id(render));
t.insert(CubeFace::Bottom,FaceDescription::new_with_render_id(render));
t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render));
generate_partial_unit_cube(t)
}
#[derive(Default)]
pub struct WedgeFaceDescription([Option<FaceDescription>;5]);
pub struct WedgeFaceDescription([FaceDescription;Self::FACES]);
impl WedgeFaceDescription{
pub fn insert(&mut self,index:WedgeFace,value:FaceDescription){
self.0[index as usize]=Some(value);
}
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
pub const FACES:usize=5;
pub fn new(RobloxWedgeDescription(part_description):RobloxWedgeDescription,textureless_render_id:RenderConfigId)->Self{
Self(part_description.map(|face_description|match face_description{
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
None=>FaceDescription::new_with_render_id(textureless_render_id),
}))
}
}
// pub fn unit_wedge(render:RenderConfigId)->Mesh{
// let mut t=WedgeFaceDescription::default();
// t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// generate_partial_unit_wedge(t)
// }
#[derive(Default)]
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
pub struct CornerWedgeFaceDescription([FaceDescription;Self::FACES]);
impl CornerWedgeFaceDescription{
pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){
self.0[index as usize]=Some(value);
}
pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
pub const FACES:usize=5;
pub fn new(RobloxCornerWedgeDescription(part_description):RobloxCornerWedgeDescription,textureless_render_id:RenderConfigId)->Self{
Self(part_description.map(|face_description|match face_description{
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
None=>FaceDescription::new_with_render_id(textureless_render_id),
}))
}
}
// pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
// let mut t=CornerWedgeFaceDescription::default();
// t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render));
// generate_partial_unit_cornerwedge(t)
// }
#[derive(Clone)]
pub struct FaceDescription{
@@ -148,7 +113,7 @@ pub struct FaceDescription{
pub color:Color4,
}
impl FaceDescription{
pub fn new_with_render_id(render:RenderConfigId)->Self {
pub fn new_with_render_id(render:RenderConfigId)->Self{
Self{
render,
transform:glam::Affine2::IDENTITY,
@@ -156,51 +121,51 @@ impl FaceDescription{
}
}
}
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
pub const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
// right (1, 0, 0)
[
[6,2,0],//[vertex,tex,norm]
[5,1,0],
[2,0,0],
[1,3,0],
[6,2],//[vertex,tex]
[5,1],
[2,0],
[1,3],
],
// top (0, 1, 0)
[
[5,3,1],
[4,2,1],
[3,1,1],
[2,0,1],
[5,3],
[4,2],
[3,1],
[2,0],
],
// back (0, 0, 1)
[
[0,3,2],
[1,2,2],
[2,1,2],
[3,0,2],
[0,3],
[1,2],
[2,1],
[3,0],
],
// left (-1, 0, 0)
[
[0,2,3],
[3,1,3],
[4,0,3],
[7,3,3],
[0,2],
[3,1],
[4,0],
[7,3],
],
// bottom (0,-1, 0)
[
[1,1,4],
[0,0,4],
[7,3,4],
[6,2,4],
[1,1],
[0,0],
[7,3],
[6,2],
],
// front (0, 0,-1)
[
[4,1,5],
[5,0,5],
[6,3,5],
[7,2,5],
[4,1],
[5,0],
[6,3],
[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();
@@ -211,7 +176,7 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new();
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
for (face_id,face_description) in face_descriptions.pairs(){
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
//assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index
@@ -238,8 +203,8 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
//push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
CUBE_DEFAULT_POLYS[face_id].map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
CUBE_DEFAULT_POLYS[face_id].map(|[pos_id,tex_id]|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index
}else{
@@ -251,7 +216,7 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
//always push vertex
let vertex=IndexedVertex{
pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index),
normal:NormalId::new(normal_index),
color:ColorId::new(color_index),
};
@@ -278,42 +243,49 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
}
}
//don't think too hard about the copy paste because this is all going into the map tool eventually...
pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{
const WEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)->Mesh{
const WEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[
// right (1, 0, 0)
&[
[6,2,0],//[vertex,tex,norm]
[2,0,0],
[1,3,0],
[6,2],//[vertex,tex]
[2,0],
[1,3],
],
// FrontTop (0, 1, -1)
&[
[3,1,1],
[2,0,1],
[6,3,1],
[7,2,1],
[3,1],
[2,0],
[6,3],
[7,2],
],
// back (0, 0, 1)
&[
[0,3,2],
[1,2,2],
[2,1,2],
[3,0,2],
[0,3],
[1,2],
[2,1],
[3,0],
],
// left (-1, 0, 0)
&[
[0,2,3],
[3,1,3],
[7,3,3],
[0,2],
[3,1],
[7,3],
],
// bottom (0,-1, 0)
&[
[1,1,4],
[0,0,4],
[7,3,4],
[6,2,4],
[1,1],
[0,0],
[7,3],
[6,2],
],
];
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//Wedge::Right
vec3::int( 0, 1,-1),//Wedge::TopFront
vec3::int( 0, 0, 1),//Wedge::Back
vec3::int(-1, 0, 0),//Wedge::Left
vec3::int( 0,-1, 0),//Wedge::Bottom
];
let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new();
@@ -324,7 +296,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new();
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
for (face_id,face_description) in face_descriptions.pairs(){
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
//assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index
@@ -351,8 +323,8 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
//push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
WEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
WEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index
}else{
@@ -364,7 +336,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
//always push vertex
let vertex=IndexedVertex{
pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index),
normal:NormalId::new(normal_index),
color:ColorId::new(color_index),
};
@@ -391,40 +363,47 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
}
}
pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedgeFaceDescription)->Mesh{
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[
// right (1, 0, 0)
&[
[6,2,0],//[vertex,tex,norm]
[5,1,0],
[1,3,0],
[6,2],//[vertex,tex]
[5,1],
[1,3],
],
// BackTop (0, 1, 1)
&[
[5,3,1],
[0,1,1],
[1,0,1],
[5,3],
[0,1],
[1,0],
],
// LeftTop (-1, 1, 0)
&[
[5,3,2],
[7,2,2],
[0,1,2],
[5,3],
[7,2],
[0,1],
],
// bottom (0,-1, 0)
&[
[1,1,3],
[0,0,3],
[7,3,3],
[6,2,3],
[1,1],
[0,0],
[7,3],
[6,2],
],
// front (0, 0,-1)
&[
[5,0,4],
[6,3,4],
[7,2,4],
[5,0],
[6,3],
[7,2],
],
];
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//CornerWedge::Right
vec3::int( 0, 1, 1),//CornerWedge::BackTop
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front
];
let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new();
@@ -435,7 +414,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new();
//note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices.
for (face_id,face_description) in face_descriptions.pairs(){
for (face_id,face_description) in face_descriptions.into_iter().enumerate(){
//assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index
@@ -462,8 +441,8 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
//push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index
}else{
@@ -475,7 +454,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
//always push vertex
let vertex=IndexedVertex{
pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tup[1]+4*transform_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index),
normal:NormalId::new(normal_index),
color:ColorId::new(color_index),
};
@@ -501,3 +480,133 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
physics_groups:vec![physics_group],
}
}
// TODO: fix face texture orientation
pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
// cylinder is oriented about the x axis
// roblox cylinders use projected grid coordinates
/// how many grid coordinates to use (positive and negative)
const GON:i32=3;
/// grid perimeter
const POINTS:[[i32;2];4*2*GON as usize]=const{
let mut points=[[0;2];{4*2*GON as usize}];
let mut i=-GON;
while i<GON{
points[(i+GON) as usize]=[i,GON];
points[(i+GON+1*2*GON) as usize]=[GON,-i];
points[(i+GON+2*2*GON) as usize]=[-i,-GON];
points[(i+GON+3*2*GON) as usize]=[-GON,i];
i+=1;
}
points
};
let mut mb=MeshBuilder::new();
let mut polygon_groups=Vec::with_capacity(CubeFaceDescription::FACES);
let mut graphics_groups=Vec::with_capacity(CubeFaceDescription::FACES);
let mut physics_group=IndexedPhysicsGroup{groups:Vec::with_capacity(CubeFaceDescription::FACES)};
let CubeFaceDescription([right,top,back,left,bottom,front])=face_descriptions;
macro_rules! end_face{
($face_description:expr,$end:expr,$iter:expr)=>{
let normal=mb.acquire_normal_id($end);
let color=mb.acquire_color_id($face_description.color);
// single polygon for physics
let polygon:Vec<_>=$iter.map(|[x,y]|{
let tex=mb.acquire_tex_id(
$face_description.transform.transform_point2(
(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
)
);
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
}).collect();
// fanned polygons for graphics
let pos=mb.acquire_pos_id($end);
let tex=mb.acquire_tex_id($face_description.transform.transform_point2(glam::Vec2::ONE/2.0));
let center=mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color});
let polygon_list=(0..POINTS.len()).map(|i|
vec![center,polygon[i],polygon[(i+1)%POINTS.len()]]
).collect();
// end face graphics
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
graphics_groups.push(IndexedGraphicsGroup{
render:$face_description.render,
groups:vec![group_id],
});
// end face physics
let polygon_list=vec![polygon];
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
physics_group.groups.push(group_id);
}
}
macro_rules! tex{
($face_description:expr,$tex:expr)=>{{
let [x,y]=$tex;
$face_description.transform.transform_point2(
glam::vec2((x+GON) as f32,(y+GON) as f32)/(2*GON) as f32
)
}};
}
macro_rules! barrel_face{
($face_description:expr,$loop:ident,$lo_dir:expr,$hi_dir:expr,$tex_0:expr,$tex_1:expr,$tex_2:expr,$tex_3:expr)=>{
let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
for $loop in -GON..GON{
// lo Z
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
// hi Z
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
// pos
let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);
let lx_hz_pos=mb.acquire_pos_id(vec3::NEG_X+hz_dir);
let hx_hz_pos=mb.acquire_pos_id(vec3::X+hz_dir);
let hx_lz_pos=mb.acquire_pos_id(vec3::X+lz_dir);
// tex
let lx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_0));
let lx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_1));
let hx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_2));
let hx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_3));
// norm
let lz_norm=mb.acquire_normal_id(lz_dir);
let hz_norm=mb.acquire_normal_id(hz_dir);
// color
let color=mb.acquire_color_id($face_description.color);
polygon_list.push(vec![
mb.acquire_vertex_id(IndexedVertex{pos:lx_lz_pos,tex:lx_lz_tex,normal:lz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:lx_hz_pos,tex:lx_hz_tex,normal:hz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:hx_hz_pos,tex:hx_hz_tex,normal:hz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:hx_lz_pos,tex:hx_lz_tex,normal:lz_norm,color}),
]);
}
// push face
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
graphics_groups.push(IndexedGraphicsGroup{
render:$face_description.render,
groups:vec![group_id],
});
physics_group.groups.push(group_id);
};
}
end_face!(right, vec3::X,POINTS.into_iter());
barrel_face!(top, z,vec3::int(0,GON,z),vec3::int(0,GON,z+1), [GON,z],[GON,z+1],[-GON,z+1],[-GON,z]);
barrel_face!(back, y,vec3::int(0,y+1,GON),vec3::int(0,y,GON), [GON,y+1],[GON,y],[-GON,y],[-GON,y+1]);
end_face!(left, vec3::NEG_X,POINTS.into_iter().rev());
barrel_face!(bottom, z,vec3::int(0,-GON,z+1),vec3::int(0,-GON,z), [-GON,z+1],[-GON,z],[GON,z],[GON,z+1]);
barrel_face!(front, y,vec3::int(0,y,-GON),vec3::int(0,y+1,-GON), [-GON,y],[-GON,y+1],[GON,y+1],[GON,y]);
let physics_groups=vec![physics_group];
mb.build(polygon_groups,graphics_groups,physics_groups)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use crate::loader::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,FaceDescription};
use rbx_mesh::mesh_data::{VertexId as MeshDataVertexId,NormalId as MeshDataNormalId,NormalId2 as MeshDataNormalId2,NormalId5 as MeshDataNormalId5};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,MeshBuilder,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId,VertexId};
use strafesnet_common::integer::vec3;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Block,
@@ -20,7 +24,7 @@ impl std::fmt::Display for Error{
// wacky state machine to make sure all vertices in a face agree upon what NormalId to use.
// Roblox duplicates this information per vertex when it should only exist per-face.
enum MeshDataNormalStatus{
Agree(MeshDataNormalId2),
Agree(MeshDataNormalId),
Conflicting,
}
struct MeshDataNormalChecker{
@@ -30,7 +34,7 @@ impl MeshDataNormalChecker{
fn new()->Self{
Self{status:None}
}
fn check(&mut self,normal:MeshDataNormalId2){
fn check(&mut self,normal:MeshDataNormalId){
self.status=match self.status.take(){
None=>Some(MeshDataNormalStatus::Agree(normal)),
Some(MeshDataNormalStatus::Agree(old_normal))=>{
@@ -43,7 +47,7 @@ impl MeshDataNormalChecker{
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
};
}
fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
fn into_agreed_normal(self)->Option<MeshDataNormalId>{
self.status.and_then(|status|match status{
MeshDataNormalStatus::Agree(normal)=>Some(normal),
MeshDataNormalStatus::Conflicting=>None,
@@ -51,18 +55,127 @@ impl MeshDataNormalChecker{
}
}
fn build_mesh2(
mb:&mut MeshBuilder,
polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
mesh:rbx_mesh::mesh_data::Mesh2,
)->Result<(),Error>{
//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 &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 mesh.faces{
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))?,
];
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{
let MeshDataNormalId2(normal_id)=vertex.normal_id;
normal_agreement_checker.check(normal_id);
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[normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
Ok(())
}
fn build_mesh5(
mb:&mut MeshBuilder,
polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
mesh:rbx_mesh::mesh_data::CSGMDL5,
)->Result<(),Error>{
//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 &pos in &mesh.positions{
let p=pos.into();
pos_min=pos_min.min(p);
pos_max=pos_max.max(p);
}
let graphics_size=pos_max-pos_min;
for face in mesh.faces.indices.chunks_exact(3){
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|&vertex_id|{
let vertex_index=vertex_id as usize;
let &pos=mesh.positions.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &MeshDataNormalId5(normal_id)=mesh.normal_ids.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &norm=mesh.normals.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &tex=mesh.tex.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &color=mesh.colors.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
normal_agreement_checker.check(normal_id);
let pos=glam::Vec3::from_array(pos)/graphics_size;
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array()).map_err(Error::Planar64Vec3)?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(norm).map_err(Error::Planar64Vec3)?);
let tex_coord=glam::Vec2::from_array(tex);
let maybe_face_description=&cube_face_description[normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>()?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
Ok(())
}
const NORMAL_FACES:usize=6;
impl std::error::Error for Error{}
pub fn convert(
roblox_physics_data:&[u8],
roblox_mesh_data:&[u8],
size:glam::Vec3,
part_texture_description:crate::rbx::RobloxPartDescription,
)->Result<model::Mesh,Error>{
const NORMAL_FACES:usize=6;
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<MeshWithSize,Error>{
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();
let mut mb=MeshBuilder::new();
// graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){
// create per-face texture coordinate affine transforms
@@ -74,46 +187,12 @@ pub fn convert(
let mesh_data=rbx_mesh::read_mesh_data_versioned(
std::io::Cursor::new(roblox_mesh_data)
).map_err(Error::RobloxMeshData)?;
let graphics_mesh=match mesh_data{
match mesh_data{
rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
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,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V2(mesh_data2))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data2.mesh)?,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4.mesh)?,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V5(mesh_data4))=>build_mesh5(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4)?,
};
for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
];
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 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];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
(0..NORMAL_FACES).map(|polygon_group_id|{
model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
@@ -125,7 +204,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)?;
@@ -134,44 +217,56 @@ pub fn convert(
// have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::Block),
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V3(meshes))
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V5(meshes))
=>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V6(meshes))
=>vec![meshes.mesh],
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V7(meshes))
=>meshes.meshes,
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(|[vertex_id0,vertex_id1,vertex_id2]|{
let face=[
mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
].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,
})
}

View File

@@ -9,3 +9,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
url = "2.5.4"
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "roblox_emulator"
version = "0.4.7"
version = "0.5.2"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -13,9 +13,12 @@ run-service=[]
[dependencies]
glam = "0.30.0"
mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.11.2", features = ["macros"] }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection = { version = "4.7.0", registry = "strafesnet" }
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_types = { version = "1.10.0", registry = "strafesnet" }
mlua = { version = "0.11.3", features = ["luau"] }
phf = { version = "0.13.1", features = ["macros"] }
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_types = "3.1.0"
[lints]
workspace = true

View File

@@ -1,93 +1,121 @@
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::Instance,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::Instance::new(script_ref),services)
}
pub fn from_ref(dom:&WeakDom)->&Context{
unsafe{&*(dom as *const WeakDom as *const Context)}
}
pub fn from_mut(dom:&mut WeakDom)->&mut Context{
unsafe{&mut *(dom as *mut WeakDom as *mut Context)}
);
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.class.as_ref(),superclass)
).map(|instance|instance.referent())
}
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect()
}
pub fn find_services(&self)->Option<Services>{
Some(Services{
workspace:*self.dom.root().children().iter().find(|&&r|
self.dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
)?,
game:self.dom.root_ref(),
let db=rbx_reflection_database::get().unwrap();
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().children().to_owned();
let children=dom.root().children().to_owned();
//insert services
let game=self.dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain");
let workspace=self.dom.insert(game,
let game=dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain")
.with_properties([
("CFrame",rbx_dom_weak::types::Variant::CFrame(rbx_dom_weak::types::CFrame::new(rbx_dom_weak::types::Vector3::new(0.0,0.0,0.0),rbx_dom_weak::types::Matrix3::identity()))),
("Size",rbx_dom_weak::types::Variant::Vector3(rbx_dom_weak::types::Vector3::new(1.0,1.0,1.0))),
("Velocity",rbx_dom_weak::types::Variant::Vector3(rbx_dom_weak::types::Vector3::new(0.0,0.0,0.0))),
("Transparency",rbx_dom_weak::types::Variant::Float32(0.0)),
("Color",rbx_dom_weak::types::Variant::Color3uint8(rbx_dom_weak::types::Color3uint8::new(255,255,255))),
("CanCollide",rbx_dom_weak::types::Variant::Bool(true)),
]);
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();
game.properties.insert("workspace".to_owned(),rbx_types::Variant::Ref(workspace));
game.properties.insert("Workspace".to_owned(),rbx_types::Variant::Ref(workspace));
}
self.dom.insert(game,InstanceBuilder::new("Lighting"));
//transfer original root instances into workspace
for instance in children{
self.dom.transfer_within(instance,workspace);
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
}
}

View File

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

View File

@@ -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);

View File

@@ -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,
))
);
}

View File

@@ -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(())
}

View File

@@ -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);

View File

@@ -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 From<rbx_types::Enum> for EnumItem<'_>{
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().unwrap();
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(i64),
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);

View File

@@ -2,49 +2,48 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref;
use rbx_dom_weak::{InstanceBuilder,WeakDom};
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
use crate::runner::vector3::Vector3;
use crate::util::static_ustr;
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());
lua.set_app_data(InstanceValueStore::default());
let instance_table=lua.create_table()?;
let table=lua.create_table()?;
//Instance.new
instance_table.raw_set("new",
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(Instance::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().unwrap();
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:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
fn get_full_name(dom:&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){
@@ -58,7 +57,7 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->S
pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
dom_mut(lua,|dom|{
let instance=script.get(dom)?;
let source=match instance.properties.get("Source"){
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"))?,
};
@@ -66,28 +65,46 @@ pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),m
})
}
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>{
pub fn find_first_child<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name)
}
pub fn find_first_descendant<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_descendant<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
dom.descendants_of(instance.referent()).find(|&inst|inst.name==name)
}
pub fn find_first_child_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_child_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.class==class)
}
pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_descendant_of_class<'a>(dom:&'a 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 WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
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 WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
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 Instance{
referent:Ref,
}
impl Instance{
pub const fn new(referent:Ref)->Self{
pub const fn new_unchecked(referent:Ref)->Self{
Self{referent}
}
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"))
}
@@ -97,40 +114,25 @@ impl Instance{
}
type_from_lua_userdata!(Instance);
//TODO: update rbx_reflection and use dom.superclasses_iter
pub struct SuperClassIter<'a> {
database: &'a rbx_reflection::ReflectionDatabase<'a>,
descriptor: Option<&'a rbx_reflection::ClassDescriptor<'a>>,
}
impl<'a> SuperClassIter<'a> {
fn next_descriptor(&self) -> Option<&'a rbx_reflection::ClassDescriptor<'a>> {
let superclass = self.descriptor?.superclass.as_ref()?;
self.database.classes.get(superclass)
}
}
impl<'a> Iterator for SuperClassIter<'a> {
type Item = &'a rbx_reflection::ClassDescriptor<'a>;
fn next(&mut self) -> Option<Self::Item> {
let next_descriptor = self.next_descriptor();
std::mem::replace(&mut self.descriptor, next_descriptor)
}
}
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)?;
Ok(Instance::new(instance.parent()))
})
});
fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{
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)?;
@@ -148,22 +150,38 @@ impl mlua::UserData for Instance{
fields.add_field_method_get("ClassName",|lua,this|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(instance.class.clone())
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_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(Instance::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:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
let name_str=&*name.to_str()?;
@@ -175,7 +193,7 @@ impl mlua::UserData for Instance{
false=>find_first_child(dom,instance,name_str),
}
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
)
})
@@ -185,13 +203,29 @@ impl mlua::UserData for Instance{
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|
Instance::new(instance.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())
)
)
})
@@ -201,24 +235,42 @@ impl mlua::UserData for Instance{
let children:Vec<_>=dom
.descendants_of(this.referent)
.map(|instance|
Instance::new(instance.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=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("IsDescendantOf",|lua,this,ancestor:Instance|
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
})
);
methods.add_method("Destroy",|lua,this,()|
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
dom_mut(lua,|dom|{
dom.destroy(this.referent);
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_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)?;
@@ -230,41 +282,40 @@ impl mlua::UserData for Instance{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
//Find existing property
match instance.properties.get(index_str)
.cloned()
// Find 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(||{
SuperClassIter{
database:db,
descriptor:Some(class),
}
.find_map(|class|
find_virtual_property(&instance.properties,class,index_str)
)
})
.or_else(||db.superclasses_iter(class).find_map(|class|
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 Instance::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.name))),
}
//find a function with a matching name
if let Some(function)=class_methods_store_mut(lua,|cf|{
let mut iter=SuperClassIter{
database:db,
descriptor:Some(class),
};
iter.find_map(|class|{
db.superclasses_iter(class).find_map(|class|{
let mut class_methods=cf.get_or_create_class_methods(&class.name)?;
class_methods.get_or_create_function(lua,index_str)
.transpose()
@@ -285,69 +336,106 @@ impl mlua::UserData for Instance{
}
//find a child with a matching name
find_first_child(dom,instance,index_str)
.map(|instance|Instance::new(instance.referent()))
.map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua)
})
});
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)?;
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
let index_str=&*index.to_str()?;
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let mut iter=SuperClassIter{
database:db,
descriptor:Some(class),
};
let property=iter.find_map(|cls|cls.properties.get(index_str)).ok_or_else(||mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)))?;
match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Vector3(typed_value.into()));
},
let property=db.superclasses_iter(class).find_map(|cls|
cls.properties.get(index_str)
).ok_or_else(||
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
)?;
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_str.to_owned(),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_str.to_owned(),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_str.to_owned(),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_str.to_owned(),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_str.to_owned(),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_str.to_owned(),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_str.to_owned(),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_string().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_str()?.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(())
})
});
@@ -372,28 +460,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.referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(Instance::new(referent))
},
other=>Err::<Instance,_>(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,_,_:(crate::runner::vector3::Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,crate::runner::vector3::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,
))
}),
},
};
@@ -409,7 +522,7 @@ struct ClassMethodsStore{
}
impl ClassMethodsStore{
/// return self.classes[class] or create the ClassMethods and then return it
fn get_or_create_class_methods(&mut self,class:&str)->Option<ClassMethods>{
fn get_or_create_class_methods(&mut self,class:&str)->Option<ClassMethods<'_>>{
// Use get_entry to get the &'static str keys of the database
// and use it as a key for the classes hashmap
CLASS_FUNCTION_DATABASE.get_entry(class)
@@ -480,16 +593,16 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
};
fn find_virtual_property(
properties:&HashMap<String,rbx_types::Variant>,
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor,
index:&str
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(virtual_property.property)?;
let variant=properties.get(&static_ustr(virtual_property.property))?;
//Transform Source property with provided function
(virtual_property.pointer)(variant)
@@ -504,11 +617,30 @@ type LUD=phf::Map<&'static str,// Class 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!{
"RenderStepped"=>|lua|{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
},
"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,
},
};
#[derive(Default)]
@@ -524,7 +656,7 @@ pub struct InstanceValues<'a>{
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
}
impl InstanceValueStore{
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues>{
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues<'_>>{
LAZY_USER_DATA.get(instance.class.as_str())
.map(|named_values|
InstanceValues{

View File

@@ -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>{

View File

@@ -3,10 +3,19 @@ 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;
mod number_range;
mod script_signal;
mod color_sequence;
mod number_sequence;

View File

@@ -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(i64),
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))),
}
}
}

View File

@@ -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);

View File

@@ -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))),
}
}
}

View File

@@ -12,14 +12,12 @@ pub enum Error{
error:mlua::Error
},
RustLua(mlua::Error),
NoServices,
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
match self{
Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"),
Self::RustLua(error)=>write!(f,"rust-side lua error: {error}"),
other=>write!(f,"{other:?}"),
}
}
}
@@ -31,20 +29,30 @@ 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)?;
Ok(())
}
unsafe fn extend_lifetime_mut<'a,T>(src:&mut T)->&'a mut T{
let ptr:*mut T=src;
unsafe{&mut*ptr}
}
impl Runner{
pub fn new()->Result<Self,Error>{
let runner=Self{
@@ -54,22 +62,20 @@ 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::Instance::new(services.game)).map_err(Error::RustLua)?;
globals.set("workspace",super::instance::Instance::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 rbx_dom_weak::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.
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{extend_lifetime_mut(&mut context.dom)});
#[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
})
}
}
@@ -77,11 +83,11 @@ 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 rbx_dom_weak::WeakDom>();
self.lua.remove_app_data::<crate::context::LuaAppData>();
#[cfg(feature="run-service")]
self.lua.remove_app_data::<crate::scheduler::Scheduler>();
Runner{

View File

@@ -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))
@@ -120,7 +117,7 @@ impl mlua::FromLua for ScriptSignal{
}
impl mlua::UserData for ScriptConnection{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fn add_fields<F:UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Connected",|_,this|{
Ok(this.position().is_some())
});
@@ -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);
})?;

View File

@@ -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(())
}

View File

@@ -0,0 +1,35 @@
use super::instance::Instance;
use super::tween_info::TweenInfo;
#[expect(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);

View File

@@ -0,0 +1,45 @@
use super::number::Number;
use super::r#enum::{CoerceEnum,Enums};
#[expect(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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
))
);
}

View File

@@ -46,12 +46,12 @@ impl Scheduler{
}
}
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::Scheduler)->mlua::Result<T>)->mlua::Result<T>{
let mut scheduler=lua.app_data_mut::<crate::scheduler::Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut Scheduler)->mlua::Result<T>)->mlua::Result<T>{
let mut scheduler=lua.app_data_mut::<Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
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(())
}

View File

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

View File

@@ -1,11 +1,14 @@
[package]
name = "strafesnet_snf"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
binrw = "0.14.0"
binrw = "0.15.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -6,7 +6,7 @@ use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::Time>;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,Time>;
#[derive(Debug)]
pub enum Error{
@@ -85,6 +85,7 @@ pub struct Segment{
#[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{
/// time of the first instruction in this segment.
#[expect(dead_code)]
time:Time,
instruction_count:u32,
/// How many total instructions in segments up to and including this segment
@@ -116,6 +117,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
segment_map,
})
}
#[expect(dead_code)]
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
}
@@ -272,7 +274,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:i
//probe header length
let mut bot_header_data=Vec::new();
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
header.write_le(&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
// the first block location is the map header
block_location.push(offset);

View File

@@ -53,8 +53,6 @@ pub(crate) enum FourCC{
Map,
#[brw(magic=b"SNFB")]
Bot,
#[brw(magic=b"SNFD")]
Demo,
}
#[binrw]
#[brw(little)]

View File

@@ -5,7 +5,6 @@ mod newtypes;
mod file;
pub mod map;
pub mod bot;
pub mod demo;
#[derive(Debug)]
pub enum Error{
@@ -13,7 +12,6 @@ pub enum Error{
Header(file::Error),
Map(map::Error),
Bot(bot::Error),
Demo(demo::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -25,7 +23,6 @@ impl std::error::Error for Error{}
pub enum SNF<R:BinReaderExt>{
Map(map::StreamableMap<R>),
Bot(bot::StreamableBot<R>),
Demo(demo::StreamableDemo<R>),
}
pub fn read_snf<R:BinReaderExt>(input:R)->Result<SNF<R>,Error>{
@@ -33,7 +30,6 @@ pub fn read_snf<R:BinReaderExt>(input:R)->Result<SNF<R>,Error>{
Ok(match file.fourcc(){
file::FourCC::Map=>SNF::Map(map::StreamableMap::new(file).map_err(Error::Map)?),
file::FourCC::Bot=>SNF::Bot(bot::StreamableBot::new(file).map_err(Error::Bot)?),
file::FourCC::Demo=>SNF::Demo(demo::StreamableDemo::new(file).map_err(Error::Demo)?),
})
}
pub fn read_map<R:BinReaderExt>(input:R)->Result<map::StreamableMap<R>,Error>{
@@ -50,13 +46,6 @@ pub fn read_bot<R:BinReaderExt>(input:R)->Result<bot::StreamableBot<R>,Error>{
_=>Err(Error::UnexpectedFourCC)
}
}
pub fn read_demo<R:BinReaderExt>(input:R)->Result<demo::StreamableDemo<R>,Error>{
let file=file::File::new(input).map_err(Error::Header)?;
match file.fourcc(){
file::FourCC::Demo=>Ok(demo::StreamableDemo::new(file).map_err(Error::Demo)?),
_=>Err(Error::UnexpectedFourCC)
}
}
#[cfg(test)]
mod tests {

View File

@@ -97,8 +97,8 @@ enum ResourceType{
}
struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>,
textures:HashMap<strafesnet_common::model::TextureId,T>,
meshes:HashMap<model::MeshId,T>,
textures:HashMap<model::TextureId,T>,
}
impl<T> Default for ResourceMap<T>{
fn default()->Self{
@@ -138,7 +138,7 @@ struct MapHeader{
//#[br(count=num_resources_external)]
//external_resources:Vec<ResourceExternalHeader>,
#[br(count=num_modes)]
modes:Vec<newtypes::gameplay_modes::Mode>,
modes:Vec<newtypes::gameplay_modes::NormalizedMode>,
#[br(count=num_attributes)]
attributes:Vec<newtypes::gameplay_attributes::CollisionAttributes>,
#[br(count=num_render_configs)]
@@ -181,11 +181,11 @@ fn read_texture<R:BinReaderExt>(file:&mut crate::file::File<R>,block_id:BlockId)
pub struct StreamableMap<R:BinReaderExt>{
file:crate::file::File<R>,
//this includes every platform... move the unconstrained datas to their appropriate data block?
modes:gameplay_modes::Modes,
modes:gameplay_modes::NormalizedModes,
//this is every possible attribute... need some sort of streaming system
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
//this is every possible render configuration... shaders and such... need streaming
render_configs:Vec<strafesnet_common::model::RenderConfig>,
render_configs:Vec<model::RenderConfig>,
//this makes sense to keep in memory for streaming, a map of which blocks occupy what space
bvh:BvhNode<BlockId>,
//something something resources hashmaps
@@ -223,7 +223,7 @@ impl<R:BinReaderExt> StreamableMap<R>{
}
Ok(Self{
file,
modes:strafesnet_common::gameplay_modes::Modes::new(modes),
modes:gameplay_modes::NormalizedModes::new(modes),
attributes,
render_configs,
bvh:strafesnet_common::bvh::generate_bvh(bvh),
@@ -366,12 +366,12 @@ fn collect_spacial_blocks(
block_location.push(sequential_block_data.position());
}else{
match bvh_node.into_content(){
strafesnet_common::bvh::RecursiveContent::Branch(bvh_node_list)=>{
RecursiveContent::Branch(bvh_node_list)=>{
for bvh_node in bvh_node_list{
collect_spacial_blocks(block_location,block_headers,sequential_block_data,bvh_node)?;
}
},
strafesnet_common::bvh::RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
}
}
Ok(())
@@ -384,13 +384,13 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
let boxen=map.models.into_iter().enumerate().map(|(model_id,model)|{
//grow your own aabb
let mesh=map.meshes.get(model.mesh.get() as usize).ok_or(Error::InvalidMeshId(model.mesh))?;
let mut aabb=strafesnet_common::aabb::Aabb::default();
let mut aabb=Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(model.transform.transform_point3(pos).fix_1());
aabb.grow(model.transform.transform_point3(pos).narrow_1().unwrap());
}
Ok(((model::ModelId::new(model_id as u32),model.into()),aabb))
}).collect::<Result<Vec<_>,_>>()?;
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|std::mem::size_of::<newtypes::model::Model>());
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|size_of::<newtypes::model::Model>());
//build blocks
//block location is initialized with two values
//the first value represents the location of the first byte after the file header
@@ -430,13 +430,13 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
num_spacial_blocks:spacial_blocks.len() as u32,
num_resource_blocks:resource_blocks.len() as u32,
//num_resources_external:0,
num_modes:map.modes.modes.len() as u32,
num_modes:map.modes.len() as u32,
num_attributes:map.attributes.len() as u32,
num_render_configs:map.render_configs.len() as u32,
spacial_blocks,
resource_blocks,
//external_resources:Vec::new(),
modes:map.modes.modes.into_iter().map(Into::into).collect(),
modes:map.modes.into_iter().map(Into::into).collect(),
attributes:map.attributes.into_iter().map(Into::into).collect(),
render_configs:map.render_configs.into_iter().map(Into::into).collect(),
};

View File

@@ -157,7 +157,7 @@ pub struct ModeHeader{
}
#[binrw::binrw]
#[brw(little)]
pub struct Mode{
pub struct NormalizedMode{
pub header:ModeHeader,
pub style:super::gameplay_style::StyleModifiers,
pub start:u32,
@@ -179,10 +179,10 @@ impl std::fmt::Display for ModeError{
}
}
impl std::error::Error for ModeError{}
impl TryInto<strafesnet_common::gameplay_modes::Mode> for Mode{
impl TryInto<strafesnet_common::gameplay_modes::NormalizedMode> for NormalizedMode{
type Error=ModeError;
fn try_into(self)->Result<strafesnet_common::gameplay_modes::Mode,Self::Error>{
Ok(strafesnet_common::gameplay_modes::Mode::new(
fn try_into(self)->Result<strafesnet_common::gameplay_modes::NormalizedMode,Self::Error>{
Ok(strafesnet_common::gameplay_modes::NormalizedMode::new(strafesnet_common::gameplay_modes::Mode::new(
self.style.try_into().map_err(ModeError::StyleModifier)?,
strafesnet_common::model::ModelId::new(self.start),
self.zones.into_iter().map(|(model_id,zone)|
@@ -192,12 +192,12 @@ impl TryInto<strafesnet_common::gameplay_modes::Mode> for Mode{
self.elements.into_iter().map(|(model_id,stage_element)|
Ok((strafesnet_common::model::ModelId::new(model_id),stage_element.try_into()?))
).collect::<Result<_,_>>().map_err(ModeError::StageElement)?,
))
)))
}
}
impl From<strafesnet_common::gameplay_modes::Mode> for Mode{
fn from(value:strafesnet_common::gameplay_modes::Mode)->Self{
let (style,start,zones,stages,elements)=value.into_inner();
impl From<strafesnet_common::gameplay_modes::NormalizedMode> for NormalizedMode{
fn from(value:strafesnet_common::gameplay_modes::NormalizedMode)->Self{
let (style,start,zones,stages,elements)=value.into_inner().into_inner();
Self{
header:ModeHeader{
zones:zones.len() as u32,

View File

@@ -104,6 +104,7 @@ impl From<strafesnet_common::gameplay_style::StyleModifiers> for StyleModifiers{
#[binrw::binrw]
#[brw(little,repr=u8)]
#[expect(dead_code)]
pub enum JumpCalculation{
Max,
BoostThenJump,
@@ -128,6 +129,7 @@ impl From<strafesnet_common::gameplay_style::JumpCalculation> for JumpCalculatio
}
}
#[expect(dead_code)]
pub enum JumpImpulse{
Time(Time),
Height(Planar64),

Some files were not shown because too many files have changed in this diff Show More