Compare commits

..

301 Commits

Author SHA1 Message Date
32f8f9caed fix lints 2025-12-18 11:03:30 -08:00
697958080c fix rebase 2025-12-18 11:03:29 -08:00
6896800c75 debug physics models visually 2025-12-18 11:03:29 -08:00
72a23f784a Session::debug_raycast_print_model_id_if_changed 2025-12-18 11:03:06 -08:00
78b9a4fdce skip faces early 2025-12-18 11:03:06 -08:00
bd6bb4b27a physics: PhysicsData::trace_ray 2025-12-18 11:03:06 -08:00
9b3a9ce437 narrow directions more carefully 2025-12-18 11:03:06 -08:00
73f0a85f81 fix predict_collision_out 2025-12-18 11:03:06 -08:00
332e11a627 add loop 2025-12-18 11:03:06 -08:00
2d7f5cdc7d add test points 2025-12-18 11:03:06 -08:00
822f4571d2 add unit test 2025-12-18 11:03:06 -08:00
ac4c84f562 partially implement md generic 2025-12-18 11:03:06 -08:00
3a7aaa20f8 fix constraints epsilon
these were supposed to be 3 voxels but were on the order of 3 units
2025-12-18 11:03:06 -08:00
2e77366a17 handle non-canonnical multi-edge spanning edges 2025-12-18 11:03:06 -08:00
8a1f434a2a Meshquery::farthest_vert 2025-12-18 11:03:06 -08:00
80d3df4659 todo 2025-12-18 11:03:06 -08:00
47f94fe359 why 2025-12-18 11:03:06 -08:00
c4a2811627 think through simplex constraints 2025-12-18 11:03:06 -08:00
b23d4d590d refactor algorithm to use a struct 2025-12-18 11:03:06 -08:00
cb9307bdf2 comments 2025-12-18 11:03:05 -08:00
ae7582704b comment todos 2025-12-18 11:03:05 -08:00
7f0f63570e add unfortunate algorithm 2025-12-18 11:03:05 -08:00
dbe96a0451 remove Eq for MinkowskiFace 2025-12-18 11:03:05 -08:00
291cedff3f breakout 2025-12-18 11:03:05 -08:00
43a2c76906 change on_exact signature 2025-12-18 11:03:05 -08:00
7d8dbf7e82 fail without crash 2025-12-18 11:03:05 -08:00
7640ea824d no print 2025-12-18 11:03:05 -08:00
f3b02bba92 use new algorithm 2025-12-18 11:03:05 -08:00
9d04df4894 convert to fev using dumbest algorithm possible 2025-12-18 11:03:05 -08:00
f90436f0cc negate minkowski input to minimum_difference 2025-12-18 11:03:05 -08:00
5e14a85d84 make hint_point consistent with vert 2025-12-18 11:03:05 -08:00
da718e4bd2 rename variable 2025-12-18 11:03:05 -08:00
5641e9a26f physics: derive Eq for Minkowski FEV 2025-12-18 11:03:05 -08:00
7e7839f4aa remove indexing 2025-12-18 11:03:05 -08:00
6448d7cc57 put comment back in 2025-12-18 11:03:05 -08:00
b8be169092 use struct 2025-12-18 11:03:05 -08:00
9c4c14c5dc reduce member fn 2025-12-18 11:03:05 -08:00
ca40e65060 split reduce 2025-12-18 11:03:05 -08:00
492e72c1bc deconstruct 2025-12-18 11:03:05 -08:00
03bf2650fd refactor using Simplex enum 2025-12-18 11:03:05 -08:00
0c8cf02287 simplify perp 2025-12-18 11:03:05 -08:00
af1374906b fix algorithm 2025-12-18 11:03:05 -08:00
570d33a030 rename 2025-12-18 11:03:05 -08:00
d0c38a6e66 style 2025-12-18 11:03:05 -08:00
dd7a636fa9 careful relative point opti 2025-12-18 11:03:05 -08:00
2483abe2ad work 2025-12-18 11:03:05 -08:00
6621e369f2 include relative point 2025-12-18 11:03:05 -08:00
1b833ef6b3 fix degenerate case 2025-12-18 11:03:05 -08:00
8a1ab4e03c fix wrong 2025-12-18 11:03:05 -08:00
069db75d3a use min diff 2025-12-18 11:03:05 -08:00
de54bcfc36 implement contains_point 2025-12-18 11:03:05 -08:00
e777b89c6d rename variable 2025-12-18 11:03:05 -08:00
d17153d17d work 2025-12-18 11:03:05 -08:00
e514c27675 reduce min dist bit width 2025-12-18 11:03:05 -08:00
d1c13757e0 FnOnce 2025-12-18 11:03:04 -08:00
d93e558678 fast fail fn 2025-12-18 11:03:04 -08:00
0048306236 more naming things 2025-12-18 11:03:04 -08:00
ee50f8dc1e refine naming 2025-12-18 11:03:04 -08:00
e4966b037f names 2025-12-18 11:03:04 -08:00
96cdd684d1 stuff 2025-12-18 11:03:04 -08:00
838130fec4 switch trait to closures 2025-12-18 11:03:04 -08:00
8a3badc270 insane trait just to remove if statements 2025-12-18 11:03:04 -08:00
4470e88d7b refactor calculation result 2025-12-18 11:03:04 -08:00
df7bee6cd1 details later 2025-12-18 11:03:04 -08:00
4fdd254f2a remove is more clear 2025-12-18 11:03:04 -08:00
8a9db203fa remove unused 2025-12-18 11:03:04 -08:00
be05fd108a work 2025-12-18 11:03:04 -08:00
6160872469 work 2025-12-18 11:03:04 -08:00
2c1fa5da22 work 2025-12-18 11:03:04 -08:00
6fe45f4873 work 2025-12-18 11:03:04 -08:00
e1dac67aa0 notes 2025-12-18 11:03:04 -08:00
f9ed33073e zero 2025-12-18 11:03:04 -08:00
91636747d4 idea 2025-12-18 11:03:04 -08:00
03b72301a3 eugh 2025-12-18 11:03:04 -08:00
214b23f780 work 2025-12-18 11:03:04 -08:00
6d98407830 work 2025-12-18 11:03:04 -08:00
978659e8c6 work 2025-12-18 11:03:04 -08:00
d00871f87f work 2025-12-18 11:03:04 -08:00
d2ed97fcf2 wip paste fns 2025-12-18 11:03:04 -08:00
ab3c693f84 no hold ref 2025-12-18 11:03:04 -08:00
f0c7677a77 work 2025-12-18 11:03:04 -08:00
eed0abcc2c work 2025-12-18 11:03:04 -08:00
bc5cdc7313 work 2025-12-18 11:03:04 -08:00
14a5a3f964 work 2025-12-18 11:03:04 -08:00
18b7bba901 work 2025-12-18 11:03:04 -08:00
1770ac7292 work 2025-12-18 11:03:04 -08:00
7384886512 work 2025-12-18 11:03:04 -08:00
1dbde609cc wip 2025-12-18 11:03:04 -08:00
e024f37843 update deps 2025-12-18 10:58:50 -08:00
4059cfa527 update wgpu 2025-12-18 10:57:19 -08:00
e4f3418bc6 document PhysicsMesh 2025-12-11 09:36:21 -08:00
6ca6d5e484 expect dead code 2025-12-10 18:05:16 -08:00
0668ac2def use allow instead of expect 2025-12-09 14:39:42 -08:00
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
101 changed files with 5876 additions and 2830 deletions

1
.gitignore vendored
View File

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

2243
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,13 @@ resolver = "2"
strip = true strip = true
codegen-units = 1 codegen-units = 1
[profile.dev] [workspace.lints.rust]
strip = false # unsafe_code = "forbid"
opt-level = 3 # 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_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" } strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" } strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "25.0.0" wgpu = "28.0.0"
[lints]
workspace = true

View File

@@ -5,7 +5,7 @@ use strafesnet_settings::settings;
use strafesnet_session::session; use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId}; use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel}; use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex}; use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex,DebugGraphicsVertex};
pub fn required_limits()->wgpu::Limits{ pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default() wgpu::Limits::default()
@@ -36,12 +36,22 @@ struct GraphicsModel{
instance_count:u32, instance_count:u32,
} }
struct DebugGraphicsMesh{
indices:Indices,
vertex_buf:wgpu::Buffer,
}
struct DebugGraphicsModel{
debug_mesh_id:u32,
bind_group:wgpu::BindGroup,
}
struct GraphicsSamplers{ struct GraphicsSamplers{
repeat:wgpu::Sampler, repeat:wgpu::Sampler,
} }
struct GraphicsBindGroupLayouts{ struct GraphicsBindGroupLayouts{
model:wgpu::BindGroupLayout, model:wgpu::BindGroupLayout,
debug_model:wgpu::BindGroupLayout,
} }
struct GraphicsBindGroups{ struct GraphicsBindGroups{
@@ -52,6 +62,7 @@ struct GraphicsBindGroups{
struct GraphicsPipelines{ struct GraphicsPipelines{
skybox:wgpu::RenderPipeline, skybox:wgpu::RenderPipeline,
model:wgpu::RenderPipeline, model:wgpu::RenderPipeline,
debug:wgpu::RenderPipeline,
} }
struct GraphicsCamera{ struct GraphicsCamera{
@@ -94,7 +105,7 @@ impl GraphicsCamera{
raw raw
} }
} }
impl std::default::Default for GraphicsCamera{ impl Default for GraphicsCamera{
fn default()->Self{ fn default()->Self{
Self{ Self{
screen_size:glam::UVec2::ONE, screen_size:glam::UVec2::ONE,
@@ -132,6 +143,8 @@ pub struct GraphicsState{
camera_buf:wgpu::Buffer, camera_buf:wgpu::Buffer,
temp_squid_texture_view:wgpu::TextureView, temp_squid_texture_view:wgpu::TextureView,
models:Vec<GraphicsModel>, models:Vec<GraphicsModel>,
debug_meshes:Vec<DebugGraphicsMesh>,
debug_models:Vec<DebugGraphicsModel>,
depth_view:wgpu::TextureView, depth_view:wgpu::TextureView,
staging_belt:wgpu::util::StagingBelt, staging_belt:wgpu::util::StagingBelt,
} }
@@ -166,8 +179,78 @@ impl GraphicsState{
self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2(); self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
} }
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){ pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
//generate debug meshes, each debug model refers to one
self.debug_meshes=map.meshes.iter().map(|mesh|{
let vertices:Vec<DebugGraphicsVertex>=mesh.unique_vertices.iter().map(|vertex|{
DebugGraphicsVertex{
pos:mesh.unique_pos[vertex.pos.get() as usize].to_array().map(Into::into),
normal:mesh.unique_normal[vertex.normal.get() as usize].to_array().map(Into::into),
}
}).collect();
let vertex_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
label:Some("Vertex"),
contents:bytemuck::cast_slice(&vertices),
usage:wgpu::BufferUsages::VERTEX,
});
let mut indices=Vec::new();
for physics_group in &mesh.physics_groups{
for polygon_group_id in &physics_group.groups{
for poly in mesh.polygon_groups[polygon_group_id.get() as usize].polys(){
// triangulate
let mut poly_vertices=poly.into_iter().copied();
if let (Some(a),Some(mut b))=(poly_vertices.next(),poly_vertices.next()){
for c in poly_vertices{
indices.extend([a,b,c]);
b=c;
}
}
}
}
}
DebugGraphicsMesh{
indices:if (u32::MAX as usize)<vertices.len(){
panic!("Model has too many vertices!")
}else if (u16::MAX as usize)<vertices.len(){
Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u32).collect(),wgpu::IndexFormat::Uint32)
}else{
Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u16).collect(),wgpu::IndexFormat::Uint16)
},
vertex_buf,
}
}).collect();
//generate debug models, only one will be rendered at a time
self.debug_models=map.models.iter().enumerate().map(|(model_id,model)|{
let model_uniforms=get_instances_buffer_data(&[GraphicsModelOwned{
transform:model.transform.into(),
normal_transform:glam::Mat3::from_cols_array_2d(&model.transform.matrix3.to_array().map(|row|row.map(Into::into))).inverse().transpose(),
color:GraphicsModelColor4::new(glam::vec4(1.0,0.0,0.0,0.2)),
}]);
let model_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
label:Some(format!("Debug Model{} Buf",model_id).as_str()),
contents:bytemuck::cast_slice(&model_uniforms),
usage:wgpu::BufferUsages::UNIFORM|wgpu::BufferUsages::COPY_DST,
});
let bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
layout:&self.bind_group_layouts.debug_model,
entries:&[
wgpu::BindGroupEntry{
binding:0,
resource:model_buf.as_entire_binding(),
},
],
label:Some(format!("Debug Model{} Bind Group",model_id).as_str()),
});
DebugGraphicsModel{
debug_mesh_id:model.mesh.get(),
bind_group,
}
}).collect();
//generate texture view per texture //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 texture_id=model::TextureId::new(texture_id as u32);
let image=match ddsfile::Dds::read(std::io::Cursor::new(texture_data)){ let image=match ddsfile::Dds::read(std::io::Cursor::new(texture_data)){
Ok(image)=>image, Ok(image)=>image,
@@ -608,6 +691,21 @@ impl GraphicsState{
}, },
], ],
}); });
let debug_model_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
label:Some("Debug Model Bind Group Layout"),
entries:&[
wgpu::BindGroupLayoutEntry{
binding:0,
visibility:wgpu::ShaderStages::VERTEX_FRAGMENT,
ty:wgpu::BindingType::Buffer{
ty:wgpu::BufferBindingType::Uniform,
has_dynamic_offset:false,
min_binding_size:None,
},
count:None,
},
],
});
let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{ let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
label:Some("Clamp Sampler"), label:Some("Clamp Sampler"),
@@ -616,7 +714,7 @@ impl GraphicsState{
address_mode_w:wgpu::AddressMode::ClampToEdge, address_mode_w:wgpu::AddressMode::ClampToEdge,
mag_filter:wgpu::FilterMode::Linear, mag_filter:wgpu::FilterMode::Linear,
min_filter:wgpu::FilterMode::Linear, min_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::FilterMode::Linear, mipmap_filter:wgpu::MipmapFilterMode::Linear,
..Default::default() ..Default::default()
}); });
let repeat_sampler=device.create_sampler(&wgpu::SamplerDescriptor{ let repeat_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
@@ -626,7 +724,7 @@ impl GraphicsState{
address_mode_w:wgpu::AddressMode::Repeat, address_mode_w:wgpu::AddressMode::Repeat,
mag_filter:wgpu::FilterMode::Linear, mag_filter:wgpu::FilterMode::Linear,
min_filter:wgpu::FilterMode::Linear, min_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::FilterMode::Linear, mipmap_filter:wgpu::MipmapFilterMode::Linear,
anisotropy_clamp:16, anisotropy_clamp:16,
..Default::default() ..Default::default()
}); });
@@ -754,15 +852,23 @@ impl GraphicsState{
&skybox_texture_bind_group_layout, &skybox_texture_bind_group_layout,
&model_bind_group_layout, &model_bind_group_layout,
], ],
push_constant_ranges:&[], immediate_size:0,
}); });
let debug_model_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
label:None,
bind_group_layouts:&[
&camera_bind_group_layout,
&debug_model_bind_group_layout,
],
push_constant_ranges:&[],
});
let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{ let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
label:None, label:None,
bind_group_layouts:&[ bind_group_layouts:&[
&camera_bind_group_layout, &camera_bind_group_layout,
&skybox_texture_bind_group_layout, &skybox_texture_bind_group_layout,
], ],
push_constant_ranges:&[], immediate_size:0,
}); });
// Create the render pipelines // Create the render pipelines
@@ -793,7 +899,7 @@ impl GraphicsState{
bias:wgpu::DepthBiasState::default(), bias:wgpu::DepthBiasState::default(),
}), }),
multisample:wgpu::MultisampleState::default(), multisample:wgpu::MultisampleState::default(),
multiview:None, multiview_mask:None,
cache:None, cache:None,
}); });
let model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{ let model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
@@ -803,7 +909,7 @@ impl GraphicsState{
module:&shader, module:&shader,
entry_point:Some("vs_entity_texture"), entry_point:Some("vs_entity_texture"),
buffers:&[wgpu::VertexBufferLayout{ 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, step_mode:wgpu::VertexStepMode::Vertex,
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4], attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4],
}], }],
@@ -828,6 +934,45 @@ impl GraphicsState{
bias:wgpu::DepthBiasState::default(), bias:wgpu::DepthBiasState::default(),
}), }),
multisample:wgpu::MultisampleState::default(), multisample:wgpu::MultisampleState::default(),
multiview_mask:None,
cache:None,
});
let debug_model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
label:Some("Debug Model Pipeline"),
layout:Some(&debug_model_pipeline_layout),
vertex:wgpu::VertexState{
module:&shader,
entry_point:Some("vs_debug"),
buffers:&[wgpu::VertexBufferLayout{
array_stride:size_of::<DebugGraphicsVertex>() as wgpu::BufferAddress,
step_mode:wgpu::VertexStepMode::Vertex,
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x3],
}],
compilation_options:wgpu::PipelineCompilationOptions::default(),
},
fragment:Some(wgpu::FragmentState{
module:&shader,
entry_point:Some("fs_debug"),
targets:&[Some(wgpu::ColorTargetState{
format:config.view_formats[0],
blend:Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask:wgpu::ColorWrites::default(),
})],
compilation_options:wgpu::PipelineCompilationOptions::default(),
}),
primitive:wgpu::PrimitiveState{
front_face:wgpu::FrontFace::Cw,
cull_mode:Some(wgpu::Face::Front),
..Default::default()
},
depth_stencil:Some(wgpu::DepthStencilState{
format:Self::DEPTH_FORMAT,
depth_write_enabled:true,
depth_compare:wgpu::CompareFunction::Always,
stencil:wgpu::StencilState::default(),
bias:wgpu::DepthBiasState::default(),
}),
multisample:wgpu::MultisampleState::default(),
multiview:None, multiview:None,
cache:None, cache:None,
}); });
@@ -870,7 +1015,8 @@ impl GraphicsState{
Self{ Self{
pipelines:GraphicsPipelines{ pipelines:GraphicsPipelines{
skybox:sky_pipeline, skybox:sky_pipeline,
model:model_pipeline model:model_pipeline,
debug:debug_model_pipeline,
}, },
bind_groups:GraphicsBindGroups{ bind_groups:GraphicsBindGroups{
camera:camera_bind_group, camera:camera_bind_group,
@@ -879,9 +1025,14 @@ impl GraphicsState{
camera, camera,
camera_buf, camera_buf,
models:Vec::new(), models:Vec::new(),
debug_meshes:Vec::new(),
debug_models:Vec::new(),
depth_view, depth_view,
staging_belt:wgpu::util::StagingBelt::new(0x100), staging_belt:wgpu::util::StagingBelt::new(device.clone(),0x100),
bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout}, bind_group_layouts:GraphicsBindGroupLayouts{
model:model_bind_group_layout,
debug_model:debug_model_bind_group_layout,
},
samplers:GraphicsSamplers{repeat:repeat_sampler}, samplers:GraphicsSamplers{repeat:repeat_sampler},
temp_squid_texture_view:squid_texture_view, temp_squid_texture_view:squid_texture_view,
} }
@@ -918,7 +1069,6 @@ impl GraphicsState{
&self.camera_buf, &self.camera_buf,
0, 0,
wgpu::BufferSize::new((camera_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(), wgpu::BufferSize::new((camera_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(),
device,
) )
.copy_from_slice(bytemuck::cast_slice(&camera_uniforms)); .copy_from_slice(bytemuck::cast_slice(&camera_uniforms));
//This code only needs to run when the uniforms change //This code only needs to run when the uniforms change
@@ -953,6 +1103,7 @@ impl GraphicsState{
}), }),
store:wgpu::StoreOp::Store, store:wgpu::StoreOp::Store,
}, },
depth_slice:None,
})], })],
depth_stencil_attachment:Some(wgpu::RenderPassDepthStencilAttachment{ depth_stencil_attachment:Some(wgpu::RenderPassDepthStencilAttachment{
view:&self.depth_view, view:&self.depth_view,
@@ -964,11 +1115,13 @@ impl GraphicsState{
}), }),
timestamp_writes:Default::default(), timestamp_writes:Default::default(),
occlusion_query_set:Default::default(), occlusion_query_set:Default::default(),
multiview_mask:None,
}); });
rpass.set_bind_group(0,&self.bind_groups.camera,&[]); rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
rpass.set_bind_group(1,&self.bind_groups.skybox_texture,&[]); rpass.set_bind_group(1,&self.bind_groups.skybox_texture,&[]);
// Draw all models.
rpass.set_pipeline(&self.pipelines.model); rpass.set_pipeline(&self.pipelines.model);
for model in &self.models{ for model in &self.models{
rpass.set_bind_group(2,&model.bind_group,&[]); rpass.set_bind_group(2,&model.bind_group,&[]);
@@ -980,6 +1133,19 @@ impl GraphicsState{
rpass.set_pipeline(&self.pipelines.skybox); rpass.set_pipeline(&self.pipelines.skybox);
rpass.draw(0..3,0..1); rpass.draw(0..3,0..1);
// render a single debug_model in red
if let Some(model_id)=frame_state.debug_model{
if let Some(model)=self.debug_models.get(model_id.get() as usize){
let mesh=&self.debug_meshes[model.debug_mesh_id as usize];
rpass.set_pipeline(&self.pipelines.debug);
rpass.set_bind_group(1,&model.bind_group,&[]);
rpass.set_vertex_buffer(0,mesh.vertex_buf.slice(..));
rpass.set_index_buffer(mesh.indices.buf.slice(..),mesh.indices.format);
//TODO: loop over triangle strips
rpass.draw_indexed(0..mesh.indices.count,0,0..1);
}
}
} }
queue.submit(std::iter::once(encoder.finish())); queue.submit(std::iter::once(encoder.finish()));

View File

@@ -8,6 +8,12 @@ pub struct GraphicsVertex{
pub normal:[f32;3], pub normal:[f32;3],
pub color:[f32;4], pub color:[f32;4],
} }
#[derive(Clone,Copy,Pod,Zeroable)]
#[repr(C)]
pub struct DebugGraphicsVertex{
pub pos:[f32;3],
pub normal:[f32;3],
}
#[derive(Clone,Copy,id::Id)] #[derive(Clone,Copy,id::Id)]
pub struct IndexedGraphicsMeshOwnedRenderConfigId(u32); pub struct IndexedGraphicsMeshOwnedRenderConfigId(u32);
pub struct IndexedGraphicsMeshOwnedRenderConfig{ pub struct IndexedGraphicsMeshOwnedRenderConfig{

View File

@@ -8,3 +8,6 @@ arrayvec = "0.7.6"
glam = "0.30.0" glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" } id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", 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> impl<T> Body<T>
where Time<T>:Copy, 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{ pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{ Self{
position, position,
@@ -96,8 +107,8 @@ impl<T> Body<T>
self.time+=dt.into(); self.time+=dt.into();
} }
pub fn infinity_dir(&self)->Option<Planar64Vec3>{ pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{ if self.velocity==vec3::zero(){
if self.acceleration==vec3::ZERO{ if self.acceleration==vec3::zero(){
None None
}else{ }else{
Some(self.acceleration) 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 strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body}; use crate::physics::{Time,Body};
use core::ops::Bound;
enum Transition<M:MeshQuery>{ enum Transition<M:MeshQuery>{
Miss, Miss,
Next(FEV<M>,GigaTime), Next(FEV<M>,GigaTime),
@@ -19,11 +21,46 @@ impl<M:MeshQuery> CrawlResult<M>{
CrawlResult::Hit(face,time)=>Some((face,time)), CrawlResult::Hit(face,time)=>Some((face,time)),
} }
} }
pub fn miss(self)->Option<FEV<M>>{ }
match self{
CrawlResult::Miss(fev)=>Some(fev), // TODO: move predict_collision_face_out algorithm in here or something
CrawlResult::Hit(_,_)=>None,
} /// 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,
} }
} }
@@ -35,9 +72,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
M::Vert:Copy, M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>, F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum, <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. //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. //if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss; let mut best_transition=Transition::Miss;
@@ -50,8 +87,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//TODO: use higher precision d value? //TODO: use higher precision d value?
//use the mesh transform translation instead of baking it into the 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)){ 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(){ if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; upper_bound=Bound::Included(dt);
best_transition=Transition::Hit(face_id,dt); best_transition=Transition::Hit(face_id,dt);
break; break;
} }
@@ -65,8 +102,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//WARNING: precision is swept under the rug! //WARNING: precision is swept under the rug!
//wrap for speed //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()){ for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt); best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break; break;
} }
@@ -76,10 +113,11 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}, },
&FEV::Edge(edge_id)=>{ &FEV::Edge(edge_id)=>{
//test each face collision time, ignoring roots with zero or conflicting derivative //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 edge_verts=mesh.edge_verts(edge_id);
let &[ev0,ev1]=edge_verts.as_ref(); let &[ev0,ev1]=edge_verts.as_ref();
let delta_pos=body.position*2-(mesh.vert(ev0)+mesh.vert(ev1)); 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(){ 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; let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces //edge_n gets parity from the order of edge_faces
@@ -87,8 +125,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//WARNING yada yada d *2 //WARNING yada yada d *2
//wrap for speed //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()){ for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt); best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break; break;
} }
@@ -99,9 +137,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//vertex normal gets parity from vert index //vertex normal gets parity from vert index
let n=edge_n*(1-2*(i as i64)); 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)){ for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4()); let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt; upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt); best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break; break;
} }
@@ -115,9 +153,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//edge is directed away from vertex, but we want the dot product to turn out negative //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); 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)){ for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4()); let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt; upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt); best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break; break;
} }
@@ -128,19 +166,13 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
} }
best_transition best_transition
} }
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{ pub fn crawl(mut self,mesh:&M,relative_body:&Body,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
let mut body_time={ let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time));
let r=(start_time-relative_body.time).to_ratio(); let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time));
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
for _ in 0..20{ 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::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), Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
} }
} }

View File

@@ -1,7 +1,8 @@
mod body; mod body;
mod push_solve;
mod face_crawler; mod face_crawler;
mod model; mod model;
mod push_solve;
mod minimum_difference;
pub mod physics; pub mod physics;

View File

@@ -0,0 +1,913 @@
use strafesnet_common::integer::vec3;
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::integer::{Fixed,Planar64,Planar64Vec3};
use crate::model::{DirectedEdge,FEV,MeshQuery};
// TODO: remove mesh invert
use crate::model::{MinkowskiMesh,MinkowskiVert};
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
type Simplex<const N:usize,Vert>=[Vert;N];
#[derive(Clone,Copy)]
enum Simplex1_3<Vert>{
Simplex1(Simplex<1,Vert>),
Simplex2(Simplex<2,Vert>),
Simplex3(Simplex<3,Vert>),
}
impl<Vert> Simplex1_3<Vert>{
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
match self{
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
Simplex1_3::Simplex2([v0,v1])=>Simplex2_4::Simplex3([v,v0,v1]),
Simplex1_3::Simplex3([v0,v1,v2])=>Simplex2_4::Simplex4([v,v0,v1,v2]),
}
}
}
#[derive(Clone,Copy)]
enum Simplex2_4<Vert>{
Simplex2(Simplex<2,Vert>),
Simplex3(Simplex<3,Vert>),
Simplex4(Simplex<4,Vert>),
}
/*
local function absDet(r, u, v, w)
if w then
return math.abs((u - r):Cross(v - r):Dot(w - r))
elseif v then
return (u - r):Cross(v - r).magnitude
elseif u then
return (u - r).magnitude
else
return 1
end
end
*/
impl<Vert> Simplex2_4<Vert>{
fn det_is_zero<M:MeshQuery<Vert=Vert>>(self,mesh:&M)->bool{
match self{
Self::Simplex4([p0,p1,p2,p3])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
let p2=mesh.vert(p2);
let p3=mesh.vert(p3);
(p1-p0).cross(p2-p0).dot(p3-p0)==Fixed::ZERO
},
Self::Simplex3([p0,p1,p2])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
let p2=mesh.vert(p2);
(p1-p0).cross(p2-p0)==vec3::zero()
},
Self::Simplex2([p0,p1])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
p1-p0==vec3::zero()
}
}
}
}
/*
local function choosePerpendicularDirection(d)
local x, y, z = d.x, d.y, d.z
local best = math.min(x*x, y*y, z*z)
if x*x == best then
return Vector3.new(y*y + z*z, -x*y, -x*z)
elseif y*y == best then
return Vector3.new(-x*y, x*x + z*z, -y*z)
else
return Vector3.new(-x*z, -y*z, x*x + y*y)
end
end
*/
fn choose_perpendicular_direction(d:Planar64Vec3)->Planar64Vec3{
let x=d.x.abs();
let y=d.y.abs();
let z=d.z.abs();
if x<y&&x<z{
Vector3::new([Fixed::ZERO,-d.z,d.y])
}else if y<z{
Vector3::new([d.z,Fixed::ZERO,-d.x])
}else{
Vector3::new([-d.y,d.x,Fixed::ZERO])
}
}
const fn choose_any_direction()->Planar64Vec3{
vec3::X
}
fn narrow_dir2(dir:Vector3<Fixed<2,64>>)->Planar64Vec3{
if dir==vec3::zero(){
return dir.narrow_1().unwrap();
}
let x=dir.x.as_bits().unsigned_abs().bits();
let y=dir.y.as_bits().unsigned_abs().bits();
let z=dir.z.as_bits().unsigned_abs().bits();
let big=x.max(y).max(z);
const MAX_BITS:u32=64+31;
if MAX_BITS<big{
dir>>(big-MAX_BITS)
}else{
dir
}.narrow_1().unwrap()
}
fn narrow_dir3(dir:Vector3<Fixed<3,96>>)->Planar64Vec3{
if dir==vec3::zero(){
return dir.narrow_1().unwrap();
}
let x=dir.x.as_bits().unsigned_abs().bits();
let y=dir.y.as_bits().unsigned_abs().bits();
let z=dir.z.as_bits().unsigned_abs().bits();
let big=x.max(y).max(z);
const MAX_BITS:u32=96+31;
if MAX_BITS<big{
dir>>(big-MAX_BITS)
}else{
dir
}.narrow_1().unwrap()
}
fn reduce1<M:MeshQuery>(
[v0]:Simplex<1,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>{
// --debug.profilebegin("reduceSimplex0")
// local a = a1 - a0
let p0=mesh.vert(v0);
// local p = -a
let p=-(p0+point);
// local direction = p
let mut dir=p;
// if direction.magnitude == 0 then
// direction = chooseAnyDirection()
if dir==vec3::zero(){
dir=choose_any_direction();
}
// return direction, a0, a1
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex1(a0, a1, b0, b1)
fn reduce2<M:MeshQuery>(
[v0,v1]:Simplex<2,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>{
// --debug.profilebegin("reduceSimplex1")
// local a = a1 - a0
// local b = b1 - b0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
// local p = -a
// local u = b - a
let p=-(p0+point);
let u=p1-p0;
// -- modify to take into account the radiuses
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = u:Cross(p):Cross(u)
let direction=u.cross(p).cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
return Reduced{
dir:choose_perpendicular_direction(u),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// -- modify the direction to take into account a0R and b0R
// return direction, a0, a1, b0, b1
return Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// local direction = p
let mut dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
dir=choose_perpendicular_direction(u);
}
// return direction, a0, a1
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex2(a0, a1, b0, b1, c0, c1)
fn reduce3<M:MeshQuery>(
[v0,mut v1,v2]:Simplex<3,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>{
// --debug.profilebegin("reduceSimplex2")
// local a = a1 - a0
// local b = b1 - b0
// local c = c1 - c0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
// local p = -a
// local u = b - a
// local v = c - a
let p=-(p0+point);
let mut u=p1-p0;
let v=p2-p0;
// local uv = u:Cross(v)
// local up = u:Cross(p)
// local pv = p:Cross(v)
// local uv_up = uv:Dot(up)
// local uv_pv = uv:Dot(pv)
let mut uv=u.cross(v);
let mut up=u.cross(p);
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local uvp = uv:Dot(p)
let uvp=uv.dot(p);
// local direction = uvp < 0 and -uv or uv
let direction=if uvp.is_negative(){
-uv
}else{
uv
};
// return direction, a0, a1, b0, b1, c0, c1
return Reduced{
dir:narrow_dir2(direction),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
};
}
// local u_u = u:Dot(u)
// local v_v = v:Dot(v)
// local uDist = uv_up/(u_u*v.magnitude)
// local vDist = uv_pv/(v_v*u.magnitude)
// local minDist2 = math.min(uDist, vDist)
let u_dist=uv_up*v.length();
let v_dist=uv_pv*u.length();
// if vDist == minDist2 then
if v_dist<u_dist{
u=v;
up=-pv;
uv=-uv;
// b0 = c0
// b1 = c1
v1=v2;
}
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = up:Cross(u)
let direction=up.cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
// direction = uv
return Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// return direction, a0, a1, b0, b1
return Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// local direction = p
let dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
// direction = uv
return Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex1([v0]),
};
}
// return direction, a0, a0
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex3(a0, a1, b0, b1, c0, c1, d0, d1)
fn reduce4<M:MeshQuery>(
[v0,mut v1,mut v2,v3]:Simplex<4,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduce<M::Vert>{
// --debug.profilebegin("reduceSimplex3")
// local a = a1 - a0
// local b = b1 - b0
// local c = c1 - c0
// local d = d1 - d0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
let p3=mesh.vert(v3);
// local p = -a
// local u = b - a
// local v = c - a
// local w = d - a
let p=-(p0+point);
let mut u=p1-p0;
let mut v=p2-p0;
let w=p3-p0;
// local uv = u:Cross(v)
// local vw = v:Cross(w)
// local wu = w:Cross(u)
// local uvw = uv:Dot(w)
// local pvw = vw:Dot(p)
// local upw = wu:Dot(p)
// local uvp = uv:Dot(p)
let mut uv=u.cross(v);
let vw=v.cross(w);
let wu=w.cross(u);
let uv_w=uv.dot(w);
let pv_w=vw.dot(p);
let up_w=wu.dot(p);
let uv_p=uv.dot(p);
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
if !pv_w.div_sign(uv_w).is_negative()
||!up_w.div_sign(uv_w).is_negative()
||!uv_p.div_sign(uv_w).is_negative(){
// origin is contained, this is a positive detection
// local direction = Vector3.new(0, 0, 0)
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
return Reduce::Escape([v0,v1,v2,v3]);
}
// local uvwSign = uvw < 0 and -1 or uvw > 0 and 1 or 0
// local uvDist = uvp*uvwSign/uv.magnitude
// local vwDist = pvw*uvwSign/vw.magnitude
// local wuDist = upw*uvwSign/wu.magnitude
// local minDist3 = math.min(uvDist, vwDist, wuDist)
let uv_dist=uv_p.mul_sign(uv_w);
let vw_dist=pv_w.mul_sign(uv_w);
let wu_dist=up_w.mul_sign(uv_w);
let wu_len=wu.length();
let uv_len=uv.length();
let vw_len=vw.length();
if vw_dist*wu_len<wu_dist*vw_len{
// if vwDist == minDist3 then
if vw_dist*uv_len<uv_dist*vw_len{
(u,v)=(v,w);
uv=vw;
// uv_p=pv_w; // unused
// b0, c0 = c0, d0
// b1, c1 = c1, d1
(v1,v2)=(v2,v3);
}else{
v2=v3;
}
}else{
// elseif wuDist == minDist3 then
if wu_dist*uv_len<uv_dist*wu_len{
(u,v)=(w,u);
uv=wu;
// uv_p=up_w; // unused
// b0, c0 = d0, b0
// b1, c1 = d1, b1
// before [a,b,c,d]
(v1,v2)=(v3,v1);
// after [a,d,b]
}else{
v2=v3;
}
}
// local up = u:Cross(p)
// local pv = p:Cross(v)
// local uv_up = uv:Dot(up)
// local uv_pv = uv:Dot(pv)
let mut up=u.cross(p);
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1, c0, c1
if uv_w.is_negative(){
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
});
}else{
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
});
}
}
// local u_u = u:Dot(u)
// local v_v = v:Dot(v)
// local uDist = uv_up/(u_u*v.magnitude)
// local vDist = uv_pv/(v_v*u.magnitude)
// local minDist2 = math.min(uDist, vDist)
let u_dist=uv_up*v.length();
let v_dist=uv_pv*u.length();
// if vDist == minDist2 then
if v_dist<u_dist{
u=v;
up=-pv;
uv=-uv;
// b0 = c0
// b1 = c1
v1=v2;
}
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = up:Cross(u)
let direction=up.cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
// direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1
if uv_w.is_negative(){
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}else{
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}
}
// return direction, a0, a1, b0, b1
return Reduce::Reduced(Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}
// local direction = p
let dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
// direction = uvw < 0 and uv or -uv
if uv_w.is_negative(){
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex1([v0]),
});
}else{
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex1([v0]),
});
}
}
// return direction, a0, a1
Reduce::Reduced(Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
})
}
struct Reduced<Vert>{
dir:Planar64Vec3,
simplex:Simplex1_3<Vert>,
}
enum Reduce<Vert>{
Escape(Simplex<4,Vert>),
Reduced(Reduced<Vert>),
}
impl<Vert> Simplex2_4<Vert>{
fn reduce<M:MeshQuery<Vert=Vert>>(self,mesh:&M,point:Planar64Vec3)->Reduce<Vert>{
match self{
Self::Simplex2(simplex)=>Reduce::Reduced(reduce2(simplex,mesh,point)),
Self::Simplex3(simplex)=>Reduce::Reduced(reduce3(simplex,mesh,point)),
Self::Simplex4(simplex)=>reduce4(simplex,mesh,point),
}
}
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition<Vert>{
Done,//found closest vert, no edges are better
Vert(Vert),//transition to vert
}
enum EV<M:MeshQuery>{
Vert(M::Vert),
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
}
impl<M:MeshQuery> From<EV<M>> for FEV<M>{
fn from(value:EV<M>)->Self{
match value{
EV::Vert(minkowski_vert)=>FEV::Vert(minkowski_vert),
EV::Edge(minkowski_edge)=>FEV::Edge(minkowski_edge),
}
}
}
trait Contains{
fn contains(&self,point:Planar64Vec3)->bool;
}
// convenience type to check if a point is within some threshold of a plane.
struct ThickPlane{
point:Planar64Vec3,
normal:Vector3<Fixed<2,64>>,
epsilon:Fixed<3,96>,
}
impl ThickPlane{
fn new<M:MeshQuery>(mesh:&M,[v0,v1,v2]:Simplex<3,M::Vert>)->Self{
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
let point=p0;
let normal=(p1-p0).cross(p2-p0);
// Allow ~ 2*sqrt(3) units of thickness on the plane
// This is to account for the variance of two voxels across the longest diagonal
let epsilon=(normal.length()*(Planar64::EPSILON*3)).wrap_3();
Self{point,normal,epsilon}
}
}
impl Contains for ThickPlane{
fn contains(&self,point:Planar64Vec3)->bool{
(point-self.point).dot(self.normal).abs()<=self.epsilon
}
}
struct ThickLine{
point:Planar64Vec3,
dir:Planar64Vec3,
epsilon:Fixed<4,128>,
}
impl ThickLine{
fn new<M:MeshQuery>(mesh:&M,[v0,v1]:Simplex<2,M::Vert>)->Self{
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let point=p0;
let dir=p1-p0;
// Allow ~ 2*sqrt(3) units of thickness on the plane
// This is to account for the variance of two voxels across the longest diagonal
let epsilon=(dir.length_squared()*(Planar64::EPSILON*3)).widen_4();
Self{point,dir,epsilon}
}
}
impl Contains for ThickLine{
fn contains(&self,point:Planar64Vec3)->bool{
(point-self.point).cross(self.dir).length_squared()<=self.epsilon
}
}
struct EVFinder<'a,M,C>{
mesh:&'a M,
constraint:C,
best_distance_squared:Fixed<2,64>,
}
impl<M:MeshQuery,C:Contains> EVFinder<'_,M,C>{
fn next_transition_vert(&mut self,vert_id:M::Vert,point:Planar64Vec3)->Transition<M::Vert>{
let mut best_transition=Transition::Done;
for &directed_edge_id in self.mesh.vert_edges(vert_id).as_ref(){
//test if this edge's opposite vertex closer
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
//select opposite vertex
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
let test_pos=self.mesh.vert(test_vert_id);
let diff=point-test_pos;
let distance_squared=diff.dot(diff);
// ensure test_vert_id is coplanar to simplex
if distance_squared<self.best_distance_squared&&self.constraint.contains(test_pos){
best_transition=Transition::Vert(test_vert_id);
self.best_distance_squared=distance_squared;
}
}
best_transition
}
fn final_ev(&mut self,vert_id:M::Vert,point:Planar64Vec3)->EV<M>{
let mut best_transition=EV::Vert(vert_id);
let vert_pos=self.mesh.vert(vert_id);
let diff=point-vert_pos;
for &directed_edge_id in self.mesh.vert_edges(vert_id).as_ref(){
//test if this edge is closer
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
let test_pos=self.mesh.vert(test_vert_id);
let edge_n=test_pos-vert_pos;
let d=edge_n.dot(diff);
//test the edge
let edge_nn=edge_n.dot(edge_n);
// ensure edge contains closest point and directed_edge_id is coplanar to simplex
if !d.is_negative()&&d<=edge_nn&&self.constraint.contains(test_pos){
let distance_squared={
let c=diff.cross(edge_n);
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<=self.best_distance_squared{
best_transition=EV::Edge(directed_edge_id.as_undirected());
self.best_distance_squared=distance_squared;
}
}
}
best_transition
}
fn crawl_boundaries(&mut self,mut vert_id:M::Vert,point:Planar64Vec3)->EV<M>{
loop{
match self.next_transition_vert(vert_id,point){
Transition::Done=>return self.final_ev(vert_id,point),
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
}
}
}
}
/// 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 crawl_to_closest_ev<M:MeshQuery>(mesh:&M,simplex:Simplex<2,M::Vert>,point:Planar64Vec3)->EV<M>{
// naively start at the closest vertex
// the closest vertex is not necessarily the one with the fewest boundary hops
// but it doesn't matter, we will get there regardless.
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
let diff=point-mesh.vert(vert_id);
(vert_id,diff.dot(diff))
}).min_by_key(|&(_,d)|d).unwrap();
let constraint=ThickLine::new(mesh,simplex);
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
//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
finder.crawl_boundaries(vert_id,point)
}
/// 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 crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiVert>,point:Planar64Vec3)->FEV::<MinkowskiMesh<'a>>{
// naively start at the closest vertex
// the closest vertex is not necessarily the one with the fewest boundary hops
// but it doesn't matter, we will get there regardless.
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
let diff=point-mesh.vert(vert_id);
(vert_id,diff.dot(diff))
}).min_by_key(|&(_,d)|d).unwrap();
let constraint=ThickPlane::new(mesh,simplex);
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
//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
match finder.crawl_boundaries(vert_id,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if we are on the wrong side
let edge_n=mesh.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
let &[v0,v1]=mesh.edge_verts(edge_id).as_ref();
mesh.vert(v0)+mesh.vert(v1)
};
for (i,&face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
//test if this face is closer
let (face_n,d)=mesh.face_nd(face_id);
//if test point is behind face, the face is invalid
// TODO: find out why I thought of this backwards
if !(face_n.dot(point)-d).is_positive(){
continue;
}
//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);
let boundary_d=boundary_n.dot(delta_pos);
//is test point behind edge, i.e. contained in the face
if !boundary_d.is_positive(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
}
}
FEV::Edge(edge_id)
},
}
}
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
const ENABLE_FAST_FAIL:bool=false;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,simplex|{
if is_intersecting{
return None;
}
// Convert simplex to FEV
// Vertices must be inverted since the mesh is inverted
Some(match simplex{
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
Simplex1_3::Simplex2([v0,v1])=>{
// invert
let (v0,v1)=(-v0,-v1);
let ev=crawl_to_closest_ev(mesh,[v0,v1],point);
if !matches!(ev,EV::Edge(_)){
println!("I can't believe it's not an edge!");
}
ev.into()
},
Simplex1_3::Simplex3([v0,v1,v2])=>{
// invert
let (v0,v1,v2)=(-v0,-v1,-v2);
// Shimmy to the side until you find a face that contains the closest point
// it's ALWAYS representable as a face, but this algorithm may
// return E or V in edge cases but I don't think that will break the face crawler
let fev=crawl_to_closest_fev(mesh,[v0,v1,v2],point);
if !matches!(fev,FEV::Face(_)){
println!("I can't believe it's not a face!");
}
fev
},
})
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
// local norm, dist, u0, u1, v0, v1, w0, w1 = expand(queryP, queryQ, a0, a1, b0, b1, c0, c1, d0, d1, 1e-5)
// let simplex=refine_to_exact(mesh,simplex);
None
},
// fast_fail value is irrelevant and will never be returned!
||unreachable!()
)
}
// local function minimumDifference(
// queryP, radiusP,
// queryQ, radiusQ,
// exitRadius, testIntersection
// )
fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
mesh:&M,
point:Planar64Vec3,
on_exact:impl FnOnce(bool,Simplex1_3<M::Vert>)->T,
on_escape:impl FnOnce(Simplex<4,M::Vert>)->T,
on_fast_fail:impl FnOnce()->T,
)->T{
// local initialAxis = queryQ() - queryP()
// local new_point_p = queryP(initialAxis)
// local new_point_q = queryQ(-initialAxis)
// local direction, a0, a1, b0, b1, c0, c1, d0, d1
let mut initial_axis=mesh.hint_point()+point;
// degenerate case
if initial_axis==vec3::zero(){
initial_axis=choose_any_direction();
}
let last_point=mesh.farthest_vert(-initial_axis);
// this represents the 'a' value in the commented code
let mut last_pos=mesh.vert(last_point);
let Reduced{dir:mut direction,simplex:mut simplex_small}=reduce1([last_point],mesh,point);
// exitRadius = testIntersection and 0 or exitRadius or 1/0
// for _ = 1, 100 do
loop{
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
// local next_point = new_point_q - new_point_p
let next_point=mesh.farthest_vert(direction);
let next_pos=mesh.vert(next_point);
// if -direction:Dot(next_point) > (exitRadius + radiusP + radiusQ)*direction.magnitude then
if ENABLE_FAST_FAIL&&direction.dot(next_pos+point).is_negative(){
return on_fast_fail();
}
let simplex_big=simplex_small.push_front(next_point);
// if
// direction:Dot(next_point - a) <= 0 or
// absDet(next_point, a, b, c) < 1e-6
if !direction.dot(next_pos-last_pos).is_positive()
||simplex_big.det_is_zero(mesh){
// Found enough information to compute the exact closest point.
// local norm = direction.unit
// local dist = a:Dot(norm)
// local hits = -dist < radiusP + radiusQ
let is_intersecting=(last_pos+point).dot(direction).is_positive();
return on_exact(is_intersecting,simplex_small);
}
// direction, a0, a1, b0, b1, c0, c1, d0, d1 = reduceSimplex(new_point_p, new_point_q, a0, a1, b0, b1, c0, c1)
match simplex_big.reduce(mesh,point){
// if a and b and c and d then
Reduce::Escape(simplex)=>{
// Enough information to conclude that the meshes are intersecting.
// Topology information is computed if needed.
return on_escape(simplex);
},
Reduce::Reduced(reduced)=>{
direction=reduced.dir;
simplex_small=reduced.simplex;
},
}
// next loop this will be a
last_pos=next_pos;
}
}
#[cfg(test)]
mod test{
use super::*;
use crate::model::{PhysicsMesh,PhysicsMeshView};
fn mesh_contains_point(mesh:PhysicsMeshView<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
#[test]
fn test_cube_points(){
let mesh=PhysicsMesh::unit_cube();
let mesh_view=mesh.complete_mesh_view();
for x in -2..=2{
for y in -2..=2{
for z in -2..=2{
let point=vec3::int(x,y,z)>>1;
assert!(mesh_contains_point(mesh_view,point),"Mesh did not contain point {point}");
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use core::ops::Range; use core::ops::{Bound,RangeBounds};
use strafesnet_common::integer::vec3::Vector3; use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter}; use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio}; use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@@ -68,11 +68,12 @@ pub enum FEV<M:MeshQuery>{
} }
//use Unit32 #[repr(C)] for map files //use Unit32 #[repr(C)] for map files
#[derive(Clone,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
struct Face{ struct Face{
normal:Planar64Vec3, normal:Planar64Vec3,
dot:Planar64, dot:Planar64,
} }
#[derive(Debug)]
struct Vert(Planar64Vec3); struct Vert(Planar64Vec3);
pub trait MeshQuery{ pub trait MeshQuery{
type Face:Copy; type Face:Copy;
@@ -89,6 +90,10 @@ pub trait MeshQuery{
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref(); 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) (self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
} }
/// This must return a point inside the mesh.
#[expect(dead_code)]
fn hint_point(&self)->Planar64Vec3;
fn farthest_vert(&self,dir:Planar64Vec3)->Self::Vert;
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3; fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset); fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>; fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
@@ -97,18 +102,26 @@ pub trait MeshQuery{
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>; fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>; fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
} }
#[derive(Debug)]
struct FaceRefs{ struct FaceRefs{
// I didn't write it down, but I assume the edges are directed
// clockwise when looking towards the face normal, i.e. right hand rule.
edges:Vec<SubmeshDirectedEdgeId>, edges:Vec<SubmeshDirectedEdgeId>,
//verts are redundant, use edge[i].verts[0]
//verts:Vec<VertId>, //verts:Vec<VertId>,
} }
#[derive(Debug)]
struct EdgeRefs{ struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top verts:[SubmeshVertId;2],//start, end
} }
#[derive(Debug)]
struct VertRefs{ struct VertRefs{
faces:Vec<SubmeshFaceId>, faces:Vec<SubmeshFaceId>,
// edges are always directed away from the vert
edges:Vec<SubmeshDirectedEdgeId>, edges:Vec<SubmeshDirectedEdgeId>,
} }
#[derive(Debug)]
pub struct PhysicsMeshData{ pub struct PhysicsMeshData{
//this contains all real and virtual faces used in both the complete mesh and convex submeshes //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 //faces are sorted such that all faces that belong to the complete mesh appear first, and then
@@ -118,6 +131,7 @@ pub struct PhysicsMeshData{
faces:Vec<Face>,//MeshFaceId indexes this list faces:Vec<Face>,//MeshFaceId indexes this list
verts:Vec<Vert>,//MeshVertId indexes this list verts:Vec<Vert>,//MeshVertId indexes this list
} }
#[derive(Debug)]
pub struct PhysicsMeshTopology{ pub struct PhysicsMeshTopology{
//mapping of local ids to PhysicsMeshData ids //mapping of local ids to PhysicsMeshData ids
faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list
@@ -143,10 +157,12 @@ impl From<MeshId> for PhysicsMeshId{
pub struct PhysicsSubmeshId(u32); pub struct PhysicsSubmeshId(u32);
pub struct PhysicsMesh{ pub struct PhysicsMesh{
data:PhysicsMeshData, data:PhysicsMeshData,
complete_mesh:PhysicsMeshTopology, // The complete mesh is unused at this time.
//Most objects in roblox maps are already convex, so the list length is 0 // complete_mesh:PhysicsMeshTopology,
//as soon as the mesh is divided into 2 submeshes, the list length jumps to 2. // Submeshes are guaranteed to be convex and may contain
//length 1 is unnecessary since the complete mesh would be a duplicate of the only submesh, but would still function properly // "virtual" faces which are not part of the complete mesh.
// Physics calculations should never resolve to hitting
// a virtual face.
submeshes:Vec<PhysicsMeshTopology>, submeshes:Vec<PhysicsMeshTopology>,
} }
impl PhysicsMesh{ impl PhysicsMesh{
@@ -210,19 +226,24 @@ impl PhysicsMesh{
}; };
Self{ Self{
data, data,
complete_mesh:mesh_topology, // complete_mesh:mesh_topology.clone(),
submeshes:Vec::new(), submeshes:vec![mesh_topology],
} }
} }
pub fn unit_cylinder()->Self{ pub fn unit_cylinder()->Self{
Self::unit_cube() Self::unit_cube()
} }
#[inline] #[inline]
pub const fn complete_mesh(&self)->&PhysicsMeshTopology{ pub fn complete_mesh(&self)->&PhysicsMeshTopology{
&self.complete_mesh // 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] #[inline]
pub const fn complete_mesh_view(&self)->PhysicsMeshView{ pub fn complete_mesh_view(&self)->PhysicsMeshView<'_>{
PhysicsMeshView{ PhysicsMeshView{
data:&self.data, data:&self.data,
topology:self.complete_mesh(), topology:self.complete_mesh(),
@@ -230,21 +251,16 @@ impl PhysicsMesh{
} }
#[inline] #[inline]
pub fn submeshes(&self)->&[PhysicsMeshTopology]{ pub fn submeshes(&self)->&[PhysicsMeshTopology]{
//the complete mesh is already a convex mesh when len()==0, len()==1 is invalid but will still work &self.submeshes
if self.submeshes.len()==0{
std::slice::from_ref(&self.complete_mesh)
}else{
&self.submeshes.as_slice()
}
} }
#[inline] #[inline]
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView{ pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView<'_>{
PhysicsMeshView{ PhysicsMeshView{
data:&self.data, data:&self.data,
topology:&self.submeshes()[submesh_id.get() as usize], 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{ self.submeshes().iter().map(|topology|PhysicsMeshView{
data:&self.data, data:&self.data,
topology, topology,
@@ -313,14 +329,20 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
if mesh.unique_pos.len()==0{ if mesh.unique_pos.len()==0{
return Err(PhysicsMeshError::ZeroVertices); 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(); let verts=mesh.unique_pos.iter().copied().map(Vert).collect();
//TODO: fix submeshes //TODO: fix submeshes
//flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id //flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id
//lower face_id points to upper face_id //lower face_id points to upper face_id
//the same face is not allowed to be in multiple polygon groups //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 faces=Vec::new();
let mut face_id_from_face=HashMap::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 //construct submesh
let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId
let mut submesh_verts=Vec::new(); let mut submesh_verts=Vec::new();
@@ -381,15 +403,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
normal:(normal/len as i64).divide().narrow_1().unwrap(), normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(), dot:(dot/(len*len) as i64).narrow_1().unwrap(),
}; };
let face_id=match face_id_from_face.get(&face){ let face_id=*face_id_from_face.entry(face).or_insert_with(||{
Some(&face_id)=>face_id, let face_id=MeshFaceId::new(faces.len() as u32);
None=>{ faces.push(face);
let face_id=MeshFaceId::new(faces.len() as u32); face_id
face_id_from_face.insert(face.clone(),face_id); });
faces.push(face);
face_id
}
};
submesh_faces.push(face_id); submesh_faces.push(face_id);
face_ref_guys.push(FaceRefEdges(face_edges)); face_ref_guys.push(FaceRefEdges(face_edges));
} }
@@ -397,16 +415,16 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
PhysicsMeshTopology{ PhysicsMeshTopology{
faces:submesh_faces, faces:submesh_faces,
verts:submesh_verts, verts:submesh_verts,
face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{ face_topology:face_ref_guys.into_iter().map(|FaceRefEdges(edges)|{
FaceRefs{edges:face_ref_guy.0} FaceRefs{edges}
}).collect(), }).collect(),
edge_topology:edge_pool.edge_guys.into_iter().map(|(edge_ref_verts,edge_ref_faces)| edge_topology:edge_pool.edge_guys.into_iter().map(|(EdgeRefVerts(verts),EdgeRefFaces(faces))|
EdgeRefs{faces:edge_ref_faces.0,verts:edge_ref_verts.0} EdgeRefs{faces,verts}
).collect(), ).collect(),
vert_topology:vert_ref_guys.into_iter().map(|vert_ref_guy| vert_topology:vert_ref_guys.into_iter().map(|VertRefGuy{edges,faces}|
VertRefs{ VertRefs{
edges:vert_ref_guy.edges.into_iter().collect(), edges:edges.into_iter().collect(),
faces:vert_ref_guy.faces.into_iter().collect(), faces:faces.into_iter().collect(),
} }
).collect(), ).collect(),
} }
@@ -416,12 +434,13 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
faces, faces,
verts, verts,
}, },
complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?, // complete_mesh:None,
submeshes:mesh_topologies, submeshes:mesh_topologies,
}) })
} }
} }
#[derive(Debug,Clone,Copy)]
pub struct PhysicsMeshView<'a>{ pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData, data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology, topology:&'a PhysicsMeshTopology,
@@ -436,6 +455,22 @@ impl MeshQuery for PhysicsMeshView<'_>{
let face_idx=self.topology.faces[face_id.get() as usize].get() as usize; 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) (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()
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
self.topology.verts.iter()
.enumerate()
.max_by_key(|&(_,&vert_id)|
dir.dot(self.data.verts[vert_id.get() as usize].0)
)
//assume there is more than zero vertices.
.unwrap().0 as u32
)
}
//ideally I never calculate the vertex position, but I have to for the graphical meshes... //ideally I never calculate the vertex position, but I have to for the graphical meshes...
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{ fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize; let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
@@ -458,6 +493,7 @@ impl MeshQuery for PhysicsMeshView<'_>{
} }
} }
#[derive(Debug)]
pub struct PhysicsMeshTransform{ pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3, pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>, pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
@@ -473,6 +509,7 @@ impl PhysicsMeshTransform{
} }
} }
#[derive(Debug,Clone,Copy)]
pub struct TransformedMesh<'a>{ pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>, view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform, transform:&'a PhysicsMeshTransform,
@@ -487,24 +524,12 @@ impl TransformedMesh<'_>{
transform, 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)) self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
} }
pub fn faces(&self)->impl Iterator<Item=SubmeshFaceId>{ pub fn faces(&self)->impl Iterator<Item=SubmeshFaceId>{
(0..self.view.topology.faces.len() as u32).map(SubmeshFaceId::new) (0..self.view.topology.faces.len() as u32).map(SubmeshFaceId::new)
} }
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
self.view.topology.verts.iter()
.enumerate()
.max_by_key(|&(_,&vert_id)|
dir.dot(self.transform.vertex.transform_point3(self.view.data.verts[vert_id.get() as usize].0))
)
//assume there is more than zero vertices.
.unwrap().0 as u32
)
}
} }
impl MeshQuery for TransformedMesh<'_>{ impl MeshQuery for TransformedMesh<'_>{
type Face=SubmeshFaceId; type Face=SubmeshFaceId;
@@ -522,6 +547,21 @@ impl MeshQuery for TransformedMesh<'_>{
// wrap for speed // wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1() self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
} }
fn hint_point(&self)->Planar64Vec3{
self.transform.vertex.translation
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
self.view.topology.verts.iter()
.enumerate()
.max_by_key(|&(_,&vert_id)|
dir.dot(self.transform.vertex.transform_point3(self.view.data.verts[vert_id.get() as usize].0))
)
//assume there is more than zero vertices.
.unwrap().0 as u32
)
}
#[inline] #[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{ fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id) self.view.face_edges(face_id)
@@ -548,11 +588,20 @@ impl MeshQuery for TransformedMesh<'_>{
//(face,vertex) //(face,vertex)
//(edge,edge) //(edge,edge)
//(vertex,face) //(vertex,face)
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum MinkowskiVert{ pub enum MinkowskiVert{
VertVert(SubmeshVertId,SubmeshVertId), VertVert(SubmeshVertId,SubmeshVertId),
} }
#[derive(Clone,Copy,Debug)] // TODO: remove this
impl core::ops::Neg for MinkowskiVert{
type Output=Self;
fn neg(self)->Self::Output{
match self{
MinkowskiVert::VertVert(v0,v1)=>MinkowskiVert::VertVert(v1,v0),
}
}
}
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum MinkowskiEdge{ pub enum MinkowskiEdge{
VertEdge(SubmeshVertId,SubmeshEdgeId), VertEdge(SubmeshVertId,SubmeshEdgeId),
EdgeVert(SubmeshEdgeId,SubmeshVertId), EdgeVert(SubmeshEdgeId,SubmeshVertId),
@@ -567,7 +616,7 @@ impl UndirectedEdge for MinkowskiEdge{
} }
} }
} }
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum MinkowskiDirectedEdge{ pub enum MinkowskiDirectedEdge{
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId), VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId), EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
@@ -588,7 +637,7 @@ impl DirectedEdge for MinkowskiDirectedEdge{
} }
} }
} }
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Debug,Hash)]
pub enum MinkowskiFace{ pub enum MinkowskiFace{
VertFace(SubmeshVertId,SubmeshFaceId), VertFace(SubmeshVertId,SubmeshFaceId),
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool), EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
@@ -598,23 +647,25 @@ pub enum MinkowskiFace{
//FaceFace //FaceFace
} }
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{ pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>, mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>, mesh1:TransformedMesh<'a>,
} }
//infinity fev algorithm state transition pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
#[derive(Debug)] pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
enum Transition{ let r=(time-relative_to).to_ratio();
Done,//found closest vert, no edges are better Ratio::new(r.num.widen_4(),r.den.widen_4())
Vert(MinkowskiVert),//transition to vert
}
enum EV{
Vert(MinkowskiVert),
Edge(MinkowskiEdge),
} }
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>; // TODO: remove this
impl<'a> core::ops::Neg for &MinkowskiMesh<'a>{
type Output=MinkowskiMesh<'a>;
fn neg(self)->Self::Output{
MinkowskiMesh::minkowski_sum(self.mesh1,self.mesh0)
}
}
impl MinkowskiMesh<'_>{ impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{ pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
@@ -623,151 +674,35 @@ impl MinkowskiMesh<'_>{
mesh1, mesh1,
} }
} }
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{ pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir)) let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?;
//continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
} }
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{ pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let mut best_transition=Transition::Done; let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){ // TODO: handle unbounded collision using infinity fev
let edge_n=self.directed_edge_n(directed_edge_id); let time=match upper_bound{
//is boundary uncrossable by a crawl from infinity Bound::Included(&time)=>time,
let edge_verts=self.edge_verts(directed_edge_id.as_undirected()); Bound::Excluded(&time)=>time,
//select opposite vertex Bound::Unbounded=>unimplemented!("unbounded collision out"),
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(){
let distance_squared=diff.dot(diff);
if distance_squared<*best_distance_squared{
best_transition=Transition::Vert(test_vert_id);
*best_distance_squared=distance_squared;
}
}
}
best_transition
}
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).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
if edge_n.dot(infinity_dir).is_zero(){
let d=edge_n.dot(diff);
//test the edge
let edge_nn=edge_n.dot(edge_n);
if !d.is_negative()&&d<=edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
//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());
*best_distance_squared=distance_squared;
}
}
}
}
best_transition
}
fn crawl_boundaries(&self,mut vert_id:MinkowskiVert,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
let mut best_distance_squared={
let diff=point-self.vert(vert_id);
diff.dot(diff)
}; };
loop{ let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?;
match self.next_transition_vert(vert_id,&mut best_distance_squared,infinity_dir,point){ // swap and negate bounds to do a time inversion
Transition::Done=>return self.final_ev(vert_id,&mut best_distance_squared,infinity_dir,point), let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
Transition::Vert(new_vert_id)=>vert_id=new_vert_id, let infinity_body=-relative_body;
} //continue backwards along the body parabola
} 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))
} }
/// 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 pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{ // TODO: make better
//start on any vertex use crate::face_crawler::{low,upp};
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if the boundary is not crossable and we are on the wrong side
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).as_ref();
self.vert(v0)+self.vert(v1)
};
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);
let boundary_d=boundary_n.dot(delta_pos);
//check if time of collision is outside Time::MIN..Time::MAX
//infinity_dir can always be treated as a velocity
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
}
}
FEV::Edge(edge_id)
},
}
}
// TODO: fundamentally improve this algorithm.
// All it needs to do is find the closest point on the mesh
// and return the FEV which the point resides on.
//
// What it actually does is use the above functions to trace a ray in from infinity,
// crawling the closest point along the mesh surface until the ray reaches
// the starting point to discover the final FEV.
//
// The actual collision prediction probably does a single test
// and then immediately returns with 0 FEV transitions on average,
// because of the strict time_limit constraint.
//
// 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>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values?
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
})
}
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|{
//continue forwards along the body parabola
fev.crawl(self,relative_body,start_time,time_limit).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|{
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,-time_limit,-start_time).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)>{
//no algorithm needed, there is only one state and two cases (Edge,None) //no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case) //determine when it passes an edge ("sliding off" case)
let start_time={ let start_time=range.start_bound().map(|&t|(t-relative_body.time).to_ratio());
let r=(start_time-relative_body.time).to_ratio(); let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time));
Ratio::new(r.num,r.den)
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let mut best_edge=None; let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0; let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){ for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
@@ -780,28 +715,17 @@ impl MinkowskiMesh<'_>{
//WARNING: truncated precision //WARNING: truncated precision
//wrap for speed //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()){ for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; best_time=Bound::Included(dt);
best_edge=Some(directed_edge_id); best_edge=Some((directed_edge_id,dt));
break; break;
} }
} }
} }
best_edge.map(|e|(e.as_undirected(),best_time)) best_edge
} }
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{ pub fn contains_point(&self,point:Planar64Vec3)->bool{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position); crate::minimum_difference::contains_point(self,point)
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,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);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
.is_some_and(|_|
self.infinity_in(-infinity_body)
.is_some()
)
} }
} }
impl MeshQuery for MinkowskiMesh<'_>{ impl MeshQuery for MinkowskiMesh<'_>{
@@ -840,6 +764,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
}, },
} }
} }
fn hint_point(&self)->Planar64Vec3{
self.mesh0.transform.vertex.translation-self.mesh1.transform.vertex.translation
}
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
}
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{ fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
match face_id{ match face_id{
MinkowskiFace::VertFace(v0,f1)=>{ MinkowskiFace::VertFace(v0,f1)=>{
@@ -929,10 +859,10 @@ impl MeshQuery for MinkowskiMesh<'_>{
} }
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{ fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{ AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1| MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1) MinkowskiVert::VertVert(v0,vert_id1)
), ),
MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0| MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1) MinkowskiVert::VertVert(vert_id0,v1)
), ),
}) })
@@ -942,38 +872,43 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiVert::VertVert(v0,v1)=>{ MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new(); let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir //detect shared volume when the other mesh is mirrored along a test edge dir
let v0f=self.mesh0.vert_faces(v0); let v0f_thing=self.mesh0.vert_faces(v0);
let v1f=self.mesh1.vert_faces(v1); let v1f_thing=self.mesh1.vert_faces(v1);
let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect(); let v0f=v0f_thing.as_ref();
let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect(); let v1f=v1f_thing.as_ref();
let the_len=v0f.as_ref().len()+v1f.as_ref().len(); let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){ for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id); let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n); let nn=n.dot(n);
// TODO: there's gotta be a better way to do this // TODO: there's gotta be a better way to do this
//make a set of faces // drop faces beyond v0f_n
let mut face_normals=Vec::with_capacity(the_len); face_normals.truncate(v0f.len());
//add mesh0 faces as-is // make a set of faces from mesh0's perspective
face_normals.clone_from(&v0f_n);
for face_n in &v1f_n{ for face_n in &v1f_n{
//add reflected mesh1 faces //add reflected mesh1 faces
//wrap for speed //wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3()); 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)); edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
} }
} }
face_normals.clone_from(&v1f_n);
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){ for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id); let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n); let nn=n.dot(n);
let mut face_normals=Vec::with_capacity(the_len); // drop faces beyond v1f_n
face_normals.clone_from(&v1f_n); face_normals.truncate(v1f.len());
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{ for face_n in &v0f_n{
//wrap for speed //wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3()); 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)); edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
} }
} }
@@ -983,11 +918,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
} }
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{ fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
unimplemented!(); unimplemented!();
vec![] #[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(); let len=normals.len();
for i in 0..len-1{ for i in 0..len-1{
for j in i+1..len{ for j in i+1..len{
@@ -1013,6 +949,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
#[test] #[test]
fn test_is_empty_volume(){ fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec())); assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec())); assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
} }

File diff suppressed because it is too large Load Diff

View File

@@ -39,20 +39,18 @@ impl Contact{
//note that this is horrible with fixed point arithmetic //note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{ 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); let det=c0.normal.dot(c0.velocity);
if det.abs()<EPSILON{ if det.abs()==Fixed::ZERO{
return None; return None;
} }
let d0=c0.normal.dot(c0.position); let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det) Some(c0.normal*d0/det)
} }
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{ 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 u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal); let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1); let det=u0_u1.dot(n0_n1);
if det.abs()<EPSILON{ if det.abs()==Fixed::ZERO{
return None; return None;
} }
let d0=c0.normal.dot(c0.position); let d0=c0.normal.dot(c0.position);
@@ -60,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) 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>>>{ 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 n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1); let det=c2.normal.dot(n0_n1);
if det.abs()<EPSILON{ if det.abs()==Fixed::ZERO{
return None; return None;
} }
let d0=c0.normal.dot(c0.position); let d0=c0.normal.dot(c0.position);
@@ -149,7 +146,7 @@ fn is_space_enclosed_4(
} }
const fn get_push_ray_0(point:Planar64Vec3)->Ray{ 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>{ fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
//wrap for speed //wrap for speed
@@ -192,7 +189,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){ const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const()) (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) get_push_ray_1(point,c0)
.map(|ray|(ray,Conts::from_iter([c0]))) .map(|ray|(ray,Conts::from_iter([c0])))
} }
@@ -321,13 +318,13 @@ mod tests{
fn test_push_solve(){ fn test_push_solve(){
let contacts=vec![ let contacts=vec![
Contact{ Contact{
position:vec3::ZERO, position:vec3::zero(),
velocity:vec3::Y, velocity:vec3::Y,
normal:vec3::Y, normal:vec3::Y,
} }
]; ];
assert_eq!( assert_eq!(
vec3::ZERO, vec3::zero(),
push_solve(&contacts,vec3::NEG_Y) 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_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" } strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" } strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -2,6 +2,7 @@ use std::collections::HashMap;
use strafesnet_common::gameplay_modes::{ModeId,StageId}; use strafesnet_common::gameplay_modes::{ModeId,StageId};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction}; use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
use strafesnet_common::model::ModelId;
// session represents the non-hardware state of the client. // session represents the non-hardware state of the client.
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state. // Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
use strafesnet_common::physics::{ use strafesnet_common::physics::{
@@ -31,7 +32,7 @@ pub enum SessionInputInstruction{
Mouse(glam::IVec2), Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction), SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction), Mode(ImplicitModeInstruction),
Misc(strafesnet_common::physics::MiscInstruction), Misc(MiscInstruction),
} }
/// Implicit mode instruction are fed separately to session. /// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction /// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
@@ -61,6 +62,7 @@ pub struct FrameState{
pub body:physics::Body, pub body:physics::Body,
pub camera:physics::PhysicsCamera, pub camera:physics::PhysicsCamera,
pub time:PhysicsTime, pub time:PhysicsTime,
pub debug_model:Option<ModelId>,
} }
pub struct Simulation{ pub struct Simulation{
@@ -77,11 +79,12 @@ impl Simulation{
physics, physics,
} }
} }
pub fn get_frame_state(&self,time:SessionTime)->FrameState{ pub fn get_frame_state(&self,time:SessionTime,debug_model:Option<ModelId>)->FrameState{
FrameState{ FrameState{
body:self.physics.camera_body(), body:self.physics.camera_body(),
camera:self.physics.camera(), camera:self.physics.camera(),
time:self.timer.time(time), time:self.timer.time(time),
debug_model,
} }
} }
} }
@@ -152,15 +155,16 @@ enum ViewState{
pub struct Session{ pub struct Session{
directories:Directories, directories:Directories,
user_settings:UserSettings, user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator, mouse_interpolator:MouseInterpolator,
view_state:ViewState, view_state:ViewState,
//gui:GuiState //gui:GuiState
geometry_shared:physics::PhysicsData, geometry_shared:PhysicsData,
simulation:Simulation, simulation:Simulation,
// below fields not included in lite session // below fields not included in lite session
recording:Recording, recording:Recording,
//players:HashMap<PlayerId,Simulation>, //players:HashMap<PlayerId,Simulation>,
replays:HashMap<BotId,Replay>, replays:HashMap<BotId,Replay>,
last_ray_hit:Option<ModelId>,
} }
impl Session{ impl Session{
pub fn new( pub fn new(
@@ -172,11 +176,12 @@ impl Session{
user_settings, user_settings,
directories, directories,
mouse_interpolator:MouseInterpolator::new(), mouse_interpolator:MouseInterpolator::new(),
geometry_shared:Default::default(), geometry_shared:PhysicsData::empty(),
simulation, simulation,
view_state:ViewState::Play, view_state:ViewState::Play,
recording:Default::default(), recording:Default::default(),
replays:HashMap::new(), replays:HashMap::new(),
last_ray_hit:None,
} }
} }
fn clear_recording(&mut self){ fn clear_recording(&mut self){
@@ -184,16 +189,29 @@ impl Session{
} }
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){ fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.clear(); 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>{ pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
match &self.view_state{ match &self.view_state{
ViewState::Play=>Some(self.simulation.get_frame_state(time)), ViewState::Play=>Some(self.simulation.get_frame_state(time,self.last_ray_hit)),
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay| ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
replay.simulation.get_frame_state(time) replay.simulation.get_frame_state(time,None)
), ),
} }
} }
pub fn debug_raycast_print_model_id_if_changed(&mut self,time:SessionTime){
if let Some(frame_state)=self.get_frame_state(time){
let ray=strafesnet_common::ray::Ray{
origin:frame_state.body.extrapolated_position(self.simulation.timer.time(time)),
direction:-frame_state.camera.rotation().z_axis,
};
let model_id=self.geometry_shared.trace_ray(ray);
if model_id!=self.last_ray_hit{
println!("hit={model_id:?}");
self.last_ray_hit=model_id;
}
}
}
pub fn user_settings(&self)->&UserSettings{ pub fn user_settings(&self)->&UserSettings{
&self.user_settings &self.user_settings
} }

View File

@@ -8,3 +8,6 @@ configparser = "3.0.2"
directories = "6.0.0" directories = "6.0.0"
glam = "0.30.0" glam = "0.30.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" } 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" edition = "2024"
[dependencies] [dependencies]
glam = "0.30.0"
strafesnet_common = { path = "../lib/common", registry = "strafesnet" } strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" } strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", 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,7 +1,15 @@
use std::io::Cursor; mod error;
use std::path::Path; mod util;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_scenes;
use std::time::Instant; use std::time::Instant;
use error::ReplayError;
use util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext}; use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){ fn main(){
@@ -13,40 +21,6 @@ fn main(){
} }
} }
#[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)
}
}
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 data=std::fs::read(path)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{ fn run_replay()->Result<(),ReplayError>{
println!("loading map file.."); println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?; let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
@@ -57,9 +31,8 @@ fn run_replay()->Result<(),ReplayError>{
let bot=strafesnet_snf::read_bot(data)?.read_all()?; let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording // create recording
let mut physics_data=PhysicsData::default();
println!("generating models.."); println!("generating models..");
physics_data.generate_models(&map); let physics_data=PhysicsData::new(&map);
println!("simulating..."); println!("simulating...");
let mut physics=PhysicsState::default(); let mut physics=PhysicsState::default();
for ins in bot.instructions{ for ins in bot.instructions{
@@ -167,9 +140,8 @@ fn test_determinism()->Result<(),ReplayError>{
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?; let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?; let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models.."); println!("generating models..");
physics_data.generate_models(&map); let physics_data=PhysicsData::new(&map);
let (send,recv)=std::sync::mpsc::channel(); let (send,recv)=std::sync::mpsc::channel();

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

View File

@@ -7,7 +7,7 @@ use crate::{valve_transform_normal,valve_transform_dist};
#[derive(Hash,Eq,PartialEq)] #[derive(Hash,Eq,PartialEq)]
struct Face{ struct Face{
normal:integer::Planar64Vec3, normal:integer::Planar64Vec3,
dot:integer::Planar64, dot:Planar64,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -187,7 +187,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
} }
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum BrushToMeshError{ pub enum BrushToMeshError{
SliceBrushSides, SliceBrushSides,
@@ -210,7 +210,7 @@ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
let color=mb.acquire_color_id(glam::Vec4::ONE); let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO); let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// normals are ignored by physics // normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO); let normal=mb.acquire_normal_id(integer::vec3::zero());
let polygon_list=faces.into_iter().map(|face|{ let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{ face.into_iter().map(|pos|{

View File

@@ -105,7 +105,7 @@ pub fn convert<'a>(
water:Some(attr::IntersectingWater{ water:Some(attr::IntersectingWater{
viscosity:integer::Planar64::ONE, viscosity:integer::Planar64::ONE,
density:integer::Planar64::ONE, density:integer::Planar64::ONE,
velocity:integer::vec3::ZERO, velocity:integer::vec3::zero(),
}), }),
}, },
general:attr::GeneralAttributes::default(), general:attr::GeneralAttributes::default(),
@@ -295,7 +295,7 @@ pub fn convert<'a>(
attributes, attributes,
transform:integer::Planar64Affine3::new( transform:integer::Planar64Affine3::new(
integer::mat3::identity(), integer::mat3::identity(),
integer::vec3::ZERO, integer::vec3::zero(),
), ),
color:glam::Vec4::ONE, color:glam::Vec4::ONE,
}); });
@@ -347,9 +347,9 @@ pub struct PartialMap1{
modes:NormalizedModes, modes:NormalizedModes,
} }
impl PartialMap1{ impl PartialMap1{
pub fn add_prop_meshes<'a>( pub fn add_prop_meshes(
self, self,
prop_meshes:Meshes, prop_meshes:Meshes<model::Mesh>,
)->PartialMap2{ )->PartialMap2{
PartialMap2{ PartialMap2{
attributes:self.attributes, attributes:self.attributes,

View File

@@ -5,7 +5,6 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk}; use crate::{Bsp,Vpk};
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum TextureError{ pub enum TextureError{
Io(std::io::Error), Io(std::io::Error),
@@ -32,7 +31,7 @@ impl Loader for TextureLoader{
type Error=TextureError; type Error=TextureError;
type Index<'a>=Cow<'a,str>; type Index<'a>=Cow<'a,str>;
type Resource=Texture; type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->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 file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?; let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new(); let mut data=Vec::new();
@@ -41,7 +40,6 @@ impl Loader for TextureLoader{
} }
} }
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum MeshError{ pub enum MeshError{
Io(std::io::Error), Io(std::io::Error),
@@ -113,7 +111,7 @@ impl ModelLoader<'_,'_>{
} }
} }
} }
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{ impl Loader for ModelLoader<'_,'_>{
type Error=MeshError; type Error=MeshError;
type Index<'a>=&'a str where Self:'a; type Index<'a>=&'a str where Self:'a;
type Resource=vmdl::Model; type Resource=vmdl::Model;
@@ -153,7 +151,7 @@ impl MeshLoader<'_,'_,'_,'_>{
} }
} }
} }
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{ impl Loader for MeshLoader<'_,'_,'_,'_>{
type Error=MeshError; type Error=MeshError;
type Index<'a>=&'a str where Self:'a; type Index<'a>=&'a str where Self:'a;
type Resource=Mesh; type Resource=Mesh;

View File

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

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_common" name = "strafesnet_common"
version = "0.6.0" version = "0.7.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -17,3 +17,6 @@ linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" } ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
glam = "0.30.0" glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" } id = { version = "0.1.0", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -2,7 +2,9 @@ use crate::integer::{vec3,Planar64Vec3};
#[derive(Clone)] #[derive(Clone)]
pub struct Aabb{ pub struct Aabb{
// min is inclusive
min:Planar64Vec3, min:Planar64Vec3,
// max is not inclusive
max:Planar64Vec3, max:Planar64Vec3,
} }
@@ -43,7 +45,7 @@ impl Aabb{
} }
#[inline] #[inline]
pub fn contains(&self,point:Planar64Vec3)->bool{ pub fn contains(&self,point:Planar64Vec3)->bool{
let bvec=self.min.lt(point)&point.lt(self.max); let bvec=self.min.le(point)&point.lt(self.max);
bvec.all() bvec.all()
} }
#[inline] #[inline]
@@ -59,11 +61,11 @@ impl Aabb{
pub fn center(&self)->Planar64Vec3{ pub fn center(&self)->Planar64Vec3{
self.min.map_zip(self.max,|(min,max)|min.midpoint(max)) self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
} }
//probably use floats for area & volume because we don't care about precision #[inline]
// pub fn area_weight(&self)->f32{ pub fn area_weight(&self)->fixed_wide::fixed::Fixed<2,64>{
// let d=self.max-self.min; let d=self.max-self.min;
// d.x*d.y+d.y*d.z+d.z*d.x d.x*d.y+d.y*d.z+d.z*d.x
// } }
// pub fn volume(&self)->f32{ // pub fn volume(&self)->f32{
// let d=self.max-self.min; // let d=self.max-self.min;
// d.x*d.y*d.z // d.x*d.y*d.z

View File

@@ -245,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>{ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
let n=boxen.len(); let n=boxen.len();
if force||n<20{ const MAX_TERMINAL_BRANCH_LEAF_NODES:usize=20;
let mut aabb=Aabb::default(); if force||n<MAX_TERMINAL_BRANCH_LEAF_NODES{
let nodes=boxen.into_iter().map(|b|{ let mut aabb_outer=Aabb::default();
aabb.join(&b.1); let nodes=boxen.into_iter().map(|(data,aabb)|{
aabb_outer.join(&aabb);
BvhNode{ BvhNode{
content:RecursiveContent::Leaf(b.0), content:RecursiveContent::Leaf(data),
aabb:b.1, aabb,
} }
}).collect(); }).collect();
BvhNode{ BvhNode{
content:RecursiveContent::Branch(nodes), content:RecursiveContent::Branch(nodes),
aabb, aabb:aabb_outer,
} }
}else{ }else{
let mut sort_x=Vec::with_capacity(n); let mut sort_x=Vec::with_capacity(n);
@@ -272,9 +273,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
sort_y.sort_by_key(|&(_,c)|c); sort_y.sort_by_key(|&(_,c)|c);
sort_z.sort_by_key(|&(_,c)|c); sort_z.sort_by_key(|&(_,c)|c);
let h=n/2; let h=n/2;
let median_x=sort_x[h].1; let (_,median_x)=sort_x[h];
let median_y=sort_y[h].1; let (_,median_y)=sort_y[h];
let median_z=sort_z[h].1; let (_,median_z)=sort_z[h];
//locate a run of values equal to the median //locate a run of values equal to the median
//partition point gives the first index for which the predicate evaluates to false //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); let first_index_eq_median_x=sort_x.partition_point(|&(_,x)|x<median_x);
@@ -313,10 +314,10 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
}; };
list_list[list_id].push((data,aabb)); list_list[list_id].push((data,aabb));
} }
let mut aabb=Aabb::default();
if list_list.len()==1{ if list_list.len()==1{
generate_bvh_node(list_list.remove(0),true) generate_bvh_node(list_list.remove(0),true)
}else{ }else{
let mut aabb=Aabb::default();
BvhNode{ BvhNode{
content:RecursiveContent::Branch( content:RecursiveContent::Branch(
list_list.into_iter().map(|b|{ list_list.into_iter().map(|b|{

View File

@@ -140,6 +140,15 @@ impl ModeId{
pub const MAIN:Self=Self(0); pub const MAIN:Self=Self(0);
pub const BONUS:Self=Self(1); 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)] #[derive(Clone)]
pub struct Mode{ pub struct Mode{
style:gameplay_style::StyleModifiers, style:gameplay_style::StyleModifiers,
@@ -440,11 +449,11 @@ impl ModesBuilder{
} }
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect()) NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
} }
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode){ pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode)->Result<(),ExistingEntryError>{
assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode"); error_if_exists(self.modes.insert(mode_id,mode))
} }
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage){ pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage)->Result<(),ExistingEntryError>{
assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage"); 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){ pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
self.mode_updates.push((mode_id,mode_update)); self.mode_updates.push((mode_id,mode_update));
@@ -453,3 +462,12 @@ impl ModesBuilder{
// self.stage_updates.push((mode_id,stage_id,stage_update)); // 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 //unused
pub mass:Planar64, pub mass:Planar64,
} }
impl std::default::Default for StyleModifiers{ impl Default for StyleModifiers{
fn default()->Self{ fn default()->Self{
Self::roblox_bhop() Self::roblox_bhop()
} }
@@ -319,7 +319,7 @@ impl WalkSettings{
self.accelerate.accel.min((-gravity.y*friction).clamp_1()) self.accelerate.accel.min((-gravity.y*friction).clamp_1())
} }
pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{ 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; return control_dir;
} }
let nn=normal.length_squared(); let nn=normal.length_squared();
@@ -329,13 +329,13 @@ impl WalkSettings{
let dd=d*d; let dd=d*d;
if dd<nnmm{ if dd<nnmm{
let cr=normal.cross(control_dir); let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{ if cr==crate::integer::vec3::zero(){
crate::integer::vec3::ZERO crate::integer::vec3::zero()
}else{ }else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1() (cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
} }
}else{ }else{
crate::integer::vec3::ZERO crate::integer::vec3::zero()
} }
} }
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{ pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
@@ -360,7 +360,7 @@ impl LadderSettings{
self.accelerate.accel self.accelerate.accel
} }
pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{ 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; return control_dir;
} }
let nn=normal.length_squared(); let nn=normal.length_squared();
@@ -382,13 +382,13 @@ impl LadderSettings{
//- fix the underlying issue //- fix the underlying issue
if dd<nnmm{ if dd<nnmm{
let cr=normal.cross(control_dir); let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{ if cr==crate::integer::vec3::zero(){
crate::integer::vec3::ZERO crate::integer::vec3::zero()
}else{ }else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1() (cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
} }
}else{ }else{
crate::integer::vec3::ZERO crate::integer::vec3::zero()
} }
} }
} }

View File

@@ -34,12 +34,41 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsu
self.process_instruction(instruction); 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 where
T:Copy, 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!!! //PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{ pub struct InstructionCollector<I,T>{

View File

@@ -1,5 +1,5 @@
pub use fixed_wide::fixed::*; pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide}; pub use ratio_ops::ratio::{Ratio,Divide,Parity};
//integer units //integer units
@@ -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) 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{ fn default()->Self{
Self::raw(0) 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::SubAssign,sub_assign);
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign); impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
impl<T> std::ops::Mul for Time<T>{ 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] #[inline]
fn mul(self,rhs:Self)->Self::Output{ 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))) 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>{ macro_rules! impl_time_i64_rhs_operator {
type Output=Self; ($op:ident,$method:ident)=>{
#[inline] impl<T> core::ops::$op<i64> for Time<T>{
fn div(self,rhs:i64)->Self::Output{ type Output=Self;
Self::raw(self.0/rhs) #[inline]
} fn $method(self,rhs:i64)->Self::Output{
} Self::raw(self.0.$method(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)
} }
} }
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{ impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>; type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time<T>)->Self::Output{ fn mul(self,rhs:Time<T>)->Self::Output{
@@ -155,7 +156,7 @@ impl<T> core::ops::Mul<Time<T>> for Planar64{
#[cfg(test)] #[cfg(test)]
mod test_time{ mod test_time{
use super::*; use super::*;
type Time=super::AbsoluteTime; type Time=AbsoluteTime;
#[test] #[test]
fn time_from_planar64(){ fn time_from_planar64(){
let a:Time=Planar64::from(1).into(); let a:Time=Planar64::from(1).into();
@@ -551,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 Planar64=fixed_wide::types::I32F32;
pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>; pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>;
pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>; pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>;
@@ -560,12 +561,6 @@ pub mod vec3{
pub use linear_ops::types::Vector3; pub use linear_ops::types::Vector3;
pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]); pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;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 ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]); 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 Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]); pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
@@ -574,6 +569,10 @@ pub mod vec3{
pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]); 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_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]); 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] #[inline]
pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{ 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)]) Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
@@ -656,13 +655,13 @@ pub mod mat3{
} }
} }
#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{ pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1 pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3, pub translation:Planar64Vec3,
} }
impl Planar64Affine3{ impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO); pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::zero());
#[inline] #[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{ pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation} Self{matrix3,translation}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::integer::{Planar64Vec3,Planar64Affine3}; use crate::integer::{Planar64,Planar64Vec3,Planar64Affine3};
use crate::gameplay_attributes; use crate::gameplay_attributes;
pub type TextureCoordinate=glam::Vec2; pub type TextureCoordinate=glam::Vec2;
@@ -168,6 +168,11 @@ impl MeshBuilder{
} }
} }
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{ 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(||{ *self.pos_id_from.entry(pos).or_insert_with(||{
let pos_id=PositionId::new(self.unique_pos.len() as u32); let pos_id=PositionId::new(self.unique_pos.len() as u32);
self.unique_pos.push(pos); self.unique_pos.push(pos);

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_deferred_loader" name = "strafesnet_deferred_loader"
version = "0.5.0" version = "0.5.1"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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::loader::Loader;
use crate::mesh::Meshes; use crate::mesh::Meshes;
use crate::texture::{RenderConfigs,Texture}; 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)] #[derive(Clone,Copy,Debug)]
pub enum LoadFailureMode{ pub enum LoadFailureMode{
@@ -93,7 +93,7 @@ impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn into_indices(self)->impl Iterator<Item=H>{ pub fn into_indices(self)->impl Iterator<Item=H>{
self.mesh_id_from_asset_id.into_keys() self.mesh_id_from_asset_id.into_keys()
} }
pub fn into_meshes<'a,L:Loader<Resource=Mesh,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{ pub fn into_meshes<'a,M:Clone,L:Loader<Resource=M,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes<M>,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()]; let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{ for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index); let resource_result=loader.load(index);

View File

@@ -4,5 +4,5 @@ pub trait Loader{
type Error:Error; type Error:Error;
type Index<'a> where Self:'a; type Index<'a> where Self:'a;
type Resource; type Resource;
fn load<'a>(&mut self,index:Self::Index<'a>)->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{ pub struct Meshes<M>{
meshes:Vec<Option<Mesh>>, meshes:Vec<Option<M>>,
} }
impl Meshes{ impl<M> Meshes<M>{
pub(crate) const fn new(meshes:Vec<Option<Mesh>>)->Self{ pub(crate) const fn new(meshes:Vec<Option<M>>)->Self{
Self{ Self{
meshes, 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)| self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh)) maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
) )

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "fixed_wide" name = "fixed_wide"
version = "0.2.0" version = "0.2.1"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -18,3 +18,6 @@ bnum = "0.13.0"
arrayvec = { version = "0.7.6", optional = true } arrayvec = { version = "0.7.6", optional = true }
paste = "1.0.15" paste = "1.0.15"
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true } 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}; 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) /// 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 /// N is the number of u64s to use
/// F is the number of fractional bits (always N*32 lol) /// F is the number of fractional bits (always N*32 lol)
@@ -68,6 +70,34 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
pub const fn midpoint(self,other:Self)->Self{ pub const fn midpoint(self,other:Self)->Self{
Self::from_bits(self.bits.midpoint(other.bits)) 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>{ impl<const F:usize> Fixed<1,F>{
/// My old code called this function everywhere so let's provide it /// My old code called this function everywhere so let's provide it
@@ -99,28 +129,6 @@ impl_from!(
i8,i16,i32,i64,i128,isize 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>{ impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self; type Output=Self;
#[inline] #[inline]
@@ -233,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 { macro_rules! impl_from_float {
( $decode:ident, $input: ty, $mantissa_bits:expr ) => { ( $decode:ident, $input: ty, $mantissa_bits:expr ) => {
impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{ impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{
@@ -281,6 +294,23 @@ macro_rules! impl_from_float {
impl_from_float!(integer_decode_f32,f32,24); impl_from_float!(integer_decode_f32,f32,24);
impl_from_float!(integer_decode_f64,f64,53); 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>{ impl<const N:usize,const F:usize> core::fmt::Display for Fixed<N,F>{
#[inline] #[inline]
fn fmt(&self,f:&mut core::fmt::Formatter)->Result<(),core::fmt::Error>{ fn fmt(&self,f:&mut core::fmt::Formatter)->Result<(),core::fmt::Error>{
@@ -304,16 +334,6 @@ macro_rules! impl_additive_operator {
self.$method(other) 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 { macro_rules! impl_additive_assign_operator {
@@ -324,15 +344,6 @@ macro_rules! impl_additive_assign_operator {
self.bits.$method(other.bits); 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));
}
}
}; };
} }
@@ -355,7 +366,7 @@ impl_additive_operator!( Fixed, BitXor, bitxor, Self );
// non-wide operators. The result is the same width as the inputs. // non-wide operators. The result is the same width as the inputs.
// This macro is not used in the default configuration. // This macro is not used in the default configuration.
#[allow(unused_macros)] #[expect(unused_macros)]
macro_rules! impl_multiplicative_operator_not_const_generic { macro_rules! impl_multiplicative_operator_not_const_generic {
( ($struct: ident, $trait: ident, $method: ident, $output: ty ), $width:expr ) => { ( ($struct: ident, $trait: ident, $method: ident, $output: ty ), $width:expr ) => {
impl<const F:usize> core::ops::$trait for $struct<$width,F>{ impl<const F:usize> core::ops::$trait for $struct<$width,F>{
@@ -534,7 +545,7 @@ impl_shift_operator!( Fixed, Shr, shr, Self );
// wide operators. The result width is the sum of the input widths, i.e. none of the multiplication // wide operators. The result width is the sum of the input widths, i.e. none of the multiplication
#[allow(unused_macros)] #[expect(unused_macros)]
macro_rules! impl_wide_operators{ macro_rules! impl_wide_operators{
($lhs:expr,$rhs:expr)=>{ ($lhs:expr,$rhs:expr)=>{
impl core::ops::Mul<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{ impl core::ops::Mul<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{

View File

@@ -229,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

@@ -20,3 +20,6 @@ paste = { version = "1.0.15", optional = true }
[dev-dependencies] [dev-dependencies]
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["wide-mul"] } fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["wide-mul"] }
[lints]
workspace = true

View File

@@ -204,13 +204,21 @@ macro_rules! impl_matrix_named_fields_shape {
type Target=$struct_outer<Vector<$size_inner,T>>; type Target=$struct_outer<Vector<$size_inner,T>>;
#[inline] #[inline]
fn deref(&self)->&Self::Target{ 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>{ impl<T> core::ops::DerefMut for Matrix<$size_outer,$size_inner,T>{
#[inline] #[inline]
fn deref_mut(&mut self)->&mut Self::Target{ 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

@@ -330,13 +330,21 @@ macro_rules! impl_vector_named_fields {
type Target=$struct<T>; type Target=$struct<T>;
#[inline] #[inline]
fn deref(&self)->&Self::Target{ 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>{ impl<T> core::ops::DerefMut for Vector<$size,T>{
#[inline] #[inline]
fn deref_mut(&mut self)->&mut Self::Target{ 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

@@ -8,3 +8,6 @@ description = "Ratio operations using trait bounds for avoiding division like th
authors = ["Rhys Lloyd <krakow20@gmail.com>"] authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies] [dependencies]
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_rbx_loader" name = "strafesnet_rbx_loader"
version = "0.6.0" version = "0.7.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -12,13 +12,17 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies] [dependencies]
bytemuck = "1.14.3" bytemuck = "1.14.3"
glam = "0.30.0" glam = "0.30.0"
lazy-regex = "3.1.0" regex = { version = "1.11.3", default-features = false }
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" } rbx_mesh = "0.5.0"
rbx_dom_weak = { version = "3.1.0-sn2", registry = "strafesnet", features = ["instance-userdata"] }
rbx_mesh = "0.3.1"
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" } rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" } roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" } strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", 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 std::io::Read;
use rbx_dom_weak::WeakDom; use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_common::map::CompleteMap;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader}; use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use error::RecoverableErrors;
pub use roblox_emulator::runner::Error as RunnerError;
mod rbx; mod rbx;
mod mesh; mod mesh;
mod error;
mod union; mod union;
pub mod loader; pub mod loader;
mod primitives; pub mod primitives;
pub mod data{ pub mod data{
pub struct RobloxMeshBytes(Vec<u8>); pub struct RobloxMeshBytes(Vec<u8>);
@@ -27,13 +33,7 @@ impl Model{
fn new(dom:WeakDom)->Self{ fn new(dom:WeakDom)->Self{
Self{dom} Self{dom}
} }
pub fn into_place(self)->Place{ pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
let Self{mut dom}=self;
let context=roblox_emulator::context::Context::from_mut(&mut dom);
let services=context.convert_into_place();
Place{dom,services}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode) to_snf(self,failure_mode)
} }
} }
@@ -44,36 +44,43 @@ impl AsRef<WeakDom> for Model{
} }
pub struct Place{ pub struct Place{
dom:WeakDom, context:Context,
services:roblox_emulator::context::Services,
} }
impl Place{ impl Place{
pub fn new(dom:WeakDom)->Option<Self>{ pub fn new(dom:WeakDom)->Result<Self,roblox_emulator::context::ServicesError>{
let context=roblox_emulator::context::Context::from_ref(&dom); let context=Context::from_place(dom)?;
Some(Self{ Ok(Self{
services:context.find_services()?, context,
dom,
}) })
} }
pub fn run_scripts(&mut self){ pub fn run_scripts(&mut self)->Result<Vec<RunnerError>,RunnerError>{
let Place{dom,services}=self; let Place{context}=self;
let runner=roblox_emulator::runner::Runner::new().unwrap(); let runner=roblox_emulator::runner::Runner::new()?;
let context=roblox_emulator::context::Context::from_mut(dom);
let scripts=context.scripts(); 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{ for script in scripts{
if let Err(e)=runnable.run_script(script){ 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) to_snf(self,failure_mode)
} }
} }
impl AsRef<WeakDom> for Place{ impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{ 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>{ pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
let mut buf=std::io::BufReader::new(input); let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?; let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match &peek[0..8]{ match peek.get(0..8){
b"<roblox!"=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary), Some(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), Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
_=>Err(ReadError::UnknownFileFormat), _=>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 dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new(); 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 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 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_step2.add_render_configs_and_textures(render_configs))
Ok(map)
} }

View File

@@ -1,5 +1,4 @@
use std::io::Read; use std::io::Read;
use rbx_dom_weak::ustr;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr}; use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh; use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture}; use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
@@ -7,6 +6,11 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes; use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription; 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>{ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{
let mut file=std::fs::File::open(path)?; let mut file=std::fs::File::open(path)?;
let mut data=Vec::new(); let mut data=Vec::new();
@@ -14,7 +18,6 @@ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::E
Ok(data) Ok(data)
} }
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum TextureError{ pub enum TextureError{
Io(std::io::Error), Io(std::io::Error),
@@ -47,7 +50,7 @@ impl Loader for TextureLoader{
type Error=TextureError; type Error=TextureError;
type Index<'a>=&'a str; type Index<'a>=&'a str;
type Resource=Texture; type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?; let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id); let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?; let data=read_entire_file(file_name)?;
@@ -55,7 +58,6 @@ impl Loader for TextureLoader{
} }
} }
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum MeshError{ pub enum MeshError{
Io(std::io::Error), Io(std::io::Error),
@@ -114,7 +116,7 @@ pub struct MeshIndex<'a>{
content:&'a str, content:&'a str,
} }
impl MeshIndex<'_>{ impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex{ pub fn file_mesh(content:&str)->MeshIndex<'_>{
MeshIndex{ MeshIndex{
mesh_type:MeshType::FileMesh, mesh_type:MeshType::FileMesh,
content, content,
@@ -139,6 +141,12 @@ impl MeshIndex<'_>{
} }
} }
#[derive(Clone)]
pub struct MeshWithSize{
pub(crate) mesh:Mesh,
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
}
pub struct MeshLoader; pub struct MeshLoader;
impl MeshLoader{ impl MeshLoader{
pub fn new()->Self{ pub fn new()->Self{
@@ -148,8 +156,8 @@ impl MeshLoader{
impl Loader for MeshLoader{ impl Loader for MeshLoader{
type Error=MeshError; type Error=MeshError;
type Index<'a>=MeshIndex<'a>; type Index<'a>=MeshIndex<'a>;
type Resource=Mesh; type Resource=MeshWithSize;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{ let mesh=match index.mesh_type{
MeshType::FileMesh=>{ MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?; let RobloxAssetId(asset_id)=index.content.parse()?;
@@ -172,12 +180,12 @@ impl Loader for MeshLoader{
return Err(MeshError::MissingInstance); return Err(MeshError::MissingInstance);
}; };
if physics_data.is_empty(){ if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("PhysicsData")){ if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref(); physics_data=data.as_ref();
} }
} }
if mesh_data.is_empty(){ if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("MeshData")){ if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref(); mesh_data=data.as_ref();
} }
} }

View File

@@ -1,12 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated}; 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)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RbxMesh(rbx_mesh::mesh::Error) RbxMesh(rbx_mesh::mesh::Error)
} }
impl std::fmt::Display for Error{ impl std::fmt::Display for Error{
@@ -16,69 +18,46 @@ impl std::fmt::Display for Error{
} }
impl std::error::Error for Error{} impl std::error::Error for Error{}
fn ingest_vertices2< fn ingest_vertices2(
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireColorId,
AcquireVertexId,
>(
vertices:Vec<Vertex2>, vertices:Vec<Vertex2>,
acquire_pos_id:&mut AcquirePosId, mb:&mut model::MeshBuilder,
acquire_tex_id:&mut AcquireTexId, )->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
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,
{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index //this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously //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), rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{ {
pos:acquire_pos_id(vertex.pos)?, let vertex=IndexedVertex{
tex:acquire_tex_id(vertex.tex), pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
normal:acquire_normal_id(vertex.norm)?, tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
color:acquire_color_id(vertex.color.map(|f|f as f32/255.0f32)) 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))),
))).collect::<Result<_,_>>()?) };
mb.acquire_vertex_id(vertex)
}
))).collect()
} }
fn ingest_vertices_truncated2< fn ingest_vertices_truncated2(
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireVertexId,
>(
vertices:Vec<Vertex2Truncated>, vertices:Vec<Vertex2Truncated>,
acquire_pos_id:&mut AcquirePosId, mb:&mut model::MeshBuilder,
acquire_tex_id:&mut AcquireTexId,
acquire_normal_id:&mut AcquireNormalId,
static_color_id:ColorId,//pick one color and fill everything with it static_color_id:ColorId,//pick one color and fill everything with it
acquire_vertex_id:&mut AcquireVertexId, )->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
)->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,
{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index //this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously //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), rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{ {
pos:acquire_pos_id(vertex.pos)?, let vertex=IndexedVertex{
tex:acquire_tex_id(vertex.tex), pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
normal:acquire_normal_id(vertex.norm)?, tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
color:static_color_id normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
}), color:static_color_id,
))).collect::<Result<_,_>>()?) };
mb.acquire_vertex_id(vertex)
}
))).collect()
} }
fn ingest_faces2_lods3( fn ingest_faces2_lods3(
@@ -89,129 +68,80 @@ fn ingest_faces2_lods3(
){ ){
//faces have to be split into polygon groups based on lod //faces have to be split into polygon groups based on lod
polygon_groups.extend(lods.windows(2).map(|lod_pair| 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(|rbx_mesh::mesh::Face2(v0,v1,v2)| 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)|
vec![vertex_id_map[&v0],vertex_id_map[&v1],vertex_id_map[&v2]] Some(vec![*vertex_id_map.get(&v0)?,*vertex_id_map.get(&v1)?,*vertex_id_map.get(&v2)?])
).collect())) ).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 //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 polygon_groups=Vec::new();
let mut acquire_pos_id=|pos|{ let mut mb=model::MeshBuilder::new();
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)
};
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{ match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
rbx_mesh::mesh::VersionedMesh::Version1(mesh)=>{ rbx_mesh::mesh::Mesh::V1(mesh)=>{
let color_id=acquire_color_id([1.0f32;4]); let color_id=mb.acquire_color_id(glam::Vec4::ONE);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).filter_map(|trip|{
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|Ok(acquire_vertex_id(IndexedVertex{ let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|{
pos:acquire_pos_id(vertex.pos)?, let vertex=IndexedVertex{
tex:acquire_tex_id([vertex.tex[0],vertex.tex[1]]), pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
normal:acquire_normal_id(vertex.norm)?, tex:mb.acquire_tex_id(glam::vec2(vertex.tex[0],vertex.tex[1])),
color:color_id, normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
})); color:color_id,
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?]) };
}).collect::<Result<_,_>>()?))); 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{ let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
//pick white and make all the vertices white //pick white and make all the vertices white
let color_id=acquire_color_id([1.0f32;4]); let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id) 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 //one big happy group for all the faces
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face| polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().filter_map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]] Some(vec![*vertex_id_map.get(&face.0)?,*vertex_id_map.get(&face.1)?,*vertex_id_map.get(&face.2)?])
).collect()))); ).collect())));
}, },
rbx_mesh::mesh::VersionedMesh::Version3(mesh)=>{ rbx_mesh::mesh::Mesh::V3(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{ let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
let color_id=acquire_color_id([1.0f32;4]); let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id) 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); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
rbx_mesh::mesh::VersionedMesh::Version4(mesh)=>{ rbx_mesh::mesh::Mesh::V4(mesh)=>{
let vertex_id_map=ingest_vertices2( let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
rbx_mesh::mesh::VersionedMesh::Version5(mesh)=>{ rbx_mesh::mesh::Mesh::V5(mesh)=>{
let vertex_id_map=ingest_vertices2( let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
} }
Ok(model::Mesh{ let mesh=mb.build(
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
polygon_groups, polygon_groups,
//these should probably be moved to the model... //these should probably be moved to the model...
//but what if models want to use the same texture //but what if models want to use the same texture
graphics_groups:vec![model::IndexedGraphicsGroup{ vec![model::IndexedGraphicsGroup{
render:RenderConfigId::new(0), render:RenderConfigId::new(0),
//the lowest lod is highest quality //the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)] groups:vec![model::PolygonGroupId::new(0)]
}], }],
//disable physics //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

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

View File

@@ -1,73 +1,76 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::loader::MeshIndex; use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
use crate::loader::{MeshWithSize,MeshIndex};
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives}; use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
use rbx_dom_weak::ustr;
use strafesnet_common::aabb::Aabb;
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_common::model; use strafesnet_common::model;
use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone}; use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone};
use strafesnet_common::gameplay_style; use strafesnet_common::gameplay_style;
use strafesnet_common::gameplay_attributes as attr; use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3}; use strafesnet_common::integer::{self,vec3,Planar64TryFromFloatError,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId; use strafesnet_common::model::RenderConfigId;
use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader}; use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes; use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture}; use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
fn class_is_a(class: &str, superclass: &str) -> bool { // disallow non-static lifetimes
if class==superclass { fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
return true rbx_dom_weak::ustr(s)
}
let class_descriptor=rbx_reflection_database::get().classes.get(class);
if let Some(descriptor) = &class_descriptor {
if let Some(class_super) = &descriptor.superclass {
return class_is_a(&class_super, superclass)
}
}
false
}
fn recursive_collect_superclass(objects: &mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom: &rbx_dom_weak::WeakDom, instance: &rbx_dom_weak::Instance, superclass: &str){
let mut stack=vec![instance];
while let Some(item)=stack.pop(){
for &referent in item.children(){
if let Some(c)=dom.get_by_ref(referent){
if class_is_a(c.class.as_str(),superclass){
objects.push(c.referent());//copy ref
}
stack.push(c);
}
}
}
}
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{
Planar64Affine3::new(
Planar64Mat3::from_cols([
vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x]).unwrap()
*integer::try_from_f32(size.x/2.0).unwrap(),
vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y]).unwrap()
*integer::try_from_f32(size.y/2.0).unwrap(),
vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z]).unwrap()
*integer::try_from_f32(size.z/2.0).unwrap(),
].map(|t|t.narrow_1().unwrap())),
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z]).unwrap()
)
} }
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->attr::CollisionAttributes{ macro_rules! lazy_regex{
($r:literal)=>{{
use regex::Regex;
use std::sync::LazyLock;
static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
&RE
}};
}
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
Ok(Planar64Affine3::new(
Planar64Mat3::from_cols([
(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
]),
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
))
}
enum GetAttributesError{
ModeIdParseInt(ParseIntContext),
DuplicateMode(ModeId),
StageIdParseInt(ParseIntContext),
DuplicateStage(DuplicateStageError),
WormholeOutIdParseInt(ParseIntContext),
DuplicateWormholeOut(u32),
WormholeInIdParseInt(ParseIntContext),
JumpLimitParseInt(ParseIntContext),
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->Result<attr::CollisionAttributes,GetAttributesError>{
let mut general=attr::GeneralAttributes::default(); let mut general=attr::GeneralAttributes::default();
let mut intersecting=attr::IntersectingAttributes::default(); let mut intersecting=attr::IntersectingAttributes::default();
let mut contacting=attr::ContactingAttributes::default(); let mut contacting=attr::ContactingAttributes::default();
let mut force_can_collide=can_collide; let mut force_can_collide=can_collide;
let mut force_intersecting=false; let mut force_intersecting=false;
let mut allow_booster=true;
match name{ match name{
"Water"=>{ "Water"=>{
force_can_collide=false; force_can_collide=false;
allow_booster=false;
//TODO: read stupid CustomPhysicalProperties //TODO: read stupid CustomPhysicalProperties
intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity}); intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity});
}, },
"Accelerator"=>{ "Accelerator"=>{
//although the new game supports collidable accelerators, this is a roblox compatability map loader //although the new game supports collidable accelerators, this is a roblox compatability map loader
force_can_collide=false; force_can_collide=false;
// Accelerator is not allowed to be booster in roblox
allow_booster=false;
general.accelerator=Some(attr::Accelerator{acceleration:velocity}); general.accelerator=Some(attr::Accelerator{acceleration:velocity});
}, },
// "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{ // "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{
@@ -76,17 +79,21 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
// force:false, // force:false,
// behaviour:model::StageElementBehaviour::Unordered // behaviour:model::StageElementBehaviour::Unordered
// })), // })),
"SetVelocity"=>general.trajectory=Some(attr::SetTrajectory::Velocity(velocity)), "SetVelocity"=>{
allow_booster=false;
general.trajectory=Some(attr::SetTrajectory::Velocity(velocity));
},
"MapStart"=>{ "MapStart"=>{
force_can_collide=false; force_can_collide=false;
force_intersecting=true; force_intersecting=true;
let mode_id=ModeId::MAIN;
modes_builder.insert_mode( modes_builder.insert_mode(
ModeId::MAIN, mode_id,
Mode::empty( Mode::empty(
gameplay_style::StyleModifiers::roblox_bhop(), gameplay_style::StyleModifiers::roblox_bhop(),
model_id model_id
) )
); ).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
}, },
"MapFinish"=>{ "MapFinish"=>{
force_can_collide=false; force_can_collide=false;
@@ -120,32 +127,36 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
); );
}, },
other=>{ other=>{
let regman=lazy_regex::regex!(r"^(BonusStart|WormholeOut)(\d+)$"); let regman=lazy_regex!(r"^(BonusStart|WormholeOut)(\d+)$");
if let Some(captures)=regman.captures(other){ if let Some(captures)=regman.captures(other){
match &captures[1]{ match &captures[1]{
"BonusStart"=>{ "BonusStart"=>{
force_can_collide=false; force_can_collide=false;
force_intersecting=true; force_intersecting=true;
let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
modes_builder.insert_mode( modes_builder.insert_mode(
ModeId::new(captures[2].parse::<u32>().unwrap()), mode_id,
Mode::empty( Mode::empty(
gameplay_style::StyleModifiers::roblox_bhop(), gameplay_style::StyleModifiers::roblox_bhop(),
model_id model_id
) )
); ).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
}, },
"WormholeOut"=>{ "WormholeOut"=>{
//the PhysicsModelId has to exist for it to be teleported to! //the PhysicsModelId has to exist for it to be teleported to!
force_intersecting=true; force_intersecting=true;
//this object is not special in strafe client, but the roblox mapping needs to be converted to model id //this object is not special in strafe client, but the roblox mapping needs to be converted to model id
assert!(wormhole_id_to_out_model.insert(captures[2].parse::<u32>().unwrap(),model_id).is_none(),"Cannot have multiple WormholeOut with same id"); let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeOutIdParseInt)?;
if wormhole_id_to_out_model.insert(wormhole_id,model_id).is_some(){
return Err(GetAttributesError::DuplicateWormholeOut(wormhole_id));
}
}, },
_=>(), _=>(),
} }
}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$") }else if let Some(captures)=lazy_regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
.captures(other){ .captures(other){
force_intersecting=true; force_intersecting=true;
let stage_id=StageId::new(captures[3].parse::<u32>().unwrap()); let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
let stage_element=StageElement::new( let stage_element=StageElement::new(
//stage_id: //stage_id:
stage_id, stage_id,
@@ -157,11 +168,12 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
//behaviour: //behaviour:
match &captures[2]{ match &captures[2]{
"Spawn"=>{ "Spawn"=>{
let mode_id=ModeId::MAIN;
modes_builder.insert_stage( modes_builder.insert_stage(
ModeId::MAIN, mode_id,
stage_id, stage_id,
Stage::empty(model_id), Stage::empty(model_id),
); ).map_err(|_|GetAttributesError::DuplicateStage(DuplicateStageError{mode_id,stage_id}))?;
//TODO: let denormalize handle this //TODO: let denormalize handle this
StageElementBehaviour::SpawnAt StageElementBehaviour::SpawnAt
}, },
@@ -171,7 +183,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
"Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger}, "Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger},
"Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport}, "Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport},
"Platform"=>StageElementBehaviour::Platform, "Platform"=>StageElementBehaviour::Platform,
_=>panic!("regex1[2] messed up bad"), _=>unreachable!("regex1[2] messed up bad"),
}, },
None None
); );
@@ -182,7 +194,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
stage_element, stage_element,
), ),
); );
}else if let Some(captures)=lazy_regex::regex!(r"^(Jump|WormholeIn)(\d+)$") }else if let Some(captures)=lazy_regex!(r"^(Jump|WormholeIn)(\d+)$")
.captures(other){ .captures(other){
match &captures[1]{ match &captures[1]{
"Jump"=>modes_builder.push_mode_update( "Jump"=>modes_builder.push_mode_update(
@@ -194,30 +206,33 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
StageId::FIRST, StageId::FIRST,
false, false,
StageElementBehaviour::Check, StageElementBehaviour::Check,
Some(captures[2].parse::<u8>().unwrap()) Some(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::JumpLimitParseInt)?)
) )
), ),
), ),
"WormholeIn"=>{ "WormholeIn"=>{
force_can_collide=false; force_can_collide=false;
force_intersecting=true; force_intersecting=true;
assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::<u32>().unwrap()).is_none(),"Impossible"); let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeInIdParseInt)?;
// It is impossible for two different objects to have the same model id
assert!(wormhole_in_model_to_id.insert(model_id,wormhole_id).is_none(),"Impossible");
}, },
_=>panic!("regex2[1] messed up bad"), _=>unreachable!("regex2[1] messed up bad"),
} }
}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$") }else if let Some(captures)=lazy_regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
.captures(other){ .captures(other){
force_can_collide=false; force_can_collide=false;
force_intersecting=true; force_intersecting=true;
let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
modes_builder.push_mode_update( modes_builder.push_mode_update(
ModeId::new(captures[2].parse::<u32>().unwrap()), mode_id,
ModeUpdate::zone( ModeUpdate::zone(
model_id, model_id,
//zone: //zone:
match &captures[1]{ match &captures[1]{
"Finish"=>Zone::Finish, "Finish"=>Zone::Finish,
"Anticheat"=>Zone::Anticheat, "Anticheat"=>Zone::Anticheat,
_=>panic!("regex3[1] messed up bad"), _=>unreachable!("regex3[1] messed up bad"),
}, },
), ),
); );
@@ -236,10 +251,10 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
} }
} }
//need some way to skip this //need some way to skip this
if velocity!=vec3::ZERO{ if allow_booster&&velocity!=vec3::zero(){
general.booster=Some(attr::Booster::Velocity(velocity)); general.booster=Some(attr::Booster::Velocity(velocity));
} }
match force_can_collide{ Ok(match force_can_collide{
true=>{ true=>{
match name{ match name{
"Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)), "Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)),
@@ -257,7 +272,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}else{ }else{
attr::CollisionAttributes::Decoration attr::CollisionAttributes::Decoration
}, },
} })
} }
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
@@ -329,12 +344,12 @@ pub struct RobloxFaceTextureDescription{
pub color:glam::Vec4, pub color:glam::Vec4,
pub transform:RobloxTextureTransform, pub transform:RobloxTextureTransform,
} }
impl core::cmp::PartialEq for RobloxFaceTextureDescription{ impl PartialEq for RobloxFaceTextureDescription{
fn eq(&self,other:&Self)->bool{ fn eq(&self,other:&Self)->bool{
self.to_bits().eq(&other.to_bits()) self.to_bits().eq(&other.to_bits())
} }
} }
impl core::cmp::Eq for RobloxFaceTextureDescription{} impl Eq for RobloxFaceTextureDescription{}
impl core::hash::Hash for RobloxFaceTextureDescription{ impl core::hash::Hash for RobloxFaceTextureDescription{
fn hash<H:core::hash::Hasher>(&self,state:&mut H){ fn hash<H:core::hash::Hasher>(&self,state:&mut H){
self.to_bits().hash(state); self.to_bits().hash(state);
@@ -403,39 +418,46 @@ fn get_content_url(content:&rbx_dom_weak::types::Content)->Option<&str>{
} }
fn get_texture_description<'a>( fn get_texture_description<'a>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>, render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
recoverable_errors:&mut RecoverableErrors,
db:&rbx_reflection::ReflectionDatabase,
dom:&'a rbx_dom_weak::WeakDom, dom:&'a rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance, object:&rbx_dom_weak::Instance,
size:&rbx_dom_weak::types::Vector3, size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription{ )->RobloxPartDescription{
//use the biggest one and cut it down later... //use the biggest one and cut it down later...
let mut part_texture_description=RobloxPartDescription::default(); let mut part_texture_description=RobloxPartDescription::default();
temp_objects.clear(); let decal=&db.classes["Decal"];
recursive_collect_superclass(temp_objects,&dom,object,"Decal"); let decals=object.children().iter().filter_map(|&referent|{
for &mut decal_ref in temp_objects{ let instance=dom.get_by_ref(referent)?;
let Some(decal)=dom.get_by_ref(decal_ref) else{ db.classes.get(instance.class.as_str()).is_some_and(|class|
println!("Decal get_by_ref failed"); db.has_superclass(class,decal)
continue; ).then_some(instance)
}; });
for decal in decals{
// decals should always have these properties,
// but it is not guaranteed by the rbx_dom_weak data structure.
let ( let (
Some(rbx_dom_weak::types::Variant::ContentId(content)), Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)), Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)), Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)), Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
)=( )=(
decal.properties.get(&ustr("Texture")), decal.properties.get(&static_ustr("TextureContent")),
decal.properties.get(&ustr("Face")), decal.properties.get(&static_ustr("Face")),
decal.properties.get(&ustr("Color3")), decal.properties.get(&static_ustr("Color3")),
decal.properties.get(&ustr("Transparency")), decal.properties.get(&static_ustr("Transparency")),
)else{ )else{
println!("Decal is missing a required property"); recoverable_errors.decal_property.push(InstancePath::new(dom,decal));
continue; continue;
}; };
let texture_id=Some(content.as_str()); let texture_id=get_content_url(content);
let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id); let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id);
let Ok(cube_face)=normalid.to_u32().try_into()else{ let Ok(cube_face)=normalid.to_u32().try_into()else{
println!("NormalId is invalid"); recoverable_errors.normal_id.push(NormalIdError{
path:InstancePath::new(dom,decal),
normal_id:normalid.to_u32(),
});
continue; continue;
}; };
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
@@ -446,10 +468,10 @@ fn get_texture_description<'a>(
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)), Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)), Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
) = ( ) = (
decal.properties.get(&ustr("OffsetStudsU")), decal.properties.get(&static_ustr("OffsetStudsU")),
decal.properties.get(&ustr("OffsetStudsV")), decal.properties.get(&static_ustr("OffsetStudsV")),
decal.properties.get(&ustr("StudsPerTileU")), decal.properties.get(&static_ustr("StudsPerTileU")),
decal.properties.get(&ustr("StudsPerTileV")), decal.properties.get(&static_ustr("StudsPerTileV")),
) )
{ {
let (size_u,size_v)=match cube_face{ let (size_u,size_v)=match cube_face{
@@ -472,6 +494,7 @@ fn get_texture_description<'a>(
} }
) )
}else{ }else{
recoverable_errors.texture_property.push(InstancePath::new(dom,decal));
(glam::Vec4::ONE,RobloxTextureTransform::identity()) (glam::Vec4::ONE,RobloxTextureTransform::identity())
} }
}else{ }else{
@@ -525,6 +548,8 @@ pub fn convert<'a>(
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>, render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>, mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
)->PartialMap1<'a>{ )->PartialMap1<'a>{
let mut recoverable_errors=RecoverableErrors::default();
let mut deferred_models_deferred_attributes=Vec::new(); let mut deferred_models_deferred_attributes=Vec::new();
let mut deferred_unions_deferred_attributes=Vec::new(); let mut deferred_unions_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new(); let mut primitive_models_deferred_attributes=Vec::new();
@@ -534,63 +559,83 @@ pub fn convert<'a>(
//just going to leave it like this for now instead of reworking the data structures for this whole thing //just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None); let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let mut object_refs=Vec::new(); let db=rbx_reflection_database::get().unwrap();
let mut temp_objects=Vec::new(); let basepart=&db.classes["BasePart"];
recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart"); let baseparts=dom.descendants().filter(|&instance|
for object_ref in object_refs { db.classes.get(instance.class.as_str()).is_some_and(|class|
if let Some(object)=dom.get_by_ref(object_ref){ db.has_superclass(class,basepart)
if let ( )
Some(rbx_dom_weak::types::Variant::CFrame(cf)), );
Some(rbx_dom_weak::types::Variant::Vector3(size)), for object in baseparts{
Some(rbx_dom_weak::types::Variant::Vector3(velocity)), let (
Some(rbx_dom_weak::types::Variant::Float32(transparency)), Some(rbx_dom_weak::types::Variant::CFrame(cf)),
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)), Some(rbx_dom_weak::types::Variant::Vector3(size)),
Some(rbx_dom_weak::types::Variant::Bool(can_collide)), Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
) = ( Some(rbx_dom_weak::types::Variant::Float32(transparency)),
object.properties.get(&ustr("CFrame")), Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
object.properties.get(&ustr("Size")), Some(&rbx_dom_weak::types::Variant::Bool(can_collide)),
object.properties.get(&ustr("Velocity")), ) = (
object.properties.get(&ustr("Transparency")), object.properties.get(&static_ustr("CFrame")),
object.properties.get(&ustr("Color")), object.properties.get(&static_ustr("Size")),
object.properties.get(&ustr("CanCollide")), object.properties.get(&static_ustr("Velocity")),
) object.properties.get(&static_ustr("Transparency")),
{ object.properties.get(&static_ustr("Color")),
let model_transform=planar64_affine3_from_roblox(cf,size); object.properties.get(&static_ustr("CanCollide")),
)else{
recoverable_errors.basepart_property.push(InstancePath::new(dom,object));
continue;
};
let model_transform=match planar64_affine3_from_roblox(cf,size){
Ok(model_transform)=>{
if model_transform.matrix3.det().is_zero(){ if model_transform.matrix3.det().is_zero(){
let mut parent_ref=object.parent(); recoverable_errors.basepart_cframe.push(CFrameError{
let mut full_path=object.name.clone(); path:InstancePath::new(dom,object),
while let Some(parent)=dom.get_by_ref(parent_ref){ error:CFrameErrorType::ZeroDeterminant,
full_path=format!("{}.{}",parent.name,full_path); });
parent_ref=parent.parent();
}
println!("Zero determinant CFrame at location {}",full_path);
println!("matrix3:{}",model_transform.matrix3);
continue; continue;
} }
model_transform
},
Err(e)=>{
recoverable_errors.basepart_cframe.push(CFrameError{
path:InstancePath::new(dom,object),
error:CFrameErrorType::Convert(e),
});
continue;
}
};
//TODO: also detect "CylinderMesh" etc here //TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){ let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&ustr("Shape")){ "Part"|"Seat"|"SpawnLocation"=>{
Shape::Primitive(shape.to_u32().try_into().expect("Funky roblox PartType")) let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape"))else{
}else{ recoverable_errors.part_property.push(InstancePath::new(dom,object));
panic!("Part has no Shape!"); continue;
},
"TrussPart"=>Shape::Primitive(Primitives::Cube),
"WedgePart"=>Shape::Primitive(Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
_=>{
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
Shape::Primitive(Primitives::Cube)
}
}; };
let Ok(shape)=shape.to_u32().try_into()else{
recoverable_errors.part_shape.push(ShapeError{
path:InstancePath::new(dom,object),
shape:shape.to_u32(),
});
continue;
};
Shape::Primitive(shape)
},
"TrussPart"|"VehicleSeat"=>Shape::Primitive(Primitives::Cube),
"WedgePart"=>Shape::Primitive(Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
"Terrain"=>continue,
_=>{
recoverable_errors.unsupported_class.insert(object.class.as_str().to_owned());
Shape::Primitive(Primitives::Cube)
}
};
let (availability,mesh_id)=match shape{ let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{ Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
//obscure rust syntax "slice pattern" //obscure rust syntax "slice pattern"
let RobloxPartDescription([ let RobloxPartDescription([
f0,//Cube::Right f0,//Cube::Right
@@ -639,66 +684,83 @@ pub fn convert<'a>(
mesh_id mesh_id
}; };
(MeshAvailability::Immediate,mesh_id) (MeshAvailability::Immediate,mesh_id)
}, },
Shape::MeshPart=>if let ( Shape::MeshPart=>{
Some(rbx_dom_weak::types::Variant::Content(mesh_content)), let (
Some(rbx_dom_weak::types::Variant::Content(texture_content)), Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
)=( Some(rbx_dom_weak::types::Variant::Content(texture_content)),
// mesh must exist )=(
object.properties.get(&ustr("MeshContent")), // mesh must exist
// texture is allowed to be none object.properties.get(&static_ustr("MeshContent")),
object.properties.get(&ustr("TextureContent")), // texture is allowed to be none
){ object.properties.get(&static_ustr("TextureContent")),
let mesh_asset_id=get_content_url(mesh_content).expect("MeshPart Mesh is not a Uri"); )else{
let texture_asset_id=get_content_url(texture_content); recoverable_errors.meshpart_property.push(InstancePath::new(dom,object));
( continue;
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
)
}else{
panic!("Mesh has no Mesh or Texture");
},
Shape::PhysicsData=>{
let mut content="";
let mut mesh_data:&[u8]=&[];
let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&ustr("AssetId")){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("MeshData")){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("PhysicsData")){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
},
}; };
let model_deferred_attributes=ModelDeferredAttributes{ let mesh_asset_id=match get_content_url(mesh_content){
mesh:mesh_id, Some(mesh_asset_id)=>mesh_asset_id,
transform:model_transform, None=>{
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency), recoverable_errors.meshpart_content.push(InstancePath::new(dom,object));
deferred_attributes:GetAttributesArgs{ // Return an empty string which will fail to parse as an asset id
name:object.name.as_str(), ""
can_collide:*can_collide, }
velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
},
}; };
match availability{ let texture_asset_id=get_content_url(texture_content);
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes), (
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{ MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
render, mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
model:model_deferred_attributes )
}), },
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{ Shape::PhysicsData=>{
render:part_texture_description, let mut content="";
model:model_deferred_attributes, let mut mesh_data:&[u8]=&[];
}), let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
content=asset_id.as_ref();
} }
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
},
};
let velocity=match vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]){
Ok(velocity)=>velocity,
Err(e)=>{
recoverable_errors.basepart_velocity.push(Planar64ConvertError{
path:InstancePath::new(dom,object),
error:e,
});
continue;
} }
};
let model_deferred_attributes=ModelDeferredAttributes{
mesh:mesh_id,
transform:model_transform,
color:glam::vec4(color3.r as f32/255f32,color3.g as f32/255f32,color3.b as f32/255f32,1.0-*transparency),
deferred_attributes:GetAttributesArgs{
name:object.name.as_str(),
can_collide,
velocity,
},
};
match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render,
model:model_deferred_attributes
}),
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
}),
} }
} }
PartialMap1{ PartialMap1{
@@ -706,25 +768,26 @@ pub fn convert<'a>(
primitive_models_deferred_attributes, primitive_models_deferred_attributes,
deferred_models_deferred_attributes, deferred_models_deferred_attributes,
deferred_unions_deferred_attributes, deferred_unions_deferred_attributes,
recoverable_errors,
} }
} }
struct MeshWithAabb{ struct MeshIdWithSize{
mesh:model::Mesh, mesh:model::MeshId,
aabb:Aabb, size:Planar64Vec3,
} }
fn acquire_mesh_id_from_render_config_id<'a>( fn acquire_mesh_id_from_render_config_id(
primitive_meshes:&mut Vec<model::Mesh>, primitive_meshes:&mut Vec<model::Mesh>,
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>, mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>, loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
old_mesh_id:model::MeshId, old_mesh_id:model::MeshId,
render:RenderConfigId, render:RenderConfigId,
)->Option<(model::MeshId,&'a Aabb)>{ )->Option<MeshIdWithSize>{
//ignore meshes that fail to load completely for now //ignore meshes that fail to load completely for now
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|( loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new()) mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{ .entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32); let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone(); let mut mesh_clone=mesh.clone();
//set the render group lool //set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){ if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render; graphics_group.render=render;
@@ -732,22 +795,22 @@ fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes.push(mesh_clone); primitive_meshes.push(mesh_clone);
mesh_id mesh_id
}), }),
&mesh_with_aabb.aabb, size,
)) })
} }
fn acquire_union_id_from_render_config_id<'a>( fn acquire_union_id_from_render_config_id(
primitive_meshes:&mut Vec<model::Mesh>, primitive_meshes:&mut Vec<model::Mesh>,
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>, union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>, loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
old_union_id:model::MeshId, old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription, part_texture_description:RobloxPartDescription,
)->Option<(model::MeshId,&'a Aabb)>{ )->Option<MeshIdWithSize>{
//ignore uniones that fail to load completely for now //ignore uniones that fail to load completely for now
loaded_meshes.get(&old_union_id).map(|union_with_aabb|( loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new()) mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
.entry(part_texture_description.clone()).or_insert_with(||{ .entry(part_texture_description.clone()).or_insert_with(||{
let union_id=model::MeshId::new(primitive_meshes.len() as u32); let union_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut union_clone=union_with_aabb.mesh.clone(); let mut union_clone=mesh.clone();
//set the render groups //set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){ for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
if let Some(face_texture_description)=maybe_face_texture_description{ if let Some(face_texture_description)=maybe_face_texture_description{
@@ -757,19 +820,20 @@ fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes.push(union_clone); primitive_meshes.push(union_clone);
union_id union_id
}), }),
&union_with_aabb.aabb, size,
)) })
} }
pub struct PartialMap1<'a>{ pub struct PartialMap1<'a>{
primitive_meshes:Vec<model::Mesh>, primitive_meshes:Vec<model::Mesh>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>, primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>, deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>, deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
recoverable_errors:RecoverableErrors,
} }
impl PartialMap1<'_>{ impl PartialMap1<'_>{
pub fn add_meshpart_meshes_and_calculate_attributes( pub fn add_meshpart_meshes_and_calculate_attributes(
mut self, mut self,
meshpart_meshes:Meshes, meshpart_meshes:Meshes<MeshWithSize>,
)->PartialMap2{ )->PartialMap2{
//calculate attributes //calculate attributes
let mut modes_builder=ModesBuilder::default(); let mut modes_builder=ModesBuilder::default();
@@ -781,22 +845,14 @@ impl PartialMap1<'_>{
//decode roblox meshes //decode roblox meshes
//generate mesh_id_map based on meshes that failed to load //generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>= let loaded_meshes:HashMap<model::MeshId,MeshWithSize>=
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{ meshpart_meshes.consume().collect();
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
(old_mesh_id,MeshWithAabb{
mesh,
aabb,
})
}).collect();
// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way // SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
// I just want to chain iterators together man // I just want to chain iterators together man
let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes); let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);
let mut model_counter=0;
let mut mesh_id_from_render_config_id=HashMap::new(); let mut mesh_id_from_render_config_id=HashMap::new();
let mut union_id_from_render_config_id=HashMap::new(); let mut union_id_from_render_config_id=HashMap::new();
//now that the meshes are loaded, these models can be generated //now that the meshes are loaded, these models can be generated
@@ -804,23 +860,22 @@ impl PartialMap1<'_>{
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{ self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id //meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes //insert into primitive_meshes
let (mesh,aabb)=acquire_mesh_id_from_render_config_id( let MeshIdWithSize{mesh,size:mesh_size}=acquire_mesh_id_from_render_config_id(
unsafe{*aint_no_way.get()}, unsafe{*aint_no_way.get()},
&mut mesh_id_from_render_config_id, &mut mesh_id_from_render_config_id,
&loaded_meshes, &loaded_meshes,
deferred_model_deferred_attributes.model.mesh, deferred_model_deferred_attributes.model.mesh,
deferred_model_deferred_attributes.render deferred_model_deferred_attributes.render
)?; )?;
let size=aabb.size();
Some(ModelDeferredAttributes{ Some(ModelDeferredAttributes{
mesh, mesh,
deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes, deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
color:deferred_model_deferred_attributes.model.color, color:deferred_model_deferred_attributes.model.color,
transform:Planar64Affine3::new( transform:Planar64Affine3::new(
Planar64Mat3::from_cols([ Planar64Mat3::from_cols([
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(), (deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(), (deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(), (deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
]), ]),
deferred_model_deferred_attributes.model.transform.translation deferred_model_deferred_attributes.model.transform.translation
), ),
@@ -828,14 +883,13 @@ impl PartialMap1<'_>{
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{ }).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id //meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes //insert into primitive_meshes
let (mesh,aabb)=acquire_union_id_from_render_config_id( let MeshIdWithSize{mesh,size}=acquire_union_id_from_render_config_id(
unsafe{*aint_no_way.get()}, unsafe{*aint_no_way.get()},
&mut union_id_from_render_config_id, &mut union_id_from_render_config_id,
&loaded_meshes, &loaded_meshes,
deferred_union_deferred_attributes.model.mesh, deferred_union_deferred_attributes.model.mesh,
deferred_union_deferred_attributes.render deferred_union_deferred_attributes.render
)?; )?;
let size=aabb.size();
Some(ModelDeferredAttributes{ Some(ModelDeferredAttributes{
mesh, mesh,
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes, deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
@@ -851,61 +905,78 @@ impl PartialMap1<'_>{
}) })
})) }))
.chain(self.primitive_models_deferred_attributes.into_iter()) .chain(self.primitive_models_deferred_attributes.into_iter())
.enumerate().map(|(model_id,model_deferred_attributes)|{ .filter_map(|model_deferred_attributes|{
let model_id=model::ModelId::new(model_id as u32); let model_id=model::ModelId::new(model_counter);
ModelOwnedAttributes{ let attributes=match get_attributes(
&model_deferred_attributes.deferred_attributes.name,
model_deferred_attributes.deferred_attributes.can_collide,
model_deferred_attributes.deferred_attributes.velocity,
model_id,
&mut modes_builder,
&mut wormhole_in_model_to_id,
&mut wormhole_id_to_out_model,
){
Ok(attributes)=>attributes,
Err(e)=>{
match e{
GetAttributesError::ModeIdParseInt(e)=>self.recoverable_errors.mode_id_parse_int.push(e),
GetAttributesError::DuplicateMode(mode_id)=>{self.recoverable_errors.duplicate_mode.insert(mode_id);},
GetAttributesError::StageIdParseInt(e)=>self.recoverable_errors.stage_id_parse_int.push(e),
GetAttributesError::DuplicateStage(duplicate_stage)=>{self.recoverable_errors.duplicate_stage.insert(duplicate_stage);},
GetAttributesError::WormholeOutIdParseInt(e)=>self.recoverable_errors.wormhole_out_id_parse_int.push(e),
GetAttributesError::DuplicateWormholeOut(wormhole_id)=>{self.recoverable_errors.duplicate_wormhole_out.insert(wormhole_id);},
GetAttributesError::WormholeInIdParseInt(e)=>self.recoverable_errors.wormhole_in_id_parse_int.push(e),
GetAttributesError::JumpLimitParseInt(e)=>self.recoverable_errors.jump_limit_parse_int.push(e),
}
return None;
}
};
model_counter+=1;
Some(ModelOwnedAttributes{
mesh:model_deferred_attributes.mesh, mesh:model_deferred_attributes.mesh,
attributes:get_attributes( attributes,
&model_deferred_attributes.deferred_attributes.name,
model_deferred_attributes.deferred_attributes.can_collide,
model_deferred_attributes.deferred_attributes.velocity,
model_id,
&mut modes_builder,
&mut wormhole_in_model_to_id,
&mut wormhole_id_to_out_model,
),
color:model_deferred_attributes.color, color:model_deferred_attributes.color,
transform:model_deferred_attributes.transform, transform:model_deferred_attributes.transform,
} })
}).collect(); }).collect();
let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{ let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{
//TODO: TAB let model_id=model::ModelId::new(model_id as u32);
let model_id=model::ModelId::new(model_id as u32); //update attributes with wormhole id
//update attributes with wormhole id //TODO: errors/prints
//TODO: errors/prints if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){ if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){ match &mut model_owned_attributes.attributes{
match &mut model_owned_attributes.attributes{ attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general}) |attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general}) =>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}), attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
attr::CollisionAttributes::Decoration=>println!("Not a wormhole"), }
} }
} }
}
//index the attributes //index the attributes
let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){ let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
attributes_id attributes_id
}else{ }else{
let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32); let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id); attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
unique_attributes.push(model_owned_attributes.attributes); unique_attributes.push(model_owned_attributes.attributes);
attributes_id attributes_id
}; };
model::Model{ model::Model{
mesh:model_owned_attributes.mesh, mesh:model_owned_attributes.mesh,
transform:model_owned_attributes.transform, transform:model_owned_attributes.transform,
color:model_owned_attributes.color, color:model_owned_attributes.color,
attributes:attributes_id, attributes:attributes_id,
}
}).collect();
PartialMap2{
meshes:self.primitive_meshes,
models,
modes:modes_builder.build_normalized(),
attributes:unique_attributes,
recoverable_errors:self.recoverable_errors,
} }
}).collect();
PartialMap2{
meshes:self.primitive_meshes,
models,
modes:modes_builder.build_normalized(),
attributes:unique_attributes,
}
} }
} }
@@ -914,12 +985,13 @@ pub struct PartialMap2{
models:Vec<model::Model>, models:Vec<model::Model>,
modes:NormalizedModes, modes:NormalizedModes,
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>, attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
recoverable_errors:RecoverableErrors,
} }
impl PartialMap2{ impl PartialMap2{
pub fn add_render_configs_and_textures( pub fn add_render_configs_and_textures(
self, self,
render_configs:RenderConfigs, render_configs:RenderConfigs,
)->map::CompleteMap{ )->(map::CompleteMap,RecoverableErrors){
let (textures,render_configs)=render_configs.consume(); let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>) let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{ =textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
@@ -938,14 +1010,17 @@ impl PartialMap2{
); );
render_config render_config
}).collect(); }).collect();
map::CompleteMap{ (
modes:self.modes, map::CompleteMap{
attributes:self.attributes, modes:self.modes,
meshes:self.meshes, attributes:self.attributes,
models:self.models, meshes:self.meshes,
//the roblox legacy texture thing always works models:self.models,
textures, //the roblox legacy texture thing always works
render_configs, textures,
} render_configs,
},
self.recoverable_errors,
)
} }
} }

View File

@@ -1,9 +1,12 @@
use rbx_mesh::mesh_data::{NormalId2 as MeshDataNormalId2,VertexId as MeshDataVertexId}; use crate::loader::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,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 rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId}; use strafesnet_common::model::{self,IndexedVertex,MeshBuilder,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId,VertexId};
use strafesnet_common::integer::vec3; use strafesnet_common::integer::vec3;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Block, Block,
@@ -21,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. // 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. // Roblox duplicates this information per vertex when it should only exist per-face.
enum MeshDataNormalStatus{ enum MeshDataNormalStatus{
Agree(MeshDataNormalId2), Agree(MeshDataNormalId),
Conflicting, Conflicting,
} }
struct MeshDataNormalChecker{ struct MeshDataNormalChecker{
@@ -31,7 +34,7 @@ impl MeshDataNormalChecker{
fn new()->Self{ fn new()->Self{
Self{status:None} Self{status:None}
} }
fn check(&mut self,normal:MeshDataNormalId2){ fn check(&mut self,normal:MeshDataNormalId){
self.status=match self.status.take(){ self.status=match self.status.take(){
None=>Some(MeshDataNormalStatus::Agree(normal)), None=>Some(MeshDataNormalStatus::Agree(normal)),
Some(MeshDataNormalStatus::Agree(old_normal))=>{ Some(MeshDataNormalStatus::Agree(old_normal))=>{
@@ -44,7 +47,7 @@ impl MeshDataNormalChecker{
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting), 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{ self.status.and_then(|status|match status{
MeshDataNormalStatus::Agree(normal)=>Some(normal), MeshDataNormalStatus::Agree(normal)=>Some(normal),
MeshDataNormalStatus::Conflicting=>None, MeshDataNormalStatus::Conflicting=>None,
@@ -52,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{} impl std::error::Error for Error{}
pub fn convert( pub fn convert(
roblox_physics_data:&[u8], roblox_physics_data:&[u8],
roblox_mesh_data:&[u8], roblox_mesh_data:&[u8],
size:glam::Vec3, size:glam::Vec3,
crate::rbx::RobloxPartDescription(part_texture_description):crate::rbx::RobloxPartDescription, RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<model::Mesh,Error>{ )->Result<MeshWithSize,Error>{
const NORMAL_FACES:usize=6; let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
// build graphics and physics meshes // build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new(); let mut mb=MeshBuilder::new();
// graphics // graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){ let graphics_groups=if !roblox_mesh_data.is_empty(){
// create per-face texture coordinate affine transforms // create per-face texture coordinate affine transforms
@@ -75,46 +187,12 @@ pub fn convert(
let mesh_data=rbx_mesh::read_mesh_data_versioned( let mesh_data=rbx_mesh::read_mesh_data_versioned(
std::io::Cursor::new(roblox_mesh_data) std::io::Cursor::new(roblox_mesh_data)
).map_err(Error::RobloxMeshData)?; ).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::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::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::CSGMDL4(mesh_data4))=>mesh_data4.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 [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
graphics_mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
graphics_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|{
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|{ (0..NORMAL_FACES).map(|polygon_group_id|{
model::IndexedGraphicsGroup{ model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render), render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
@@ -126,7 +204,11 @@ pub fn convert(
}; };
//physics //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( let physics_data=rbx_mesh::read_physics_data_versioned(
std::io::Cursor::new(roblox_physics_data) std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?; ).map_err(Error::RobloxPhysicsData)?;
@@ -135,44 +217,56 @@ pub fn convert(
// have not seen this format in practice // have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block) |rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::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, =>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{ }else{
Vec::new() // generate a unit cube as default physics
}; let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
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);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO); let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics) let normal=mb.acquire_normal_id(vec3::zero());
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{ let color=mb.acquire_color_id(glam::Vec4::ONE);
let face=[ let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?, mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?, ).to_vec()).to_vec()));
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?, polygon_groups_normal_it.chain([Ok(polygon_group)]).collect::<Result<_,_>>()?
].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 physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{ let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)] groups:vec![PolygonGroupId::new(id as u32)]
}).collect(); }).collect();
Ok(mb.build( let mesh=mb.build(
polygon_groups, polygon_groups,
graphics_groups, graphics_groups,
physics_groups, physics_groups,
)) );
Ok(MeshWithSize{
mesh,
size:vec3::ONE,
})
} }

View File

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

View File

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

View File

@@ -1,93 +1,121 @@
use rbx_dom_weak::{types::Ref,ustr,InstanceBuilder,WeakDom}; use crate::util::static_ustr;
use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom};
pub fn class_is_a(class:&str,superclass:&str)->bool{ #[derive(Debug)]
class==superclass pub enum ServicesError{
||rbx_reflection_database::get().classes.get(class) WorkspaceNotFound,
.is_some_and(|descriptor| }
descriptor.superclass.as_ref().is_some_and(|class_super| impl std::fmt::Display for ServicesError{
class_is_a(class_super,superclass) 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 struct Context{
pub(crate)dom:WeakDom, pub(crate)dom:WeakDom,
pub(crate)services:Services,
} }
impl Context{ impl Context{
pub const fn new(dom:WeakDom)->Self{ pub fn from_place(dom:WeakDom)->Result<Context,ServicesError>{
Self{dom} 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") let script=InstanceBuilder::new("Script")
.with_property("Source",rbx_types::Variant::String(source)); .with_property("Source",rbx_types::Variant::String(source));
let script_ref=script.referent(); let script_ref=script.referent();
let mut context=Self::new(WeakDom::new( let dom=WeakDom::new(
InstanceBuilder::new("DataModel") InstanceBuilder::new("DataModel")
.with_child(script) .with_child(script)
)); );
let services=context.convert_into_place(); let context=Self::from_model(dom);
(context,crate::runner::instance::Instance::new(script_ref),services) (context,crate::runner::instance::Instance::new_unchecked(script_ref))
}
pub fn from_ref(dom:&WeakDom)->&Context{
unsafe{&*(dom as *const WeakDom as *const Context)}
}
pub fn from_mut(dom:&mut WeakDom)->&mut Context{
unsafe{&mut *(dom as *mut WeakDom as *mut Context)}
} }
/// Creates an iterator over all items of a particular class. /// 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{ pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
self.dom.descendants().filter(|&instance| let db=rbx_reflection_database::get().unwrap();
class_is_a(instance.class.as_ref(),superclass) let Some(superclass)=db.classes.get(superclass)else{
).map(|instance|instance.referent()) panic!("Invalid class");
} };
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{ self.dom.descendants().filter_map(|instance|{
self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect() let class=db.classes.get(instance.class.as_str())?;
} db.has_superclass(class,superclass).then(||instance.referent())
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(),
}) })
} }
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 //snapshot root instances
let children=self.dom.root().children().to_owned(); let children=dom.root().children().to_owned();
//insert services //insert services
let game=self.dom.root_ref(); let game=dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain"); let terrain_bldr=InstanceBuilder::new("Terrain")
let workspace=self.dom.insert(game, .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") InstanceBuilder::new("Workspace")
//Set Workspace.Terrain property equal to Terrain //Set Workspace.Terrain property equal to Terrain
.with_property("Terrain",terrain_bldr.referent()) .with_property("Terrain",terrain_bldr.referent())
.with_child(terrain_bldr) .with_child(terrain_bldr)
); );
{
//Lowercase and upper case workspace property!
let game=self.dom.root_mut();
game.properties.insert(ustr("workspace"),rbx_types::Variant::Ref(workspace));
game.properties.insert(ustr("Workspace"),rbx_types::Variant::Ref(workspace));
}
self.dom.insert(game,InstanceBuilder::new("Lighting"));
//transfer original root instances into workspace //transfer original root instances into workspace
for instance in children{ for instance in children{
self.dom.transfer_within(instance,workspace); dom.transfer_within(instance,workspace);
} }
Services{ {
game, //Lowercase and upper case workspace property!
workspace, 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{ impl AsRef<WeakDom> for Context{
pub game:Ref, fn as_ref(&self)->&WeakDom{
pub workspace:Ref, &self.dom
}
} }

View File

@@ -1,3 +1,4 @@
mod util;
pub mod runner; pub mod runner;
pub mod context; pub mod context;
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
@@ -5,3 +6,7 @@ pub(crate) mod scheduler;
#[cfg(test)] #[cfg(test)]
mod tests; 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; use super::vector3::Vector3;
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct CFrame(pub(crate)glam::Affine3A); pub struct CFrame(glam::Affine3A);
impl CFrame{ impl CFrame{
pub fn new( 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) glam::vec3a(v.x,v.y,v.z)
} }
impl Into<rbx_types::CFrame> for CFrame{ impl From<CFrame> for rbx_types::CFrame{
fn into(self)->rbx_types::CFrame{ fn from(CFrame(cf):CFrame)->rbx_types::CFrame{
rbx_types::CFrame::new( rbx_types::CFrame::new(
vec3_to_glam(self.0.translation), vec3_to_glam(cf.translation),
rbx_types::Matrix3::new( rbx_types::Matrix3::new(
vec3_to_glam(self.0.matrix3.x_axis), vec3_to_glam(cf.matrix3.x_axis),
vec3_to_glam(self.0.matrix3.y_axis), vec3_to_glam(cf.matrix3.y_axis),
vec3_to_glam(self.0.matrix3.z_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>{ 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.new
cframe_table.raw_set("new", table.raw_set("new",
lua.create_function(|_,tuple:( lua.create_function(|lua,tuple:(
mlua::Value,mlua::Value,Option<f32>, mlua::Value,mlua::Value,Option<Number>,
Option<f32>,Option<f32>,Option<f32>, Option<Number>,Option<Number>,Option<Number>,
Option<f32>,Option<f32>,Option<f32>, Option<Number>,Option<Number>,Option<Number>,
Option<f32>,Option<f32>,Option<f32>, Option<Number>,Option<Number>,Option<Number>,
)|match tuple{ )|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) //CFrame.new(pos)
( (
mlua::Value::UserData(pos),mlua::Value::Nil,None, 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,
None,None,None, None,None,None,
)=>{ )=>{
let pos:Vector3=pos.take()?; let Vector3(pos):&Vector3=&*pos.borrow()?;
Ok(CFrame::point(pos.0.x,pos.0.y,pos.0.z)) Ok(CFrame::point(pos.x,pos.y,pos.z))
}, },
//TODO: CFrame.new(pos,look) //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,
None,None,None, None,None,None,
)=>{ )=>{
let _pos:Vector3=pos.take()?; let _pos:&Vector3=&*pos.borrow()?;
let _look:Vector3=look.take()?; let _look:&Vector3=&*look.borrow()?;
Err(mlua::Error::runtime("Not yet implemented")) Err(mlua::Error::runtime("Not yet implemented"))
}, },
//CFrame.new(x,y,z) //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, 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) //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(xx),Some(yx),Some(zx),
Some(xy),Some(yy),Some(zy), Some(xy),Some(yy),Some(zy),
Some(xz),Some(yz),Some(zz), Some(xz),Some(yz),Some(zz),
)=>Ok(CFrame::new(x as f32,y as f32,z, )=>Ok(CFrame::new(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into(),
xx,yx,zx, xx.into(),yx.into(),zx.into(),
xy,yy,zy, xy.into(),yy.into(),zy.into(),
xz,yz,zz, xz.into(),yz.into(),zz.into(),
)), )),
_=>Err(mlua::Error::runtime("Invalid arguments")) _=>Err(mlua::Error::runtime("Invalid arguments"))
})? })?
)?; )?;
//CFrame.Angles //CFrame.Angles
cframe_table.raw_set("Angles", let from_euler_angles=lua.create_function(|_,(x,y,z):(Number,Number,Number)|
lua.create_function(|_,(x,y,z):(f32,f32,f32)| Ok(CFrame::angles(x.into(),y.into(),z.into()))
Ok(CFrame::angles(x,y,z))
)?
)?; )?;
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(()) Ok(())
} }
impl mlua::UserData for CFrame{ impl mlua::UserData for CFrame{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
//CFrame.p fields.add_field_method_get("p",|_,CFrame(this)|Ok(Vector3(this.translation)));
fields.add_field_method_get("p",|_,this|Ok(Vector3(this.0.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){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("components",|_,this,()|Ok(( methods.add_method("components",|_,CFrame(this),()|Ok((
this.0.translation.x, this.translation.x,
this.0.translation.y, this.translation.y,
this.0.translation.z, this.translation.z,
this.0.matrix3.x_axis.x, this.matrix3.x_axis.x,
this.0.matrix3.y_axis.x, this.matrix3.y_axis.x,
this.0.matrix3.z_axis.x, this.matrix3.z_axis.x,
this.0.matrix3.x_axis.y, this.matrix3.x_axis.y,
this.0.matrix3.y_axis.y, this.matrix3.y_axis.y,
this.0.matrix3.z_axis.y, this.matrix3.z_axis.y,
this.0.matrix3.x_axis.z, this.matrix3.x_axis.z,
this.0.matrix3.y_axis.z, this.matrix3.y_axis.z,
this.0.matrix3.z_axis.z, this.matrix3.z_axis.z,
))); )));
methods.add_method("VectorToWorldSpace",|_,this,v:Vector3| methods.add_method("VectorToWorldSpace",|_,CFrame(this),Vector3(v):Vector3|
Ok(Vector3(this.0.transform_vector3a(v.0))) 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,|_,(CFrame(this),CFrame(val)):(Self,Self)|Ok(Self(this*val)));
methods.add_meta_function(mlua::MetaMethod::Mul,|_,(this,val):(Self,Self)|Ok(Self(this.0*val.0))); methods.add_meta_function(mlua::MetaMethod::ToString,|_,CFrame(this):Self|
methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self|
Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})", Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})",
this.0.translation.x, this.translation.x,
this.0.translation.y, this.translation.y,
this.0.translation.z, this.translation.z,
this.0.matrix3.x_axis.x, this.matrix3.x_axis.x,
this.0.matrix3.y_axis.x, this.matrix3.y_axis.x,
this.0.matrix3.z_axis.x, this.matrix3.z_axis.x,
this.0.matrix3.x_axis.y, this.matrix3.x_axis.y,
this.0.matrix3.y_axis.y, this.matrix3.y_axis.y,
this.0.matrix3.z_axis.y, this.matrix3.z_axis.y,
this.0.matrix3.x_axis.z, this.matrix3.x_axis.z,
this.0.matrix3.y_axis.z, this.matrix3.y_axis.z,
this.0.matrix3.z_axis.z, this.matrix3.z_axis.z,
)) ))
); );
} }

View File

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

View File

@@ -1,31 +1,29 @@
#[derive(Clone,Copy)] #[derive(Clone)]
pub struct ColorSequence{} pub struct ColorSequence(rbx_types::ColorSequence);
impl ColorSequence{ impl ColorSequence{
pub const fn new()->Self{ pub const fn new(keypoints:Vec<rbx_types::ColorSequenceKeypoint>)->Self{
Self{} Self(rbx_types::ColorSequence{keypoints})
} }
} }
impl Into<rbx_types::ColorSequence> for ColorSequence{ impl From<ColorSequence> for rbx_types::ColorSequence{
fn into(self)->rbx_types::ColorSequence{ fn from(ColorSequence(value):ColorSequence)->rbx_types::ColorSequence{
rbx_types::ColorSequence{ value
keypoints:Vec::new()
}
} }
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ 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| lua.create_function(|_,_:mlua::MultiValue|
Ok(ColorSequence::new()) Ok(ColorSequence::new(Vec::new()))
)? )?
)?; )?;
globals.set("ColorSequence",number_sequence_table)?; globals.set("ColorSequence",table)?;
Ok(()) Ok(())
} }
impl mlua::UserData for ColorSequence{} 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)] #[derive(Clone,Copy)]
pub struct EnumItem<'a>{ pub struct EnumItem<'a>{
ed:&'a rbx_reflection::EnumDescriptor<'a>, name:Option<&'a str>,
value:u32,
} }
impl<'a> EnumItem<'a>{
impl Into<rbx_types::Enum> for Enum{ fn known_name((name,&value):(&'a std::borrow::Cow<'a,str>,&u32))->Self{
fn into(self)->rbx_types::Enum{ Self{name:Some(name.as_ref()),value}
rbx_types::Enum::from_u32(self.0) }
}
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>{ #[derive(Clone,Copy)]
const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{ pub struct Enums;
Self{ed} 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>{ 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_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,val):(EnumItem<'_>,mlua::String)|{ methods.add_method("FromName",|_,this:&EnumItems,name:mlua::String|Ok(this.from_name(&*name.to_str()?)));
match this.ed.items.get(&*val.to_str()?){ methods.add_method("FromValue",|_,this:&EnumItems,value:u32|Ok(this.from_value(value)));
Some(&id)=>Enum(id).into_lua(lua), methods.add_method("GetEnumItems",|_,this:&EnumItems,()|->mlua::Result<Vec<EnumItem>>{
None=>mlua::Value::Nil.into_lua(lua), 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); 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,48 +2,48 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti}; use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref; use rbx_types::Ref;
use rbx_dom_weak::{ustr,Ustr,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>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store //class functions store
lua.set_app_data(ClassMethodsStore::default()); 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.new
instance_table.raw_set("new", table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{ lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
let class_name_str=&*class_name.to_str()?; 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|{ dom_mut(lua,|dom|{
//TODO: Nil instances Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
}) })
})? })?
)?; )?;
globals.set("Instance",instance_table)?; globals.set("Instance",table)?;
Ok(()) Ok(())
} }
// LMAO look at this function! // 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>{ 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) f(*dom)
} }
fn coerce_float32(value:&mlua::Value)->Option<f32>{ pub fn class_is_a(class:&str,superclass:&str)->bool{
match value{ let db=rbx_reflection_database::get().unwrap();
&mlua::Value::Integer(i)=>Some(i as f32), let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
&mlua::Value::Number(f)=>Some(f as f32), return false;
_=>None, };
} db.has_superclass(class,superclass)
} }
fn get_full_name(dom:&WeakDom,instance:&rbx_dom_weak::Instance)->String{
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
let mut full_name=instance.name.clone(); let mut full_name=instance.name.clone();
let mut pref=instance.parent(); let mut pref=instance.parent();
while let Some(parent)=dom.get_by_ref(pref){ while let Some(parent)=dom.get_by_ref(pref){
@@ -57,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>{ pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=script.get(dom)?; let instance=script.get(dom)?;
let source=match instance.properties.get(&ustr("Source")){ let source=match instance.properties.get(&static_ustr("Source")){
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(), Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
_=>Err(mlua::Error::external("Missing script.Source"))?, _=>Err(mlua::Error::external("Missing script.Source"))?,
}; };
@@ -65,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) 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) 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) 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) 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)] #[derive(Clone,Copy)]
pub struct Instance{ pub struct Instance{
referent:Ref, referent:Ref,
} }
impl Instance{ impl Instance{
pub const fn new(referent:Ref)->Self{ pub const fn new_unchecked(referent:Ref)->Self{
Self{referent} 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>{ 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")) dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
} }
@@ -98,19 +116,23 @@ type_from_lua_userdata!(Instance);
impl mlua::UserData for Instance{ impl mlua::UserData for Instance{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ 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|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
Ok(Instance::new(instance.parent())) Ok(Instance::new(instance.parent()))
}) })
}); }
fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{ fields.add_field_method_get("parent",get_parent);
let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?; 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_mut(lua,|dom|{
dom.transfer_within(this.referent,parent.referent); dom.transfer_within(this.referent,parent_ref);
Ok(()) 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|{ fields.add_field_method_get("Name",|lua,this|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
@@ -133,17 +155,33 @@ impl mlua::UserData for Instance{
}); });
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ 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|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
let children:Vec<_>=instance let children:Vec<_>=instance
.children() .children()
.iter() .iter()
.copied() .copied()
.map(Instance::new) .map(Instance::new_unchecked)
.collect(); .collect();
Ok(children) 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>>{ fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
let name_str=&*name.to_str()?; let name_str=&*name.to_str()?;
@@ -155,7 +193,7 @@ impl mlua::UserData for Instance{
false=>find_first_child(dom,instance,name_str), false=>find_first_child(dom,instance,name_str),
} }
.map(|instance| .map(|instance|
Instance::new(instance.referent()) Instance::new_unchecked(instance.referent())
) )
) )
}) })
@@ -165,13 +203,29 @@ impl mlua::UserData for Instance{
methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{ methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?; let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok( Ok(
match search_descendants.unwrap_or(false){ match search_descendants.unwrap_or(false){
true=>find_first_descendant_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,this.get(dom)?,class_str), false=>find_first_child_of_class(dom,inst,class_str),
} }
.map(|instance| .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())
) )
) )
}) })
@@ -181,24 +235,42 @@ impl mlua::UserData for Instance{
let children:Vec<_>=dom let children:Vec<_>=dom
.descendants_of(this.referent) .descendants_of(this.referent)
.map(|instance| .map(|instance|
Instance::new(instance.referent()) Instance::new_unchecked(instance.referent())
) )
.collect(); .collect();
Ok(children) 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|{ dom_mut(lua,|dom|{
let instance=this.get(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_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(()) 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|{ methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
@@ -207,29 +279,37 @@ impl mlua::UserData for Instance{
}); });
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{ methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{
let index_str=&*index.to_str()?; let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
//println!("__index t={} i={index:?}",instance.name); //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"))?; let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
//Find existing property // Find existing property
match instance.properties.get(&index_ustr) // Interestingly, ustr can know ahead of time if
.cloned() // 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 //Find default value
.or_else(||db.find_default_property(class,index_str).cloned()) .or_else(||db.find_default_property(class,index_str).cloned())
//Find virtual property //Find virtual property
.or_else(||db.superclasses_iter(class).find_map(|class| .or_else(||db.superclasses_iter(class).find_map(|class|
find_virtual_property(&instance.properties,class,&index_ustr) find_virtual_property(&instance.properties,class,index_str)
)) ))
{ {
Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int64(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::Float32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Float64(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::String(val))=>return 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::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).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=>(), None=>(),
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))), other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
} }
@@ -245,78 +325,117 @@ impl mlua::UserData for Instance{
} }
//find or create an associated userdata object //find or create an associated userdata object
let instance=this.get_mut(dom)?; if let Some(value)=instance_value_store_mut(lua,|ivs|{
if let Some(value)=get_or_create_userdata(instance,lua,index_ustr)?{ //TODO: walk class tree somehow
match ivs.get_or_create_instance_values(&instance){
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
None=>Ok(None)
}
})?{
return value.into_lua(lua); return value.into_lua(lua);
} }
// drop mutable borrow
//find a child with a matching name //find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str) find_first_child(dom,instance,index_str)
.map(|instance|Instance::new(instance.referent())) .map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua) .into_lua(lua)
}) })
}); });
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{ methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?; let instance=this.get_mut(dom)?;
//println!("__newindex t={} i={index:?} v={value:?}",instance.name); let db=rbx_reflection_database::get().unwrap();
let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?; let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let property=db.superclasses_iter(class).find_map(|cls| let property=db.superclasses_iter(class).find_map(|cls|
cls.properties.get(index_str) cls.properties.get(index_str)
).ok_or_else(|| ).ok_or_else(||
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)) mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
)?; )?;
match &property.data_type{ let value=match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?;
instance.properties.insert(index_ustr,rbx_types::Variant::Vector3(typed_value.into()));
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?; let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
instance.properties.insert(index_ustr,rbx_types::Variant::Float32(typed_value)); rbx_types::Variant::Float32(typed_value)
}, },
rbx_reflection::DataType::Enum(enum_name)=>{ rbx_reflection::DataType::Enum(enum_name)=>{
let typed_value=match &value{ let typed_value=match &value{
&mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)), &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::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)),
mlua::Value::String(s)=>{ 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"))?)) 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)=>{ 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()) Ok(e.into())
}, },
_=>Err(mlua::Error::runtime("Expected Enum")), _=>Err(mlua::Error::runtime("Expected Enum")),
}?; }?;
instance.properties.insert(index_ustr,rbx_types::Variant::Enum(typed_value)); rbx_types::Variant::Enum(typed_value)
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{ 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()?; let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?;
instance.properties.insert(index_ustr,rbx_types::Variant::Color3(typed_value.into())); rbx_types::Variant::Color3(typed_value.into())
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?; let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
instance.properties.insert(index_ustr,rbx_types::Variant::Bool(typed_value)); rbx_types::Variant::Bool(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
rbx_types::Variant::Int32(typed_value)
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?; let typed_value=match &value{
instance.properties.insert(index_ustr,rbx_types::Variant::String(typed_value.to_owned())); 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)=>{ 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()?; let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
instance.properties.insert(index_ustr,rbx_types::Variant::NumberSequence(typed_value.into())); rbx_types::Variant::NumberSequence(typed_value.clone().into())
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{ 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()?; let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
instance.properties.insert(index_ustr,rbx_types::Variant::ColorSequence(typed_value.into())); 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:?}"))), 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(()) Ok(())
}) })
}); });
@@ -341,28 +460,53 @@ type CFD=phf::Map<&'static str,// Class name
ClassFunctionPointer 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!{ static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
"DataModel"=>phf::phf_map!{ "DataModel"=>phf::phf_map!{
"GetService"=>cf!(|lua,_this,service:mlua::String|{ "service"=>GET_SERVICE,
dom_mut(lua,|dom|{ "GetService"=>GET_SERVICE,
//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"))),
}
})
}),
}, },
"Terrain"=>phf::phf_map!{ "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,
))
}),
}, },
}; };
@@ -378,7 +522,7 @@ struct ClassMethodsStore{
} }
impl ClassMethodsStore{ impl ClassMethodsStore{
/// return self.classes[class] or create the ClassMethods and then return it /// 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 // Use get_entry to get the &'static str keys of the database
// and use it as a key for the classes hashmap // and use it as a key for the classes hashmap
CLASS_FUNCTION_DATABASE.get_entry(class) CLASS_FUNCTION_DATABASE.get_entry(class)
@@ -451,41 +595,95 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
fn find_virtual_property( fn find_virtual_property(
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>, properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor, class:&rbx_reflection::ClassDescriptor,
index:&Ustr, index:&str,
)->Option<rbx_types::Variant>{ )->Option<rbx_types::Variant>{
//Find virtual property //Find virtual property
let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?; let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
let virtual_property=class_virtual_properties.get(index)?; let virtual_property=class_virtual_properties.get(index)?;
//Get source property //Get source property
let variant=properties.get(&ustr(virtual_property.property))?; let variant=properties.get(&static_ustr(virtual_property.property))?;
//Transform Source property with provided function //Transform Source property with provided function
(virtual_property.pointer)(variant) (virtual_property.pointer)(variant)
} }
// lazy-loaded per-instance userdata values // lazy-loaded per-instance userdata values
// This whole thing is a bad idea and a garbage collection nightmare.
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>; type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
type LUD=phf::Map<&'static str,// Class name type LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name phf::Map<&'static str,// Value name
CreateUserData 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!{ static LAZY_USER_DATA:LUD=phf::phf_map!{
"RunService"=>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,
}, },
}; };
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:rbx_dom_weak::Ustr)->mlua::Result<Option<mlua::AnyUserData>>{ #[derive(Default)]
use std::collections::hash_map::Entry; pub struct InstanceValueStore{
Ok(match LAZY_USER_DATA.get(instance.class.as_str()){ values:HashMap<Ref,
Some(userdata_map)=>match instance.userdata.entry(index){ HashMap<&'static str,
Entry::Occupied(entry)=>Some(entry.get().clone()), mlua::AnyUserData
Entry::Vacant(entry)=>match userdata_map.get(index.as_str()){ >
Some(create_userdata)=>Some(entry.insert(create_userdata(lua)?).clone()), >,
None=>None, }
}, pub struct InstanceValues<'a>{
}, named_values:&'static phf::Map<&'static str,CreateUserData>,
None=>None, 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<'_>>{
LAZY_USER_DATA.get(instance.class.as_str())
.map(|named_values|
InstanceValues{
named_values,
values:self.values.entry(instance.referent())
.or_insert_with(||HashMap::new()),
}
)
}
}
impl InstanceValues<'_>{
pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
Ok(match self.named_values.get_entry(index){
Some((&static_index_str,&function_pointer))=>Some(
match self.values.entry(static_index_str){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(
function_pointer(lua)?
).clone(),
}
),
None=>None,
})
}
}
pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
f(&mut *cf)
} }

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{ macro_rules! type_from_lua_userdata_lua_lifetime{
($ty:ident)=>{ ($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{ impl mlua::FromLua for $ty<'static>{

View File

@@ -3,10 +3,19 @@ mod macros;
mod runner; mod runner;
mod r#enum; mod r#enum;
mod task;
mod udim;
mod tween;
mod udim2;
mod color3; mod color3;
mod cframe; mod cframe;
mod number;
mod vector2;
mod vector3; mod vector3;
mod brickcolor;
mod tween_info;
pub mod instance; pub mod instance;
mod number_range;
mod script_signal; mod script_signal;
mod color_sequence; mod color_sequence;
mod number_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)] #[derive(Clone)]
pub struct NumberSequence{} pub struct NumberSequence(rbx_types::NumberSequence);
impl NumberSequence{ impl NumberSequence{
pub const fn new()->Self{ pub const fn new(keypoints:Vec<rbx_types::NumberSequenceKeypoint>)->Self{
Self{} Self(rbx_types::NumberSequence{keypoints})
} }
} }
impl Into<rbx_types::NumberSequence> for NumberSequence{ impl From<NumberSequence> for rbx_types::NumberSequence{
fn into(self)->rbx_types::NumberSequence{ fn from(NumberSequence(value):NumberSequence)->rbx_types::NumberSequence{
rbx_types::NumberSequence{ value
keypoints:Vec::new()
}
} }
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ 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| lua.create_function(|_,_:mlua::MultiValue|
Ok(NumberSequence::new()) Ok(NumberSequence::new(Vec::new()))
)? )?
)?; )?;
globals.set("NumberSequence",number_sequence_table)?; globals.set("NumberSequence",table)?;
Ok(()) Ok(())
} }
impl mlua::UserData for NumberSequence{} 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 error:mlua::Error
}, },
RustLua(mlua::Error), RustLua(mlua::Error),
NoServices,
} }
impl std::fmt::Display for Error{ impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
match self{ match self{
Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"), Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"),
Self::RustLua(error)=>write!(f,"rust-side lua error: {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 //global environment
let globals=lua.globals(); let globals=lua.globals();
#[cfg(feature="run-service")] super::task::set_globals(lua,&globals)?;
crate::scheduler::set_globals(lua,&globals)?;
super::script_signal::set_globals(lua,&globals)?; super::script_signal::set_globals(lua,&globals)?;
super::r#enum::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::color3::set_globals(lua,&globals)?;
super::brickcolor::set_globals(lua,&globals)?;
super::vector2::set_globals(lua,&globals)?;
super::vector3::set_globals(lua,&globals)?; super::vector3::set_globals(lua,&globals)?;
super::cframe::set_globals(lua,&globals)?; super::cframe::set_globals(lua,&globals)?;
super::instance::instance::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::number_sequence::set_globals(lua,&globals)?;
super::color_sequence::set_globals(lua,&globals)?; super::color_sequence::set_globals(lua,&globals)?;
Ok(()) Ok(())
} }
unsafe fn extend_lifetime_mut<'a,T>(src:&mut T)->&'a mut T{
let ptr:*mut T=src;
unsafe{&mut*ptr}
}
impl Runner{ impl Runner{
pub fn new()->Result<Self,Error>{ pub fn new()->Result<Self,Error>{
let runner=Self{ let runner=Self{
@@ -54,22 +62,20 @@ impl Runner{
Ok(runner) Ok(runner)
} }
pub fn runnable_context<'a>(self,context:&'a mut Context)->Result<Runnable<'a>,Error>{ 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(); let globals=self.lua.globals();
globals.set("game",super::instance::Instance::new(services.game)).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(services.workspace)).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 // SAFETY: This is not a &'static mut WeakDom,
self.lua.set_app_data::<&'static mut rbx_dom_weak::WeakDom>(unsafe{core::mem::transmute(&mut context.dom)}); // 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")] #[cfg(feature="run-service")]
self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default()); self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
Ok(Runnable{ Ok(Runnable{
lua:self.lua, 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. //Runnable is the same thing but has context set, which it holds the lifetime for.
pub struct Runnable<'a>{ pub struct Runnable<'a>{
lua:mlua::Lua, lua:mlua::Lua,
_lifetime:&'a std::marker::PhantomData<()> _lifetime:std::marker::PhantomData<&'a ()>
} }
impl Runnable<'_>{ impl Runnable<'_>{
pub fn drop_context(self)->Runner{ 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")] #[cfg(feature="run-service")]
self.lua.remove_app_data::<crate::scheduler::Scheduler>(); self.lua.remove_app_data::<crate::scheduler::Scheduler>();
Runner{ Runner{
@@ -123,15 +129,20 @@ impl Runnable<'_>{
} }
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
pub fn run_service_step(&self)->Result<(),mlua::Error>{ pub fn run_service_step(&self)->Result<(),mlua::Error>{
let render_stepped_signal=super::instance::instance::dom_mut(&self.lua,|dom|{ let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{
let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?; let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?;
Ok(match run_service.userdata.get(&rbx_dom_weak::ustr("RenderStepped")){ super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{
Some(render_stepped)=>Some(render_stepped.borrow::<super::script_signal::ScriptSignal>()?.clone()), //unwrap because I trust my find_first_child_of_class function to
None=>None let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?;
let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?;
//let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?;
//let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?;
Ok(render_stepped)
}) })
})?; })?;
if let Some(render_stepped_signal)=render_stepped_signal{ if let Some(render_stepped)=render_stepped{
render_stepped_signal.fire(&mlua::MultiValue::new()); let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?;
signal.fire(&mlua::MultiValue::new());
} }
Ok(()) Ok(())
} }

View File

@@ -98,12 +98,9 @@ impl ScriptConnection{
impl mlua::UserData for ScriptSignal{ impl mlua::UserData for ScriptSignal{
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("Connect",|_lua,this,f:mlua::Function| methods.add_method("connect",|_lua,this,f:mlua::Function|Ok(this.connect(f)));
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)));
methods.add_method("Once",|_lua,this,f:mlua::Function|
Ok(this.once(f))
);
// Fire is not allowed to be called from Lua // Fire is not allowed to be called from Lua
// methods.add_method("Fire",|_lua,this,args:mlua::MultiValue| // methods.add_method("Fire",|_lua,this,args:mlua::MultiValue|
// Ok(this.fire(args)) // Ok(this.fire(args))
@@ -120,7 +117,7 @@ impl mlua::FromLua for ScriptSignal{
} }
impl mlua::UserData for ScriptConnection{ 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|{ fields.add_field_method_get("Connected",|_,this|{
Ok(this.position().is_some()) 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>(())?; .call::<mlua::Function>(())?;
lua.register_userdata_type::<ScriptSignal>(|reg|{ lua.register_userdata_type::<ScriptSignal>(|reg|{
reg.add_field("wait",wait.clone());
reg.add_field("Wait",wait); reg.add_field("Wait",wait);
mlua::UserData::register(reg); 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)] #[derive(Clone,Copy)]
pub struct Vector3(pub(crate)glam::Vec3A); 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>{ 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.new
vector3_table.raw_set("new", table.raw_set("new",
lua.create_function(|_,(x,y,z):(f32,f32,f32)| lua.create_function(|_,(x,y,z):(Option<Number>,Option<Number>,Option<Number>)|
Ok(Vector3::new(x,y,z)) 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(()) Ok(())
} }
impl Into<rbx_types::Vector3> for Vector3{ impl From<Vector3> for rbx_types::Vector3{
fn into(self)->rbx_types::Vector3{ fn from(Vector3(v):Vector3)->rbx_types::Vector3{
rbx_types::Vector3::new(self.0.x,self.0.y,self.0.z) 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{ impl mlua::UserData for Vector3{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ 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("magnitude",|_,Vector3(this)|Ok(this.length()));
fields.add_field_method_get("x",|_,this|Ok(this.0.x)); fields.add_field_method_get("Magnitude",|_,Vector3(this)|Ok(this.length()));
fields.add_field_method_set("x",|_,this,val|{ fields.add_field_method_get("unit",|_,Vector3(this)|Ok(Vector3(this.normalize())));
this.0.x=val; fields.add_field_method_get("Unit",|_,Vector3(this)|Ok(Vector3(this.normalize())));
Ok(()) 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",|_,this|Ok(this.0.y)); fields.add_field_method_get("y",|_,Vector3(this)|Ok(this.y));
fields.add_field_method_set("y",|_,this,val|{ fields.add_field_method_get("Y",|_,Vector3(this)|Ok(this.y));
this.0.y=val; fields.add_field_method_get("z",|_,Vector3(this)|Ok(this.z));
Ok(()) fields.add_field_method_get("Z",|_,Vector3(this)|Ok(this.z));
});
fields.add_field_method_get("z",|_,this|Ok(this.0.z));
fields.add_field_method_set("z",|_,this,val|{
this.0.z=val;
Ok(())
});
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
//methods.add_method("area",|_,this,()| Ok(this.length * this.width)); //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::Add,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this+val)));
methods.add_meta_function(mlua::MetaMethod::Div,|_,(this,val):(Self,mlua::Value)|{ methods.add_meta_function(mlua::MetaMethod::Sub,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this-val)));
match val{ methods.add_meta_function(mlua::MetaMethod::Mul,|lua,(lhs,rhs):(mlua::Value,mlua::Value)|{
mlua::Value::Integer(n)=>Ok(Self(this.0/(n as f32))), match (lhs,rhs){
mlua::Value::Number(n)=>Ok(Self(this.0/(n as f32))), (mlua::Value::UserData(lhs),mlua::Value::UserData(rhs))=>lhs.borrow_scoped(|Vector3(lhs):&Vector3|rhs.borrow_scoped(|Vector3(rhs):&Vector3|Self(lhs*rhs)))?,
mlua::Value::UserData(ud)=>{ (lhs,mlua::Value::UserData(rhs))=>{
let rhs:Vector3=ud.take()?; let lhs=Number::from_lua(lhs,lua)?;
Ok(Self(this.0/rhs.0)) 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:?}"))), 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({},{},{})", Ok(format!("Vector3.new({},{},{})",
this.0.x, this.x,
this.0.y, this.y,
this.0.z, 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>{ 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::<crate::scheduler::Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?; let mut scheduler=lua.app_data_mut::<Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
f(&mut *scheduler) 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{ let delay=match dt{
mlua::Value::Integer(i)=>i.max(0) as u64*60, mlua::Value::Integer(i)=>i.max(0) as u64*60,
mlua::Value::Number(f)=>{ mlua::Value::Number(f)=>{
@@ -75,32 +75,3 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
Ok(()) 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] [package]
name = "strafesnet_snf" name = "strafesnet_snf"
version = "0.3.0" version = "0.3.1"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
binrw = "0.14.0" binrw = "0.15.0"
id = { version = "0.1.0", registry = "strafesnet" } 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; 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)] #[derive(Debug)]
pub enum Error{ pub enum Error{
@@ -85,6 +85,7 @@ pub struct Segment{
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{ pub struct SegmentInfo{
/// time of the first instruction in this segment. /// time of the first instruction in this segment.
#[expect(dead_code)]
time:Time, time:Time,
instruction_count:u32, instruction_count:u32,
/// How many total instructions in segments up to and including this segment /// How many total instructions in segments up to and including this segment
@@ -116,6 +117,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
segment_map, segment_map,
}) })
} }
#[expect(dead_code)]
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{ 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))?) 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 //probe header length
let mut bot_header_data=Vec::new(); 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 // the first block location is the map header
block_location.push(offset); block_location.push(offset);

View File

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

View File

@@ -5,7 +5,6 @@ mod newtypes;
mod file; mod file;
pub mod map; pub mod map;
pub mod bot; pub mod bot;
pub mod demo;
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
@@ -13,7 +12,6 @@ pub enum Error{
Header(file::Error), Header(file::Error),
Map(map::Error), Map(map::Error),
Bot(bot::Error), Bot(bot::Error),
Demo(demo::Error),
} }
impl std::fmt::Display for Error{ impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ 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>{ pub enum SNF<R:BinReaderExt>{
Map(map::StreamableMap<R>), Map(map::StreamableMap<R>),
Bot(bot::StreamableBot<R>), Bot(bot::StreamableBot<R>),
Demo(demo::StreamableDemo<R>),
} }
pub fn read_snf<R:BinReaderExt>(input:R)->Result<SNF<R>,Error>{ 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(){ Ok(match file.fourcc(){
file::FourCC::Map=>SNF::Map(map::StreamableMap::new(file).map_err(Error::Map)?), 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::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>{ 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) _=>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)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -97,8 +97,8 @@ enum ResourceType{
} }
struct ResourceMap<T>{ struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>, meshes:HashMap<model::MeshId,T>,
textures:HashMap<strafesnet_common::model::TextureId,T>, textures:HashMap<model::TextureId,T>,
} }
impl<T> Default for ResourceMap<T>{ impl<T> Default for ResourceMap<T>{
fn default()->Self{ fn default()->Self{
@@ -185,7 +185,7 @@ pub struct StreamableMap<R:BinReaderExt>{
//this is every possible attribute... need some sort of streaming system //this is every possible attribute... need some sort of streaming system
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>, attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
//this is every possible render configuration... shaders and such... need streaming //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 //this makes sense to keep in memory for streaming, a map of which blocks occupy what space
bvh:BvhNode<BlockId>, bvh:BvhNode<BlockId>,
//something something resources hashmaps //something something resources hashmaps
@@ -223,7 +223,7 @@ impl<R:BinReaderExt> StreamableMap<R>{
} }
Ok(Self{ Ok(Self{
file, file,
modes:strafesnet_common::gameplay_modes::NormalizedModes::new(modes), modes:gameplay_modes::NormalizedModes::new(modes),
attributes, attributes,
render_configs, render_configs,
bvh:strafesnet_common::bvh::generate_bvh(bvh), bvh:strafesnet_common::bvh::generate_bvh(bvh),
@@ -366,12 +366,12 @@ fn collect_spacial_blocks(
block_location.push(sequential_block_data.position()); block_location.push(sequential_block_data.position());
}else{ }else{
match bvh_node.into_content(){ 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{ for bvh_node in bvh_node_list{
collect_spacial_blocks(block_location,block_headers,sequential_block_data,bvh_node)?; 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(()) 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)|{ let boxen=map.models.into_iter().enumerate().map(|(model_id,model)|{
//grow your own aabb //grow your own aabb
let mesh=map.meshes.get(model.mesh.get() as usize).ok_or(Error::InvalidMeshId(model.mesh))?; 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{ for &pos in &mesh.unique_pos{
aabb.grow(model.transform.transform_point3(pos).narrow_1().unwrap()); aabb.grow(model.transform.transform_point3(pos).narrow_1().unwrap());
} }
Ok(((model::ModelId::new(model_id as u32),model.into()),aabb)) Ok(((model::ModelId::new(model_id as u32),model.into()),aabb))
}).collect::<Result<Vec<_>,_>>()?; }).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 //build blocks
//block location is initialized with two values //block location is initialized with two values
//the first value represents the location of the first byte after the file header //the first value represents the location of the first byte after the file header

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
} }
} }
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{ impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::physics::InstructionConvert; type Error=InstructionConvert;
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{ fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
Ok(Self{ Ok(Self{
time:value.time.get(), time:value.time.get(),

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "map-tool" name = "map-tool"
version = "1.7.0" version = "1.7.2"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -12,20 +12,19 @@ flate2 = "1.0.27"
futures = "0.3.31" futures = "0.3.31"
image = "0.25.2" image = "0.25.2"
image_dds = "0.7.1" image_dds = "0.7.1"
lazy-regex = "3.1.0" rbx_asset = { version = "0.5.0", registry = "strafesnet" }
rbx_asset = { version = "0.4.4", registry = "strafesnet" } rbx_binary = "2.0.1"
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" } rbx_dom_weak = "4.1.0"
rbx_dom_weak = { version = "3.1.0-sn2", registry = "strafesnet" } rbx_reflection_database = "2.0.2"
rbx_reflection_database = "1.0.0" rbx_xml = "2.0.1"
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" } rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.0", path = "../lib/bsp_loader", registry = "strafesnet" } strafesnet_bsp_loader = { version = "0.3.1", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../lib/deferred_loader", registry = "strafesnet" } strafesnet_deferred_loader = { version = "0.5.1", path = "../lib/deferred_loader", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registry = "strafesnet" } strafesnet_rbx_loader = { version = "0.7.0", path = "../lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" } strafesnet_snf = { version = "0.3.1", path = "../lib/snf", registry = "strafesnet" }
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] } tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.8.0" vbsp = "0.9.1"
vbsp-entities-css = "0.6.0" vbsp-entities-css = "0.6.0"
vmdl = "0.2.0" vmdl = "0.2.0"
vmt-parser = "0.2.0" vmt-parser = "0.2.0"
@@ -36,3 +35,6 @@ vtf = "0.3.0"
#lto = true #lto = true
#strip = true #strip = true
#codegen-units = 1 #codegen-units = 1
[lints]
workspace = true

View File

@@ -3,11 +3,16 @@ use std::io::{Cursor,Read,Seek};
use std::collections::HashSet; use std::collections::HashSet;
use clap::{Args,Subcommand}; use clap::{Args,Subcommand};
use anyhow::Result as AResult; use anyhow::Result as AResult;
use rbx_dom_weak::{ustr,Instance}; use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode; use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId; use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
const DOWNLOAD_LIMIT:usize=16; const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -27,8 +32,12 @@ pub struct RobloxToSNFSubcommand {
pub struct DownloadAssetsSubcommand{ pub struct DownloadAssetsSubcommand{
#[arg(required=true)] #[arg(required=true)]
roblox_files:Vec<PathBuf>, roblox_files:Vec<PathBuf>,
// #[arg(long)] #[arg(long,group="cookie",required=true)]
// cookie_file:Option<String>, cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
} }
impl Commands{ impl Commands{
@@ -37,13 +46,27 @@ impl Commands{
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await, Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
Commands::DownloadAssets(subcommand)=>download_assets( Commands::DownloadAssets(subcommand)=>download_assets(
subcommand.roblox_files, subcommand.roblox_files,
rbx_asset::cookie::Cookie::new("".to_string()), cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
).await, ).await,
} }
} }
} }
#[allow(unused)] async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<rbx_asset::cookie::Cookie>{
let cookie=match (literal,environment,file){
(Some(cookie_literal),None,None)=>cookie_literal,
(None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?,
(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?,
};
Ok(rbx_asset::cookie::Cookie::new(cookie))
}
#[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum LoadDomError{ enum LoadDomError{
IO(std::io::Error), IO(std::io::Error),
@@ -102,8 +125,8 @@ SurfaceAppearance.RoughnessMapContent
WrapLayer.ReferenceMeshContent WrapLayer.ReferenceMeshContent
*/ */
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){ fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&ustr(property))else{ let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str()); println!("property={} does not exist for class={}",property,object.class.as_str());
return; return;
}; };
@@ -117,8 +140,8 @@ fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,
}; };
content_list.insert(asset_id); content_list.insert(asset_id);
} }
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){ fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&ustr(property))else{ let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str()); println!("property={} does not exist for class={}",property,object.class.as_str());
return; return;
}; };
@@ -144,8 +167,8 @@ impl UniqueAssets{
fn collect(&mut self,object:&Instance){ fn collect(&mut self,object:&Instance){
match object.class.as_str(){ match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"), "Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"), "Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"), "Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"), "FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{ "MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent"); accumulate_content(&mut self.textures,object,"TextureContent");
@@ -169,7 +192,7 @@ impl UniqueAssets{
} }
} }
#[allow(unused)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum UniqueAssetError{ enum UniqueAssetError{
IO(std::io::Error), IO(std::io::Error),
@@ -244,8 +267,8 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
tokio::fs::write(path,&data).await?; tokio::fs::write(path,&data).await?;
break Ok(DownloadResult::Data(data)); break Ok(DownloadResult::Data(data));
}, },
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{ Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
if scwuab.status_code.as_u16()==429{ if status_code.as_u16()==429{
if retry==12{ if retry==12{
println!("Giving up asset download {asset_id}"); println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1; stats.timed_out_downloads+=1;
@@ -257,7 +280,7 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
retry+=1; retry+=1;
}else{ }else{
stats.failed_downloads+=1; stats.failed_downloads+=1;
println!("weird scuwab error: {scwuab:?}"); println!("weird status_code error: status_code={status_code} url={} body={}",url_and_body.url,url_and_body.body);
break Ok(DownloadResult::Failed); break Ok(DownloadResult::Failed);
} }
}, },
@@ -398,7 +421,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
} }
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)] #[expect(dead_code)]
enum ConvertError{ enum ConvertError{
IO(std::io::Error), IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error), SNFMap(strafesnet_snf::map::Error),
@@ -411,17 +434,23 @@ impl std::fmt::Display for ConvertError{
} }
} }
impl std::error::Error for ConvertError{} impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?; struct Errors{
script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
convert_errors:strafesnet_rbx_loader::RecoverableErrors,
}
fn convert_to_snf(path:&Path,output_folder:PathBuf)->Result<Errors,ConvertError>{
let entire_file=std::fs::read(path).map_err(ConvertError::IO)?;
let model=strafesnet_rbx_loader::read( let model=strafesnet_rbx_loader::read(
std::io::Cursor::new(entire_file) entire_file.as_slice()
).map_err(ConvertError::RobloxRead)?; ).map_err(ConvertError::RobloxRead)?;
let mut place=model.into_place(); let mut place=strafesnet_rbx_loader::Place::from(model);
place.run_scripts(); let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?; let (map,convert_errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder; let mut dest=output_folder;
dest.push(path.file_stem().unwrap()); dest.push(path.file_stem().unwrap());
@@ -430,24 +459,37 @@ async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?; strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(()) Ok(Errors{
script_errors,
convert_errors,
})
} }
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{ async fn roblox_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf)->AResult<()>{
let start=std::time::Instant::now(); let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get(); let thread_limit=std::thread::available_parallelism()?.get();
let mut it=paths.into_iter(); let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0); static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
// This is wrong! Calling roblox_to_snf multiple times keeps adding permits
SEM.add_permits(thread_limit); SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){ while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone(); let output_folder=output_folder.clone();
tokio::spawn(async move{ tokio::task::spawn_blocking(move||{
let result=convert_to_snf(path.as_path(),output_folder).await; let result=convert_to_snf(path.as_path(),output_folder);
drop(permit); drop(permit);
match result{ match result{
Ok(())=>(), Ok(errors)=>{
for error in errors.script_errors{
println!("Script error: {error}");
}
let error_count=errors.convert_errors.count();
if error_count!=0{
println!("Error count: {error_count}");
println!("Errors: {}",errors.convert_errors);
}
},
Err(e)=>println!("Convert error: {e:?}"), Err(e)=>println!("Convert error: {e:?}"),
} }
}); });

View File

@@ -452,7 +452,7 @@ fn bsp_contents(path:PathBuf)->AResult<()>{
} }
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)] #[expect(dead_code)]
enum ConvertError{ enum ConvertError{
IO(std::io::Error), IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error), SNFMap(strafesnet_snf::map::Error),
@@ -484,7 +484,7 @@ async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output
Ok(()) Ok(())
} }
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{ async fn source_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
let start=std::time::Instant::now(); let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get(); let thread_limit=std::thread::available_parallelism()?.get();

View File

@@ -28,5 +28,12 @@ strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", o
strafesnet_session = { path = "../engine/session", registry = "strafesnet" } strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" } strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true } strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "25.0.0" wgpu = "28.0.0"
winit = "0.30.7" winit = "0.30.7"
[profile.dev]
strip = false
opt-level = 3
[lints]
workspace = true

View File

@@ -3,7 +3,7 @@ use std::io::Read;
#[cfg(any(feature="roblox",feature="source"))] #[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode; use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum ReadError{ pub enum ReadError{
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
@@ -63,7 +63,7 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
} }
} }
#[allow(dead_code)] #[expect(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum LoadError{ pub enum LoadError{
ReadError(ReadError), ReadError(ReadError),
@@ -97,11 +97,16 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)), ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{ ReadFormat::Roblox(model)=>{
let mut place=model.into_place(); let mut place=strafesnet_rbx_loader::Place::from(model);
place.run_scripts(); let script_errors=place.run_scripts().unwrap();
Ok(LoadFormat::Map( for error in script_errors{
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)? println!("Script error: {error}");
)) }
let (map,errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?;
if errors.count()!=0{
print!("Errors encountered while loading the map:\n{}",errors);
}
Ok(LoadFormat::Map(map))
}, },
#[cfg(feature="source")] #[cfg(feature="source")]
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map( ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(

View File

@@ -21,7 +21,7 @@ WorkerDescription{
pub fn new( pub fn new(
mut graphics:graphics::GraphicsState, mut graphics:graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration, mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface, surface:wgpu::Surface<'_>,
device:wgpu::Device, device:wgpu::Device,
queue:wgpu::Queue, queue:wgpu::Queue,
)->crate::compat_worker::INWorker<'_,Instruction>{ )->crate::compat_worker::INWorker<'_,Instruction>{

View File

@@ -77,5 +77,8 @@ pub fn new<'a>(
run_session_instruction!(ins.time,SessionInstruction::LoadReplay(bot)); run_session_instruction!(ins.time,SessionInstruction::LoadReplay(bot));
} }
} }
//whatever just do it
session.debug_raycast_print_model_id_if_changed(ins.time);
}) })
} }

View File

@@ -52,7 +52,7 @@ impl<'a> SetupContextPartial2<'a>{
let required_features=required_features(); let required_features=required_features();
//no helper function smh gotta write it myself //no helper function smh gotta write it myself
let adapters=self.instance.enumerate_adapters(self.backends); let adapters=pollster::block_on(self.instance.enumerate_adapters(self.backends));
let mut chosen_adapter=None; let mut chosen_adapter=None;
let mut chosen_adapter_score=0; let mut chosen_adapter_score=0;
@@ -119,12 +119,13 @@ impl<'a> SetupContextPartial3<'a>{
let (device, queue)=pollster::block_on(self.adapter let (device, queue)=pollster::block_on(self.adapter
.request_device( .request_device(
&wgpu::DeviceDescriptor { &wgpu::DeviceDescriptor{
label: None, label:None,
required_features: (optional_features & self.adapter.features()) | required_features, required_features:(optional_features&self.adapter.features())|required_features,
required_limits: needed_limits, required_limits:needed_limits,
memory_hints:wgpu::MemoryHints::Performance, memory_hints:wgpu::MemoryHints::Performance,
trace: wgpu::Trace::Off, trace:wgpu::Trace::Off,
experimental_features:wgpu::ExperimentalFeatures::disabled(),
}, },
)) ))
.expect("Unable to find a suitable GPU adapter!"); .expect("Unable to find a suitable GPU adapter!");

View File

@@ -86,6 +86,29 @@ fn vs_entity_texture(
return result; return result;
} }
@group(1)
@binding(0)
var<uniform> model_instance: ModelInstance;
struct DebugEntityOutput {
@builtin(position) position: vec4<f32>,
@location(1) normal: vec3<f32>,
@location(2) view: vec3<f32>,
};
@vertex
fn vs_debug(
@location(0) pos: vec3<f32>,
@location(1) normal: vec3<f32>,
) -> DebugEntityOutput {
var position: vec4<f32> = model_instance.transform * vec4<f32>(pos, 1.0);
var result: DebugEntityOutput;
result.normal = model_instance.normal_transform * normal;
result.view = position.xyz - camera.view_inv[3].xyz;//col(3)
result.position = camera.proj * camera.view * position;
return result;
}
//group 2 is the skybox texture //group 2 is the skybox texture
@group(1) @group(1)
@binding(0) @binding(0)
@@ -110,3 +133,8 @@ fn fs_entity_texture(vertex: EntityOutputTexture) -> @location(0) vec4<f32> {
let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb; let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb;
return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d)); return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d));
} }
@fragment
fn fs_debug(vertex: DebugEntityOutput) -> @location(0) vec4<f32> {
return model_instance.color;
}

View File

@@ -188,7 +188,7 @@ impl WindowContext<'_>{
} }
} }
fn device_event(&mut self,time:SessionTime,event: winit::event::DeviceEvent){ fn device_event(&mut self,time:SessionTime,event:winit::event::DeviceEvent){
match event{ match event{
winit::event::DeviceEvent::MouseMotion{ winit::event::DeviceEvent::MouseMotion{
delta, delta,

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