Compare commits

...

351 Commits

Author SHA1 Message Date
6e6bafe719
use .entry() match in generate_models 2025-05-02 19:36:34 -07:00
5f0ddc2f28
stop using transmute and document unsafe replacement code 2025-04-27 15:16:57 -07:00
ba1c1ec8c6
roblox_emulator: move lifetime inside PhantomData, saving 8 bytes 2025-04-24 13:51:28 -07:00
67cafd8cbb
update deps including Decal.TextureContent 2025-04-23 18:05:09 -07:00
ca88eb1cad
roblox_emulator: convert numbers to string in Instance.__newindex 2025-04-23 17:28:08 -07:00
2bf34fd04c
roblox_emulator: move coerce method onto CoerceEnum struct 2025-04-23 17:19:59 -07:00
b5da30fd9a
silence some warnings 2025-04-23 16:07:28 -07:00
95fffbbf42
roblox_emulator: bump version 2025-04-23 16:04:03 -07:00
9a636d5b50
roblox_emulator: Terrain.FillCylinder 2025-04-23 15:52:46 -07:00
2c8954c2b4
roblox_emulator: add Instance.Destroy aliases 2025-04-23 15:52:46 -07:00
985a703811
roblox_emulator: add CFrame.new(nil) constructor 2025-04-23 15:43:59 -07:00
37dbe35222
roblox_emulator: coerce enums in Terrain functions 2025-04-23 15:41:31 -07:00
f0de24d161
roblox_emulator: more BrickColor constructors 2025-04-23 15:39:34 -07:00
7d5ff55803
roblox_emulator: add Terrain:FillBall stub 2025-04-23 15:27:24 -07:00
7fffe05751
roblox_emulator: Color3.FromRGB alias 2025-04-23 15:22:16 -07:00
2ac38efef6
roblox_emulator: ClickDetector.MouseClick stub 2025-04-23 15:03:10 -07:00
5881e593b2
roblox_emulator: implement Instance.GetFullName 2025-04-23 15:03:10 -07:00
64a4499544
roblox_emulator: export mlua error 2025-04-23 15:03:10 -07:00
fca9e1c325
roblox_emulator: implement Error trait for ServicesError 2025-04-23 15:03:10 -07:00
6a88003b09
roblox_emulator: CFrame.FromEulerAnglesXYZ aliases 2025-04-23 15:03:10 -07:00
08b358c192
roblox_emulator: Vector2 2025-04-23 14:19:18 -07:00
62d9bcff81
roblox_emulator: implement __newindex CFrame 2025-04-23 14:12:21 -07:00
08f9163605
roblox_emulator: Tween Create & Play stub 2025-04-23 14:07:52 -07:00
b507624d91
roblox_emulator: shove tick into task module 2025-04-23 13:56:47 -07:00
d8358ec25c
roblox_emulator: create task module 2025-04-23 13:56:47 -07:00
e673a12beb
roblox_emulator: support more Vector3.new argument variants 2025-04-23 13:56:47 -07:00
18269423a5
roblox_emulator: implement TweenInfo 2025-04-23 13:56:47 -07:00
1d82799400
roblox_emulator: implement CoerceEnum 2025-04-23 13:28:47 -07:00
3a9fdebb4d
roblox_emulator: refactor Enums 2025-04-23 13:28:47 -07:00
f8ef17e3f5
roblox_emulator: TweenService:Create() stub 2025-04-23 12:23:35 -07:00
8b2f37d3d5
roblox_emulator: Sound:Play() stub 2025-04-23 12:23:21 -07:00
16dfe7524f
roblox_emulator: deduplicate static_ustr 2025-04-23 12:11:02 -07:00
3d399635d7
roblox_emulator: refactor nil instances 2025-04-23 12:04:46 -07:00
8bfc201d1f
roblox_emulator: fix EnumItems.GetEnumItems 2025-04-23 12:04:46 -07:00
bb8e131464
roblox_emulator: use extended instances 2025-04-23 11:42:11 -07:00
05bbe05979
use forked rbx-dom for extended instances 2025-04-23 11:17:16 -07:00
17a2199f36
roblox_emulator: add ScriptSignal:wait alias for :Wait 2025-04-23 02:12:31 -07:00
84db6503f9
roblox_emulator: implement EnumItem.__eq 2025-04-23 02:00:09 -07:00
5b5f356863
roblox_emulator: implement __index Enum 2025-04-23 01:54:18 -07:00
d07571519e
roblox_emulator: remove EnumItem::new 2025-04-23 01:38:51 -07:00
6464343428
roblox_emulator: rename Enum stuff to somewhat match Roblox 2025-04-23 01:38:27 -07:00
534b45d7dd
roblox_emulator: EnumItems:GetEnumItems & Enum.Name 2025-04-23 01:32:10 -07:00
3ff8ccf58d
roblox_emulator: implement Instance event stubs 2025-04-23 01:17:08 -07:00
67d2a3e398
roblox_emulator: implement __newindex UDim2 2025-04-23 01:15:08 -07:00
c1f8a13888
roblox_emulator: implement Vector3.__mul 2025-04-23 01:15:08 -07:00
e11af82443
roblox_emulator: tweak From impls 2025-04-23 01:15:08 -07:00
5b1d5502d8
roblox_emulator: fixup CFrame 2025-04-23 01:01:30 -07:00
f48d86fe56
roblox_emulator: replace coerce_float32 with a special Number type 2025-04-23 01:01:30 -07:00
877964be1f
roblox_emulator: implement case aliases for CFrame 2025-04-22 22:53:37 -07:00
62d983ef2a
roblox_emulator: implement NumberRange 2025-04-22 22:49:57 -07:00
8b7a4ee8c1
roblox_emulator: fixup NumberSequence 2025-04-22 22:49:41 -07:00
7ebcfa7bc3
roblox_emulator: stub BasePart.Touched & TouchEnded 2025-04-22 22:29:36 -07:00
c98f53a151
roblox_emulator: implement IsAncestorOf & IsDescendantOf 2025-04-22 22:26:46 -07:00
94025b52e2
roblox_emulator: .parent alias to .Parent 2025-04-22 21:43:57 -07:00
213d2f3f6a
roblox_emulator: Vector3.__sub + .Unit 2025-04-22 21:35:26 -07:00
438a22bd86
roblox_emulator: fixup CFrame 2025-04-22 21:33:02 -07:00
e6fb8437b3
roblox_emulator: implement __newindex Int32 2025-04-22 21:26:26 -07:00
8b6166f48e
roblox_emulator: support Workspace & MaterialService via GetService 2025-04-22 21:26:26 -07:00
3f89f2c1c5
roblox_emulator: implement FindFirstChildWhichIsA 2025-04-22 21:19:24 -07:00
c6ebb179a1
roblox_emulator: use common function pointer for lazy user data 2025-04-22 21:06:18 -07:00
14fa450e3e
roblox_emulator: implement Players service stub 2025-04-22 21:04:29 -07:00
448f40a5d8
roblox_emulator: stub Terrain.SetMaterialColor 2025-04-22 20:57:09 -07:00
4ecdcec17c
roblox_emulator: implement Instance.Clone 2025-04-22 20:53:49 -07:00
261b88ada6
roblox_emulator: isA alias for IsA 2025-04-22 19:56:56 -07:00
5ea0a1b07d
roblox_emulator: implement __newindex Instance 2025-04-22 19:42:54 -07:00
3b8d32913e
roblox_emulator: implement __index Color3 2025-04-22 19:42:35 -07:00
3928431ac7
roblox_emulator: implement __index String 2025-04-22 19:29:06 -07:00
6ecf403a4f
roblox_emulator: add :connect alias to ScriptSignal.Connect 2025-04-22 19:28:26 -07:00
1f7ee20c1e
roblox_emulator: implement Stepped, Heartbeat for RunService 2025-04-22 19:22:01 -07:00
19c30c8701
roblox_emulator: implement :children alias for GetChildren 2025-04-22 19:18:46 -07:00
dabb25b3d3
roblox_emulator: use coerce_float32 in CFrame 2025-04-22 19:15:26 -07:00
78593200eb
roblox_emulator: fixup Vector3 2025-04-22 19:15:04 -07:00
7649d30b55
roblox_emulator: do not take UserData 2025-04-22 19:11:24 -07:00
6af8dd3c1f
roblox_emulator: implement upper case methods on Vector3 2025-04-22 18:52:55 -07:00
861a8afc47
roblox_emulator: remove Vector3 component setters 2025-04-22 18:52:24 -07:00
f0b2470039
roblox_emulator: implement :service alias for GetService 2025-04-22 18:44:39 -07:00
c0ad20d54c
roblox_emulator: implement UDim & UDim2 2025-04-22 18:44:21 -07:00
9aea73e134
roblox_emulator: implement BrickColor 2025-04-22 18:22:48 -07:00
b34d3f89f9
roblox_emulator: implement __index bool 2025-04-22 18:12:47 -07:00
5600948d8a
roblox_emulator: fix error message 2025-04-22 18:12:47 -07:00
d722bcb46f
roblox_emulator: implement __newindex ContentId 2025-04-22 18:12:47 -07:00
081c95190f
roblox_emulator: ignore disabled scripts and ModuleScripts 2025-04-22 18:02:58 -07:00
d1f4e2132f
common: do a minute amount of error reporting 2025-04-22 17:47:07 -07:00
5317d96243
roblox_emulator: rename Instance constructors 2025-04-22 17:28:44 -07:00
8bb20ffe81
roblox_emulator: nil instances 2025-04-22 17:28:44 -07:00
03b16db10a
roblox_emulator: use a type alias to minimize errors 2025-04-22 16:08:45 -07:00
f0d4915fba
roblox_emulator: move services into Context 2025-04-22 15:31:56 -07:00
6e8da50b35
rbx_loader: tweak model read 2025-04-22 14:55:20 -07:00
615372aad5
rbx_loader: stronger Place & Model types 2025-04-22 14:55:20 -07:00
08d47b0f63
roblox_emulator: do not create ustr if there is going to be an error 2025-04-22 14:00:30 -07:00
08b5445838
roblox_emulator: fix subtle ustr bugs 2025-04-22 14:00:29 -07:00
fa07d16cf4
refactor some util functions to use new rbx-dom features 2025-04-18 11:17:03 -07:00
327688d79e
rbx-dom: guard rail ustr footguns 2025-04-18 11:17:03 -07:00
1662d814ec
rbx_loader: disable roblox_emulator run-service feature 2025-04-18 10:28:48 -07:00
a3d39a81d0
update rbx-dom 2025-04-17 13:35:04 -07:00
cb31c847d5
map-tool: code clean ups 2025-04-14 21:23:23 -07:00
f579370841
update deps 2025-04-14 21:23:23 -07:00
2a542e0026
remove pointless PhantomData 2025-04-13 21:23:21 -07:00
ca51256a16
deferred_loader: remove PhantomData using GATs 2025-04-13 20:11:47 -07:00
65e17da386 rbx_loader: deconstruct instead of index tuple (with vengeance) 2025-03-21 13:03:55 -07:00
1cf21be185 rbx_loader: rename primitive functions to reflect what it does 2025-03-21 12:55:04 -07:00
413e176741 todo 2025-03-21 12:53:29 -07:00
39b2397d5e rbx_loader: implement roblox cylinder 2025-03-21 12:51:53 -07:00
2ed01dffbd physics: TODO 2025-03-20 18:28:15 -07:00
d0ee597416 rbx_loader: refactor primitive types 2025-03-20 18:28:15 -07:00
be85530000 rbx_loader: primitives: code cleanup 2025-03-20 18:28:15 -07:00
55ff199269 physics: [BUG 6] update move state on collision end with non-walk contact while walking 2025-03-20 13:32:52 -07:00
02615af1fd common: integer: angle constructor 2025-03-19 16:34:42 -07:00
6594ffa87e common: integer: more zero constants 2025-03-19 16:34:38 -07:00
6295dc68ef physics: fix lint 2025-03-19 12:52:54 -07:00
8fb0d30e1d graphics: move get_instances_buffer_data away from the cool render code 2025-03-14 15:38:35 -07:00
0fb0230cb1 tools: create dev profile 2025-03-14 15:24:47 -07:00
bdb1090664 physics: implement precise width conversion 2025-03-13 17:06:23 -07:00
3ce7746489 snf: implement precise width conversion 2025-03-13 17:06:23 -07:00
7978050f8a rbx_loader: implement precise width conversion 2025-03-13 17:06:23 -07:00
77ee36fa72 bsp_loader: implement precise width conversion 2025-03-13 17:06:23 -07:00
0fc1ec3086 common: implement precise width conversion 2025-03-13 16:57:36 -07:00
f60c03ac56 common: update deps 2025-03-13 16:57:36 -07:00
b570d8809d linear_ops: v0.1.1 unwrap vector of results 2025-03-13 16:57:36 -07:00
3f268ec034 linear_ops: allow unwraping vector of results 2025-03-13 16:57:36 -07:00
ba161ef6d1 fixed_wide: v0.2.0 precise width conversion 2025-03-13 16:57:36 -07:00
0755d4413c fixed_wide: expand upon width conversion 2025-03-13 16:13:03 -07:00
459cb13957 physics: monomorphise MeshQuery over AsRef 2025-03-12 19:34:45 -07:00
a5ad7d4d6e it: cli 2025-03-12 17:23:21 -07:00
3d5e76f078 bsp_loader: fix utf8 brush model 2025-03-11 18:13:39 -07:00
78c53ae429 physics: set style on reset and spawn 2025-03-11 17:09:29 -07:00
e794b2649c bsp_loader: implement ladders and water 2025-03-11 16:41:46 -07:00
50f6fe5bd8 common: fix source styles 2025-03-11 15:26:45 -07:00
fc3681f41f it: read_entire_file use built in 2025-03-11 14:07:24 -07:00
a2369b4211 rbx_loader: never do &Vec + deconstruct Face2 (style) 2025-03-09 23:47:23 -07:00
bfea49ffae it: measure simulate speed 2025-03-08 17:39:47 -08:00
8c7063d340 common: aabb: make function inline-able 2025-03-08 14:34:54 -08:00
651150760c common: aabb: use midpoint for center 2025-03-08 14:34:54 -08:00
3d203e2da9 fixed_wide: add midpoint 2025-03-08 14:34:54 -08:00
3798f438cf use as_bits_mut to optimize sqrt 2025-03-08 13:58:56 -08:00
4ace8317fe update deps 2025-03-08 13:55:14 -08:00
ab4a9bb922 bsp_loader: generate prop matrix with euler angles 2025-03-02 18:23:47 -08:00
a9ef07ce78 update vbsp 2025-03-02 18:23:47 -08:00
561e41c760 common: delete unused ModesUpdate 2025-02-28 13:15:55 -08:00
9896a09576 common: delete updatable trait 2025-02-28 13:15:55 -08:00
82840fa6cb refactor type safety for modes data normalization 2025-02-28 13:15:55 -08:00
2c87cb71df common: gameplay_modes: rename internal field on ModeBuilder 2025-02-28 12:37:49 -08:00
709f622547 common: don't use .0 for newtypes 2025-02-28 12:10:16 -08:00
b20264b382 common: move ModesBuilder to common 2025-02-28 11:08:38 -08:00
17cce2d702 silence lints 2025-02-28 10:50:53 -08:00
a2b2ddc9ce bsp_loader: use texture_info function 2025-02-26 15:29:23 -08:00
cb57d893e0 update vbsp fork 2025-02-26 15:26:56 -08:00
9e8d66cec1 map-tool: gimme_them_textures iterate over a shorter list of prop model paths 2025-02-26 12:00:51 -08:00
39fe833198 map-tool: grab brush models 2025-02-26 12:00:51 -08:00
e7688a95fd bsp_loader: generate physics from brushes 2025-02-26 12:00:51 -08:00
d2cc98b04d deps: fork vbsp 2025-02-26 11:59:59 -08:00
9e887580af bsp_loader: valve_transform_{dist|normal} 2025-02-21 13:18:20 -08:00
92feac572e bsp_loader: include missing model path in error 2025-02-21 13:18:20 -08:00
fd02a40783 common: CollisionAttributes::intersect_default 2025-02-21 13:18:20 -08:00
7c787a0e0f linear_ops: repr(transparent) to ward off UB 2025-02-21 13:18:20 -08:00
6a7c076203 Planar64Affine3::IDENTITY 2025-02-21 13:18:20 -08:00
af3abbcb4d Planar64Affine3::from_translation 2025-02-21 13:14:57 -08:00
4859c37780 common: BvhNode::sample_ray 2025-02-21 12:54:13 -08:00
19e65802f6 common: Aabb::contains(point) 2025-02-21 12:54:13 -08:00
2cf1547423 common: Time is Ord 2025-02-21 12:54:13 -08:00
63305f91c7 common: Ray module 2025-02-21 12:54:13 -08:00
e875826250 TransformedMesh::faces 2025-02-21 12:54:13 -08:00
0a44c1630f ratio_ops v0.1.1 fix Ord 2025-02-21 12:54:13 -08:00
5cffc03ef6 ratio_ops: fix Ord for Ratio 2025-02-21 12:53:14 -08:00
d638e633ba update deps 2025-02-20 19:02:03 -08:00
61e44f2aba upgrade rust edition to 2024 2025-02-20 18:58:01 -08:00
347b1176d2 update quat pc symbolic links 2025-02-13 09:50:42 -08:00
7c0ad5b601 common: integer: Time <-> Ratio 2025-02-06 13:35:43 -08:00
62851bbd60 common: instruction: relax InstructionCollector trait bounds 2025-02-06 12:39:38 -08:00
b3a6d08656 silence millions of lints in unused module 2025-02-06 12:26:56 -08:00
5409548348 common: don't use TimeInner as Instruction generics 2025-02-06 12:26:17 -08:00
57552c1a6a common: bvh: rename generics 2025-02-06 12:12:16 -08:00
aace3bb2a3 common: bvh: use sort_by_key 2025-02-06 11:20:35 -08:00
4899003766 common: bvh: rip the_tester 2025-02-06 10:37:22 -08:00
0c991715ab common: bvh: empty not Default 2025-02-06 10:27:00 -08:00
72e0caa84a common: evict cursed BvhWeightNode 2025-02-06 10:21:24 -08:00
4805d237e2 map-tool: lower case all the things 2025-02-04 13:12:28 -08:00
8e91fbb08e bsp_loader: case folding 2025-02-04 12:57:28 -08:00
a2a69c48d6 map-tool: handle error paths properly 2025-02-04 12:27:14 -08:00
e74535d91f map-tool: no allocate trick 2025-02-04 12:27:14 -08:00
dbca48f051 bsp_loader: Bsp named field 2025-02-04 12:01:39 -08:00
ad6584d82a map-tool: search for more files 2025-02-04 11:58:19 -08:00
ceb5fff333 map-tool: use paths for deps 2025-02-04 11:57:25 -08:00
c6f31a3eb5 bsp_loader: deduplicate graphics groups by render id 2025-02-04 10:07:41 -08:00
6c81fc43bb bsp_loader: unused remnant 2025-02-04 10:00:08 -08:00
5317cd0609 bsp_loader: models are baked for graphics, physics comes from brushes 2025-02-04 09:53:39 -08:00
09d151ee1c update deps 2025-02-04 09:28:18 -08:00
441b9a8559 add map tool to workspace 2025-02-04 09:27:38 -08:00
707ff98d27 Merge branch 'map-tool' 2025-02-04 09:25:36 -08:00
2c1910bc24 map-tool: move into folder 2025-02-04 09:24:35 -08:00
614535ce8c rbxassetid: add crate stuff and test 2025-02-04 09:19:05 -08:00
78c29657d6 snf: 0.3.0 bot files 2025-02-04 09:19:05 -08:00
9d9695f363 specify explicit versions 2025-02-04 09:19:05 -08:00
ce3d8fde11 fixed_wide: 0.1.2 2025-02-04 09:19:05 -08:00
1ddc013fd0 add unions symlink to tools 2025-02-04 09:19:05 -08:00
d0d2e6eca3 tweak primitives 2025-02-04 09:19:05 -08:00
1ba7b0591b refactor loading and implement unions 2025-02-04 09:19:05 -08:00
205db9a0db v0.7.0 loader rewrite 2025-02-04 09:09:10 -08:00
ca50bf35c2 loader + tokio rewrite 2025-02-04 09:09:01 -08:00
6522c255cd remove common 2025-02-04 09:06:27 -08:00
6b4122c59c deferred_loader: view indices without operating a Loader 2025-01-30 07:31:02 -08:00
eb0365217d move conversion code into loader libs 2025-01-29 16:26:38 -08:00
c128046d88 bump library versions 2025-01-29 14:41:59 -08:00
ff57825232 restore symbolic links for loading textures and meshes 2025-01-29 14:41:59 -08:00
8e867449f0 implement new bsp_loader 2025-01-29 14:41:59 -08:00
f028dc6c85 implement new rbx_loader 2025-01-29 14:41:59 -08:00
85480cb13e impl deferred_loader::Loader for bsp_loader 2025-01-29 14:41:59 -08:00
a594f77453 impl deferred_loader::Loader for rbx_loader 2025-01-29 14:38:12 -08:00
c356a9d654 move rbxassetid into lib 2025-01-29 14:38:12 -08:00
23ee771c22 refactor deferred_loader 2025-01-29 14:38:12 -08:00
a5079f21d7 wip download_assets (pre-tokio) 2025-01-27 08:48:11 -08:00
349cd9c233 v1.6.0 split roblox and source 2025-01-27 07:51:39 -08:00
d455cf4dc9 Add strafesnet registry 2025-01-27 07:51:39 -08:00
3227a6486a update deps 2025-01-27 07:51:39 -08:00
1ce51dd4da split commands into roblox and source 2025-01-27 07:45:38 -08:00
1ad9723905 move writeattributes to mapfixer 2025-01-27 07:04:05 -08:00
be9eb30a5c minimize lints 2025-01-24 05:20:22 -08:00
42310c5d2b convert to app 2025-01-23 06:07:46 -08:00
988750a3c6 physics: typo 2025-01-22 14:52:47 -08:00
2f26662dda it: drop strafe-client::file dependency 2025-01-22 09:37:16 -08:00
6f739dba8d add integration testing 2025-01-22 09:36:35 -08:00
2eaddd493d session: use replay directory 2025-01-22 08:57:15 -08:00
d5a6f8e1bc compute UserSettings from directories 2025-01-22 08:54:05 -08:00
95fb43b908 settings: implement directories 2025-01-22 08:52:27 -08:00
3e814cb41a settings: add directories dep 2025-01-22 07:47:41 -08:00
ea854a548f Update README.md 2025-01-22 07:10:04 -08:00
5f2cf8f32e physics: change body api 2025-01-22 07:00:42 -08:00
b6b090de78 physics: place version code into lib.rs 2025-01-22 06:45:56 -08:00
affbada62e move replay tests into test module 2025-01-22 05:48:08 -08:00
8d2ba28700 move engine components into modules 2025-01-22 05:48:08 -08:00
3eb4e76ab2 replace .map_or(None, with .and_then( 2025-01-22 05:47:32 -08:00
5d31419370 use Range to express time range 2025-01-22 05:47:32 -08:00
44a58044c7 readme: try it out 2025-01-22 05:47:32 -08:00
83ac776b78 Revert "bodge surfs"
This reverts commit 65b49d27261754903b2e39194c80ac096afc1da2.
2025-01-22 05:47:32 -08:00
41b28fa7d2 v1.5.7 update rbx_loader 2024-10-04 20:04:13 -07:00
a2ab23097b update rbx_loader 2024-10-04 20:03:11 -07:00
602061b44c v1.5.6 update rbx_loader 2024-10-03 20:35:54 -07:00
1989369956 update rbx_loader 2024-10-03 20:33:59 -07:00
a18aea828c v1.5.5 update deps 2024-10-01 17:24:43 -07:00
b7000ee9af update deps 2024-10-01 17:20:43 -07:00
2b77ea5712 v1.5.4 update to asset-tool 0.4.X + improve asset id parsing 2024-10-01 13:15:48 -07:00
cf98f8e7bb use rbxassetid parser from deferred_loader 2024-10-01 13:02:54 -07:00
a56c114d08
Automatically create sub-directories when downloading assets; move dds files into the textures folder
the latter change is because roblox-to-snf expects the files in textures, not textures/dds
2024-09-25 23:48:59 +01:00
b6a5324ae7
Fix asset-tool invocations when downloading assets 2024-09-25 23:43:54 +01:00
6f5a3c5176 v1.5.3 roblox emulator 2024-09-21 13:48:23 -07:00
6bab31f3b3 silence dead code 2024-09-21 13:47:40 -07:00
9cdeed160f update rbx_loader & run scripts 2024-09-21 13:45:32 -07:00
d0c59b51a4 v1.5.2 2024-07-31 11:51:38 -07:00
451f3ccecb source to snf 2024-07-31 11:51:18 -07:00
ed9701981d limit parallel threads by waiting for the first thread to complete 2024-07-30 12:24:23 -07:00
60e0197344 v1.5.1 2024-07-29 16:48:02 -07:00
4d97a490c1 convert snf 2024-07-29 16:48:02 -07:00
52ba44c6be named args 2024-04-19 00:44:05 -07:00
95b6272b18 more texture sources + use asset tool to download meshes & textures 2024-04-19 00:44:05 -07:00
0172675b04 v1.5.0 rewrite clap usage + remove mapfixer stuff 2024-03-08 10:43:36 -08:00
982b4aecac rewrite clap usage 2024-03-08 10:43:36 -08:00
c1ddcdb0c5 remove mapfixer + asset-tool functions 2024-03-08 10:43:36 -08:00
c2d0a4487c misc edits 2024-03-08 10:01:54 -08:00
dc9fd2c442 import PathBuf 2024-03-08 09:55:17 -08:00
4199d41d3f timeless License 2024-01-30 18:38:47 -08:00
7fbcb206ff probably was wrong but idc about testing it 2024-01-30 16:39:57 -08:00
a17901d473 v1.4.0 valve maps 2024-01-12 11:34:09 -08:00
b88c6b899a commands for valve maps 2024-01-12 11:32:12 -08:00
835d4bbecd add valve map deps 2024-01-12 11:32:12 -08:00
b756dc979c move main to top 2024-01-12 11:32:12 -08:00
1e888ebb01 tabs 2024-01-09 01:36:14 -08:00
b9dccb1af5 update deps 2023-11-18 16:13:24 -08:00
c6d293cc6b write "Ref" attribute based on internal roblox part id 2023-11-18 15:53:37 -08:00
a386f90f51 switch to lazy_regex 2023-11-18 15:47:54 -08:00
43115cbac6 mesh downloader 2023-11-09 15:51:23 -08:00
35b5aff9a7 stop being cringe 2023-11-09 15:51:11 -08:00
36419af870 update deps 2023-10-27 18:10:11 -07:00
a7518bef46 print full path 2023-10-27 18:10:05 -07:00
6df1f41599 not a bug 2023-10-19 03:00:30 -07:00
422d0a160d print special message for roblox xml 2023-10-19 03:00:30 -07:00
1727f9213c it's actually jfif 2023-10-19 03:00:30 -07:00
afa9e7447d print fourcc on unrecognized image format 2023-10-19 03:00:30 -07:00
ff85efa54f add unzip-all command 2023-10-19 03:00:30 -07:00
fa69c53cfc JPEG support 2023-10-19 03:00:30 -07:00
a57c228580 styling 2023-10-19 03:00:30 -07:00
5dc69db885 extremely fancy code to transparently handle gzip 2023-10-19 03:00:30 -07:00
e54400a436 alphabetize for no reason 2023-10-19 03:00:30 -07:00
e2a5edf8df Fill in placeholder values
Nobody told me I was chilling with placeholder values in my license
2023-10-02 03:46:17 -04:00
d6dd1b8abd drop Jeftai Error, convert errors to anyhow 2023-10-01 20:08:43 -07:00
a2b793fcd3 add anyhow dep 2023-10-01 20:07:11 -07:00
9cb34f14c8 fixme 2023-09-30 03:38:34 -07:00
bd2e3aa2d3 v1.3.0 srgb textures + non mod 4 images export to uncompressed + rbxmx support 2023-09-30 03:15:57 -07:00
07f6053839 multiplex roblox format 2023-09-30 03:14:06 -07:00
0d5b918ea1 add rbx_xml dep 2023-09-30 03:14:05 -07:00
20a568220a use plain format if image is not the right shape 2023-09-29 18:30:01 -07:00
d670d4129e use srgb 2023-09-29 13:27:23 -07:00
de7b0bd5cc stop failing when maps are corrupted while downloading textures 2023-09-29 03:14:29 -07:00
01524146c7 fixme 2023-09-29 02:58:49 -07:00
45e8e415d0 v1.2.0 convert-textures
gzip no workie yet
some pngs upset the decoder lib
2023-09-29 02:58:49 -07:00
4417bafc5c naive multithreaded convert downloaded textures to dds 2023-09-29 02:58:32 -07:00
8553625738 change texture download path 2023-09-28 22:05:25 -07:00
3a3749eaeb add deps image, gzip (flate2), image_dds 2023-09-28 22:05:00 -07:00
53539f290b print map file 2023-09-23 13:07:52 -07:00
479dd37f53 v1.1.0 download-textures 2023-09-22 14:27:06 -07:00
34b6a869f0 add download textures command 2023-09-22 14:24:28 -07:00
19a455ee5e print wget exit status 2023-09-22 14:23:29 -07:00
9904b7a044 switch extract to pathbuf 2023-09-22 14:22:44 -07:00
6efa811eb6 generalize recursive_collect 2023-09-22 14:21:22 -07:00
81e4a201bd add delete to interactive upload 2023-09-15 17:10:33 -07:00
8fd5618af2 tweak prompt 2023-09-14 17:05:22 -07:00
54c26d6e1e ARE YOU STUPID 2023-09-14 16:23:53 -07:00
110ec94a08 v1.0.0 map tool implements some subset of operations at every step the map verification process from start to finish 2023-09-14 16:23:53 -07:00
980da5a6a7 write approximate code for uploading new asset 2023-09-14 16:22:15 -07:00
1cd77984d4 wait for downloads before exiting 2023-09-14 14:32:25 -07:00
b0fe231388 Merge pull request 'stop macro from returning' () from Jeftaei/map-tool:master into master
Reviewed-on: 
2023-09-14 17:19:01 -04:00
5a4a39ab75 add upload action + wait for status code 2023-09-14 14:17:00 -07:00
Jeft
1b2324deeb stop macro from returning 2023-09-14 16:15:39 -05:00
4c485e76e4 implement upload 2023-09-14 13:54:08 -07:00
7bbb9ca24f have to write the model ref to make a valid model file 2023-09-14 13:53:52 -07:00
eff55af1b4 models are rbxm 2023-09-14 12:06:02 -07:00
0d05cc9996 comment on laziness 2023-09-13 21:22:28 -07:00
2a55ef90df don't replace std Result 2023-09-13 21:04:33 -07:00
1a6202ae66 Merge pull request 'Nice looking errors' () from Jeftaei/map-tool:master into master
Reviewed-on: 
2023-09-13 16:51:24 -04:00
Jeft
742f7b4ec0 we Love errors 2023-09-13 09:13:23 -05:00
2cb346f49a re-prompt action 2023-09-13 00:28:59 -07:00
e5cca9ed04 common location 2023-09-12 19:29:51 -07:00
52d911a25a implement Exit/Delete 2023-09-12 19:29:51 -07:00
7ab20f36a7 fix parse 2023-09-12 19:05:01 -07:00
a7554da1c5 flush prompt 2023-09-12 19:05:01 -07:00
37f0dad7a1 ref cannot refer to object in another dom 2023-09-12 19:05:01 -07:00
e309f15cb8 implement extract 2023-09-12 19:04:46 -07:00
29374e4ff5 write edited file and report how many scripts were replaced 2023-09-12 19:04:46 -07:00
b7d04d1f40 this is wrong 2023-09-12 18:17:04 -07:00
432ec11ea6 fix write_dom 2023-09-12 18:17:04 -07:00
01449b1850 fix int parsing 2023-09-12 18:17:04 -07:00
327d0a4992 print map script stats on completion 2023-09-12 18:17:04 -07:00
420dbaa022 implement blocked in interactive 2023-09-12 18:17:04 -07:00
cad29af4bb panic lol 2023-09-12 18:17:04 -07:00
e0e8744bfd interactive mode v1 2023-09-12 18:17:04 -07:00
b434dce0f6 helper funcs 2023-09-12 18:17:04 -07:00
6ef8fd2f69 get_full_name 2023-09-12 18:17:04 -07:00
7234065bd8 fail better 2023-09-12 18:17:04 -07:00
41d8e700c5 Commands::Interactive 2023-09-12 18:17:04 -07:00
4ca3d56f0f upload comments 2023-09-12 18:17:04 -07:00
593b6902fd Scan::Flagged 2023-09-12 18:17:04 -07:00
7523c4313a scan enum, Source property missing if fatal 2023-09-12 18:17:04 -07:00
694440bd29 use wget to download maps 2023-09-12 18:17:04 -07:00
755e1d4d5b optimize release binary size 2023-09-12 18:17:04 -07:00
4334a6f330 v2 with clap 2023-09-12 18:17:04 -07:00
553ad2cca5 add clap dep 2023-09-12 18:17:03 -07:00
3f15d2f5a8 delete recursive function 2023-09-12 18:16:45 -07:00
206bb01c1a rename to map-tool 2023-09-05 00:43:05 -07:00
df24bc9945 collect referent instead of Source 2023-09-05 00:43:05 -07:00
4c1632c0b6 v1 POC (proof of concept) 2023-09-05 00:43:05 -07:00
9e482891ba Initial commit 2023-09-05 00:43:05 -07:00
38d1ff62ab Initial commit 2023-09-05 03:41:37 -04:00
141 changed files with 9276 additions and 3426 deletions

2780
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,5 +1,10 @@
[workspace]
members = [
"engine/graphics",
"engine/physics",
"engine/session",
"engine/settings",
"integration-testing",
"lib/bsp_loader",
"lib/common",
"lib/deferred_loader",
@ -7,8 +12,10 @@ members = [
"lib/linear_ops",
"lib/ratio_ops",
"lib/rbx_loader",
"lib/rbxassetid",
"lib/roblox_emulator",
"lib/snf",
"map-tool",
"strafe-client",
]
resolver = "2"
@ -17,3 +24,7 @@ resolver = "2"
#lto = true
strip = true
codegen-units = 1
[profile.dev]
strip = false
opt-level = 3

@ -3,6 +3,9 @@
# Strafe Project
Monorepo for working on projects related to strafe client.
## Try it out
See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for downloads.
## How to build and run
1. Have rust and git installed
2. `git clone https://git.itzana.me/StrafesNET/strafe-project`
@ -10,4 +13,4 @@ Monorepo for working on projects related to strafe client.
4. `cargo run --release --bin strafe-client`
## Licenses
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client has a sole proprietor license.
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client and engine crates have a sole proprietor license.

@ -0,0 +1,14 @@
[package]
name = "strafesnet_graphics"
version = "0.1.0"
edition = "2024"
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
ddsfile = "0.5.1"
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "25.0.0"

8
engine/graphics/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,9 +1,15 @@
use std::borrow::Cow;
use std::collections::{HashSet,HashMap};
use strafesnet_common::map;
use strafesnet_settings::settings;
use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
struct Indices{
count:u32,
@ -97,6 +103,26 @@ impl std::default::Default for GraphicsCamera{
}
}
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}
pub struct GraphicsState{
pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups,
@ -136,7 +162,7 @@ impl GraphicsState{
pub fn clear(&mut self){
self.models.clear();
}
pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){
pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){
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){
@ -448,7 +474,7 @@ impl GraphicsState{
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities
let mut model_count=0;
let mut instance_count=0;
let uniform_buffer_binding_size=crate::setup::required_limits().max_uniform_buffer_binding_size as usize;
let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize;
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
self.models.reserve(models.len());
for model in models.into_iter(){
@ -608,7 +634,7 @@ impl GraphicsState{
// Create the render pipeline
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
label:None,
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))),
});
//load textures
@ -636,10 +662,10 @@ impl GraphicsState{
wgpu::TextureFormat::Astc{
block:AstcBlock::B4x4,
channel:AstcChannel::UnormSrgb,
}=>&include_bytes!("../images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
_=>unreachable!(),
};
@ -682,7 +708,7 @@ impl GraphicsState{
//squid
let squid_texture_view={
let bytes=include_bytes!("../images/squid.dds");
let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
@ -864,7 +890,7 @@ impl GraphicsState{
&mut self,
device:&wgpu::Device,
config:&wgpu::SurfaceConfiguration,
user_settings:&crate::settings::UserSettings,
user_settings:&settings::UserSettings,
){
self.depth_view=Self::create_depth_texture(config,device);
self.camera.screen_size=glam::uvec2(config.width,config.height);
@ -875,7 +901,7 @@ impl GraphicsState{
view:&wgpu::TextureView,
device:&wgpu::Device,
queue:&wgpu::Queue,
frame_state:crate::session::FrameState,
frame_state:session::FrameState,
){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input
@ -961,22 +987,3 @@ impl GraphicsState{
self.staging_belt.recall();
}
}
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}

@ -0,0 +1,2 @@
pub mod model;
pub mod graphics;

10
engine/physics/Cargo.toml Normal file

@ -0,0 +1,10 @@
[package]
name = "strafesnet_physics"
version = "0.1.0"
edition = "2024"
[dependencies]
arrayvec = "0.7.6"
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

8
engine/physics/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -31,15 +31,23 @@ impl<T> Body<T>
time,
}
}
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1:self,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.position
+(self.velocity*dt).map(|elem|elem.divide().fix_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().fix_1())
+(self.velocity*dt).map(|elem|elem.divide().clamp_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_1())
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().fix_1())
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
}
pub fn advance_time(&mut self,time:Time<T>){
self.position=self.extrapolated_position(time);
@ -63,12 +71,12 @@ impl<T> Body<T>
D2:Copy,
Planar64:core::ops::Mul<D2,Output=N4>,
N4:integer::Divide<D2,Output=T1>,
T1:integer::Fix<Planar64>,
T1:integer::Clamp<Planar64>,
{
// a*dt^2/2 + v*dt + p
// (a*dt/2+v)*dt+p
(self.acceleration.map(|elem|dt*elem/2)+self.velocity).map(|elem|dt.mul_ratio(elem))
.map(|elem|elem.divide().fix())+self.position
.map(|elem|elem.divide().clamp())+self.position
}
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
@ -77,12 +85,12 @@ impl<T> Body<T>
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
T1:integer::Clamp<Planar64>,
{
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:crate::model_physics::GigaTime){
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();
@ -137,14 +145,6 @@ pub struct VirtualBody<'a,T>{
impl<T> VirtualBody<'_,T>
where Time<T>:Copy,
{
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}

@ -1,4 +1,4 @@
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge};
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
@ -57,13 +57,14 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}
}
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.face_edges(face_id).iter(){
for &directed_edge_id in mesh.face_edges(face_id).as_ref(){
let edge_n=mesh.directed_edge_n(directed_edge_id);
let n=n.cross(edge_n);
let verts=mesh.edge_verts(directed_edge_id.as_undirected());
let &[v0,v1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
//WARNING: d is moved out of the *2 block because of adding two vertices!
//WARNING: precision is swept under the rug!
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
@ -77,13 +78,15 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
let delta_pos=body.position*2-(mesh.vert(edge_verts[0])+mesh.vert(edge_verts[1]));
for (i,&edge_face_id) in mesh.edge_faces(edge_id).iter().enumerate(){
let &[ev0,ev1]=edge_verts.as_ref();
let delta_pos=body.position*2-(mesh.vert(ev0)+mesh.vert(ev1));
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
//WARNING yada yada d *2
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
@ -92,12 +95,12 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}
}
//test each vertex collision time, ignoring roots with zero or conflicting derivative
for (i,&vert_id) in edge_verts.iter().enumerate(){
for (i,&vert_id) in edge_verts.as_ref().iter().enumerate(){
//vertex normal gets parity from vert index
let n=edge_n*(1-2*(i as i64));
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
@ -108,12 +111,12 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
},
&FEV::Vert(vert_id)=>{
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.vert_edges(vert_id).iter(){
for &directed_edge_id in mesh.vert_edges(vert_id).as_ref(){
//edge is directed away from vertex, but we want the dot product to turn out negative
let n=-mesh.directed_edge_n(directed_edge_id);
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
@ -128,11 +131,11 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
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.fix_4(),r.den.fix_4())
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){

45
engine/physics/src/lib.rs Normal file

@ -0,0 +1,45 @@
mod body;
mod push_solve;
mod face_crawler;
mod model;
pub mod physics;
// Physics bug fixes can easily desync all bots.
//
// When replaying a bot, use the exact physics version which it was recorded with.
//
// When validating a new bot, ignore the version and use the latest version,
// and overwrite the version in the file.
//
// Compatible physics versions should be determined
// empirically at development time via leaderboard resimulation.
//
// Compatible physics versions should result in an identical leaderboard state,
// or the only bots which fail are ones exploiting a surgically patched bug.
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
pub struct PhysicsVersion(u32);
pub const VERSION:PhysicsVersion=PhysicsVersion(0);
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
let compat=[0];
let mut input_version=0;
while input_version<compat.len(){
// compatible version must be greater than or equal to the input version
assert!(input_version as u32<=compat[input_version]);
// compatible version must be a version that exists
assert!(compat[input_version]<=VERSION.0);
input_version+=1;
}
compat
};
pub enum PhysicsVersionError{
UnknownPhysicsVersion,
}
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
}else{
Err(PhysicsVersionError::UnknownPhysicsVersion)
}
}

@ -1,5 +1,5 @@
use std::borrow::{Borrow,Cow};
use std::collections::{HashSet,HashMap};
use core::ops::Range;
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@ -7,6 +7,13 @@ use strafesnet_common::physics::Time;
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
struct AsRefHelper<T>(T);
impl<T> AsRef<T> for AsRefHelper<T>{
fn as_ref(&self)->&T{
&self.0
}
}
pub trait UndirectedEdge{
type DirectedEdge:Copy+DirectedEdge;
fn as_directed(&self,parity:bool)->Self::DirectedEdge;
@ -68,27 +75,27 @@ struct Face{
}
struct Vert(Planar64Vec3);
pub trait MeshQuery{
type Face:Clone;
type Edge:Clone+DirectedEdge;
type Vert:Clone;
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
let verts=self.edge_verts(edge_id);
self.vert(verts[1].clone())-self.vert(verts[0].clone())
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
let verts=self.edge_verts(directed_edge_id.as_undirected());
(self.vert(verts[1].clone())-self.vert(verts[0].clone()))*((directed_edge_id.parity() as i64)*2-1)
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->Cow<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Cow<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>;
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
}
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
@ -371,8 +378,8 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}
//assume face hash is stable, and there are no flush faces...
let face=Face{
normal:(normal/len as i64).divide().fix_1(),
dot:(dot/(len*len) as i64).fix_1(),
normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
};
let face_id=match face_id_from_face.get(&face){
Some(&face_id)=>face_id,
@ -434,20 +441,20 @@ impl MeshQuery for PhysicsMeshView<'_>{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
self.data.verts[vert_idx].0
}
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
}
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].faces)
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].faces)
}
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].verts)
}
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.vert_topology[vert_id.get() as usize].edges.as_slice()
}
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.topology.vert_topology[vert_id.get() as usize].faces.as_slice()
}
}
@ -483,12 +490,15 @@ impl TransformedMesh<'_>{
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
}
pub fn faces(&self)->impl Iterator<Item=SubmeshFaceId>{
(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)|
.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.
@ -509,26 +519,27 @@ impl MeshQuery for TransformedMesh<'_>{
(transformed_n,transformed_d)
}
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
// wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
}
#[inline]
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
self.view.edge_faces(edge_id)
}
#[inline]
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
self.view.edge_verts(edge_id)
}
#[inline]
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.vert_edges(vert_id)
}
#[inline]
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.view.vert_faces(vert_id)
}
}
@ -617,12 +628,12 @@ impl MinkowskiMesh<'_>{
}
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{
let mut best_transition=Transition::Done;
for &directed_edge_id in self.vert_edges(vert_id).iter(){
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
let edge_verts=self.edge_verts(directed_edge_id.as_undirected());
//select opposite vertex
let test_vert_id=edge_verts[directed_edge_id.parity() as usize];
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
//test if it's closer
let diff=point-self.vert(test_vert_id);
if edge_n.dot(infinity_dir).is_zero(){
@ -638,7 +649,7 @@ impl MinkowskiMesh<'_>{
fn final_ev(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
let mut best_transition=EV::Vert(vert_id);
let diff=point-self.vert(vert_id);
for &directed_edge_id in self.vert_edges(vert_id).iter(){
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
//check if time of collision is outside Time::MIN..Time::MAX
@ -649,7 +660,8 @@ impl MinkowskiMesh<'_>{
if !d.is_negative()&&d<=edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
(c.dot(c)/edge_nn).divide().fix_2()
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<=*best_distance_squared{
best_transition=EV::Edge(directed_edge_id.as_undirected());
@ -685,10 +697,10 @@ impl MinkowskiMesh<'_>{
let edge_n=self.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
let &[v0,v1]=self.edge_verts(edge_id).borrow();
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v0)+self.vert(v1)
};
for (i,&face_id) in self.edge_faces(edge_id).iter().enumerate(){
for (i,&face_id) in self.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=self.face_nd(face_id).0;
//edge-face boundary nd, n facing out of the face towards the edge
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
@ -718,8 +730,8 @@ impl MinkowskiMesh<'_>{
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time,)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().map_or(None,|dir|{
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;
@ -729,23 +741,23 @@ impl MinkowskiMesh<'_>{
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&Body,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone(),start_time).map_or(None,|fev|{
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,start_time:Time,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
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).map_or(None,|fev|{
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,start_time:Time,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
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)
//determine when it passes an edge ("sliding off" case)
let start_time={
@ -754,19 +766,20 @@ impl MinkowskiMesh<'_>{
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).iter(){
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//f x e points in
let n=face_n.cross(edge_n);
let verts=self.edge_verts(directed_edge_id.as_undirected());
let d=n.dot(self.vert(verts[0])+self.vert(verts[1]));
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
let d=n.dot(self.vert(v0)+self.vert(v1));
//WARNING! d outside of *2
//WARNING: truncated precision
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_edge=Some(directed_edge_id);
@ -807,12 +820,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let edge0_n=self.mesh0.edge_n(e0);
let edge1_n=self.mesh1.edge_n(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).borrow();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).borrow();
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
let n=edge0_n.cross(edge1_n);
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
((n*(parity as i64*4-2)).fix_3(),((e0d-e1d)*(parity as i64*2-1)).fix_4())
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
},
MinkowskiFace::FaceVert(f0,v1)=>{
let (n,d)=self.mesh0.face_nd(f0);
@ -827,44 +840,44 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
self.mesh1.face_edges(f1).as_ref().iter().map(|&edge_id1|
MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse())
}).collect())
).collect()
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let e0v=self.mesh0.edge_verts(e0);
let e1v=self.mesh1.edge_verts(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
//could sort this if ordered edges are needed
//probably just need to reverse this list according to parity
Cow::Owned(vec![
MinkowskiDirectedEdge::VertEdge(e0v[0],e1.as_directed(parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v[0]),
MinkowskiDirectedEdge::VertEdge(e0v[1],e1.as_directed(!parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v[1]),
])
vec![
MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0),
MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1),
]
},
MinkowskiFace::FaceVert(f0,v1)=>{
Cow::Owned(self.mesh0.face_edges(f0).iter().map(|&edge_id0|{
self.mesh0.face_edges(f0).as_ref().iter().map(|&edge_id0|
MinkowskiDirectedEdge::EdgeVert(edge_id0,v1)
}).collect())
).collect()
},
}
}
fn edge_faces(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiFace;2]>{
fn edge_faces(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiFace;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
//faces are listed backwards from the minkowski mesh
let v0e=self.mesh0.vert_edges(v0);
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).borrow();
Cow::Owned([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
for &directed_edge_id0 in v0e.iter(){
for &directed_edge_id0 in v0e.as_ref(){
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
//must be behind other face.
let d=edge_face1_n.dot(edge0_n);
@ -888,13 +901,13 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiEdge::EdgeVert(e0,v1)=>{
//tracking index with an external variable because .enumerate() is not available
let v1e=self.mesh1.vert_edges(v1);
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).borrow();
Cow::Owned([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in v1e.iter(){
for &directed_edge_id1 in v1e.as_ref(){
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
let d=edge_face0_n.dot(edge1_n);
if d.is_negative(){
@ -914,31 +927,27 @@ impl MeshQuery for MinkowskiMesh<'_>{
},
}
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->Cow<[MinkowskiVert;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
Cow::Owned(self.mesh1.edge_verts(e1).map(|vert_id1|{
MinkowskiVert::VertVert(v0,vert_id1)
}))
},
MinkowskiEdge::EdgeVert(e0,v1)=>{
Cow::Owned(self.mesh0.edge_verts(e0).map(|vert_id0|{
MinkowskiVert::VertVert(vert_id0,v1)
}))
},
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
}
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
fn vert_edges(&self,vert_id:MinkowskiVert)->impl AsRef<[MinkowskiDirectedEdge]>{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir
let v0f=self.mesh0.vert_faces(v0);
let v1f=self.mesh1.vert_faces(v1);
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
let the_len=v0f.len()+v1f.len();
for &directed_edge_id in self.mesh0.vert_edges(v0).iter(){
let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
let the_len=v0f.as_ref().len()+v1f.as_ref().len();
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// TODO: there's gotta be a better way to do this
@ -948,30 +957,33 @@ impl MeshQuery for MinkowskiMesh<'_>{
face_normals.clone_from(&v0f_n);
for face_n in &v1f_n{
//add reflected mesh1 faces
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
}
}
for &directed_edge_id in self.mesh1.vert_edges(v1).iter(){
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
let mut face_normals=Vec::with_capacity(the_len);
face_normals.clone_from(&v1f_n);
for face_n in &v0f_n{
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().fix_3());
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
}
}
Cow::Owned(edges)
edges
},
}
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
unimplemented!()
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
unimplemented!();
vec![]
}
}
@ -1001,12 +1013,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
#[test]
fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec()));
assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_3()].to_vec()));
}
#[test]
fn build_me_a_cube(){
let mesh=PhysicsMesh::unit_cube();
//println!("mesh={:?}",mesh);
assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec()));
assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec()));
}

@ -1,5 +1,5 @@
use std::collections::{HashMap,HashSet};
use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
use strafesnet_common::bvh;
use strafesnet_common::map;
use strafesnet_common::run;
@ -14,45 +14,6 @@ use strafesnet_common::integer::{self,vec3,mat3,Planar64,Planar64Vec3,Planar64Ma
pub use strafesnet_common::physics::{Time,TimeInner};
use gameplay::ModeState;
// Physics bug fixes can easily desync all bots.
//
// When replaying a bot, use the exact physics version which it was recorded with.
//
// When validating a new bot, ignore the version and use the latest version,
// and overwrite the version in the file.
//
// Compatible physics versions should be determined
// empirically at development time via leaderboard resimulation.
//
// Compatible physics versions should result in an identical leaderboard state,
// or the only bots which fail are ones exploiting a surgically patched bug.
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
pub struct PhysicsVersion(u32);
pub const VERSION:PhysicsVersion=PhysicsVersion(1);
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
let compat=[0,1];
let mut input_version=0;
while input_version<compat.len(){
// compatible version must be greater that or equal to the input version
assert!(input_version as u32<=compat[input_version]);
// compatible version must be a version that exists
assert!(compat[input_version]<=VERSION.0);
input_version+=1;
}
compat
};
pub enum PhysicsVersionError{
UnknownPhysicsVersion,
}
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
}else{
Err(PhysicsVersionError::UnknownPhysicsVersion)
}
}
pub type Body=crate::body::Body<TimeInner>;
type MouseState=strafesnet_common::mouse::MouseState<TimeInner>;
@ -146,6 +107,7 @@ enum TransientAcceleration{
#[derive(Clone,Debug)]
struct ContactMoveState{
jump_direction:JumpDirection,
// TODO: this is bad data normalization, remove this
contact:ContactCollision,
target:TransientAcceleration,
}
@ -156,8 +118,8 @@ impl TransientAcceleration{
}else{
//normal friction acceleration is clippedAcceleration.dot(normal)*friction
TransientAcceleration::Reachable{
acceleration:target_diff.with_length(accel).divide().fix_1(),
time:time+Time::from((target_diff.length()/accel).divide().fix_1())
acceleration:target_diff.with_length(accel).divide().wrap_1(),
time:time+Time::from((target_diff.length()/accel).divide().clamp_1())
}
}
}
@ -268,12 +230,6 @@ impl PhysicsModels{
.map(|model|&model.transform),
}
}
fn contact_model(&self,model_id:ContactModelId)->&ContactModel{
&self.contact_models[&model_id]
}
fn intersect_model(&self,model_id:IntersectModelId)->&IntersectModel{
&self.intersect_models[&model_id]
}
fn contact_attr(&self,model_id:ContactModelId)->&gameplay_attributes::ContactAttributes{
&self.contact_attributes[&self.contact_models[&model_id].attr_id]
}
@ -308,7 +264,7 @@ impl PhysicsCamera{
);
self.clamped_mouse_pos=unclamped_mouse_pos;
}
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2 {
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2{
let a=-self.sensitivity.mul_int((self.clamped_mouse_pos+mouse_delta).as_i64vec2());
let ax=Angle32::wrap_from_i64(a.x);
let ay=Angle32::clamp_from_i64(a.y)
@ -452,7 +408,7 @@ impl HitboxMesh{
let transform=PhysicsMeshTransform::new(transform);
let transformed_mesh=TransformedMesh::new(mesh.complete_mesh_view(),&transform);
for vert in transformed_mesh.verts(){
aabb.grow(vert.fix_1());
aabb.grow(vert.narrow_1().unwrap());
}
Self{
halfsize:aabb.size()>>1,
@ -505,12 +461,12 @@ impl StyleHelper for StyleModifiers{
}
fn get_y_control_dir(&self,camera:&PhysicsCamera,controls:Controls)->Planar64Vec3{
(camera.rotation_y()*self.get_control_dir(controls)).fix_1()
(camera.rotation_y()*self.get_control_dir(controls)).wrap_1()
}
fn get_propulsion_control_dir(&self,camera:&PhysicsCamera,controls:Controls)->Planar64Vec3{
//don't interpolate this! discrete mouse movement, constant acceleration
(camera.rotation()*self.get_control_dir(controls)).fix_1()
(camera.rotation()*self.get_control_dir(controls)).wrap_1()
}
fn calculate_mesh(&self)->HitboxMesh{
let mesh=match self.hitbox.mesh{
@ -601,7 +557,7 @@ impl MoveState{
=>None,
}
}
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
//check if you have a valid walk state and create an instruction
match self{
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
@ -646,12 +602,17 @@ impl MoveState{
fn cull_velocity(&mut self,velocity:Planar64Vec3,body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState){
//TODO: be more precise about contacts
if set_velocity_cull(body,touching,models,hitbox_mesh,velocity){
//TODO do better
// TODO do better
// TODO: unduplicate this code
match self.get_walk_state(){
//did you stop touching the thing you were walking on?
// did you stop touching the thing you were walking on?
Some(walk_state)=>if !touching.contacts.contains(&walk_state.contact){
self.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
}else{
// stopped touching something else while walking
self.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
},
// not walking, but stopped touching something
None=>self.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
}
}
@ -829,14 +790,14 @@ impl TouchingState{
}).collect();
crate::push_solve::push_solve(&contacts,acceleration)
}
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
// let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
// let relative_body=body.relative_to(&Body::ZERO);
let relative_body=body;
for contact in &self.contacts{
//detect face slide off
let model_mesh=models.contact_mesh(contact);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time,collector.time(),contact.face_id).map(|(_face,time)|{
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{
TimedInstruction{
time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd(
@ -850,7 +811,7 @@ impl TouchingState{
//detect model collision in reverse
let model_mesh=models.intersect_mesh(intersect);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_out(&relative_body,start_time,collector.time()).map(|(_face,time)|{
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
TimedInstruction{
time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd(
@ -923,7 +884,7 @@ impl PhysicsState{
fn reset_to_default(&mut self){
*self=Self::default();
}
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,Time>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time)
}
fn cull_velocity(&mut self,data:&PhysicsData,velocity:Planar64Vec3){
@ -980,7 +941,7 @@ pub struct PhysicsData{
impl Default for PhysicsData{
fn default()->Self{
Self{
bvh:bvh::BvhNode::default(),
bvh:bvh::BvhNode::empty(),
models:Default::default(),
modes:Default::default(),
hitbox_mesh:StyleModifiers::default().calculate_mesh(),
@ -995,21 +956,21 @@ pub struct PhysicsContext<'a>{
// the physics consumes both Instruction and PhysicsInternalInstruction,
// but can only emit PhysicsInternalInstruction
impl InstructionConsumer<InternalInstruction> for PhysicsContext<'_>{
type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,TimeInner>){
type Time=Time;
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,Time>){
atomic_internal_instruction(&mut self.state,&self.data,ins)
}
}
impl InstructionConsumer<Instruction> for PhysicsContext<'_>{
type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,TimeInner>){
type Time=Time;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Time>){
atomic_input_instruction(&mut self.state,&self.data,ins)
}
}
impl InstructionEmitter<InternalInstruction> for PhysicsContext<'_>{
type TimeInner=TimeInner;
type Time=Time;
//this little next instruction function could cache its return value and invalidate the cached value by watching the State.
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
next_instruction_internal(&self.state,&self.data,time_limit)
}
}
@ -1017,7 +978,7 @@ impl PhysicsContext<'_>{
pub fn run_input_instruction(
state:&mut PhysicsState,
data:&PhysicsData,
instruction:TimedInstruction<Instruction,TimeInner>
instruction:TimedInstruction<Instruction,Time>
){
let mut context=PhysicsContext{state,data};
context.process_exhaustive(instruction.time);
@ -1027,10 +988,7 @@ impl PhysicsContext<'_>{
impl PhysicsData{
/// use with caution, this is the only non-instruction way to mess with physics
pub fn generate_models(&mut self,map:&map::CompleteMap){
let mut modes=map.modes.clone();
for mode in &mut modes.modes{
mode.denormalize_data();
}
let modes=map.modes.clone().denormalize();
let mut used_contact_attributes=Vec::new();
let mut used_intersect_attributes=Vec::new();
@ -1048,31 +1006,30 @@ impl PhysicsData{
let mut used_meshes=Vec::new();
let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new();
for (model_id,model) in map.models.iter().enumerate(){
//TODO: use .entry().or_insert_with(||{
let attr_id=if let Some(&attr_id)=physics_attr_id_from_model_attr_id.get(&model.attributes){
attr_id
}else{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
physics_attr_id_from_model_attr_id.insert(model.attributes,attr_id);
Some(attr_id)
})
}){
Some(attr_id)=>attr_id,
None=>continue,
let attr_id=match physics_attr_id_from_model_attr_id.entry(model.attributes){
std::collections::hash_map::Entry::Occupied(entry)=>*entry.get(),
std::collections::hash_map::Entry::Vacant(entry)=>{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
Some(*entry.insert(attr_id))
})
}){
Some(attr_id)=>attr_id,
None=>continue,
}
}
};
let mesh_id=if let Some(&mesh_id)=physics_mesh_id_from_model_mesh_id.get(&model.mesh){
@ -1132,7 +1089,7 @@ impl PhysicsData{
let mut aabb=aabb::Aabb::default();
let transformed_mesh=TransformedMesh::new(view,transform);
for v in transformed_mesh.verts(){
aabb.grow(v.fix_1());
aabb.grow(v.narrow_1().unwrap());
}
(ConvexMeshId{
model_id,
@ -1166,7 +1123,7 @@ impl PhysicsData{
}
//this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
//JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit);
@ -1179,15 +1136,15 @@ impl PhysicsData{
state.body.grow_aabb(&mut aabb,state.time,collector.time());
aabb.inflate(data.hitbox_mesh.halfsize);
//relative to moving platforms
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
//let relative_body=state.body.relative_to(&Body::ZERO);
let relative_body=&state.body;
data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits.
let model_mesh=data.models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_in(relative_body,state.time,collector.time())
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
//temp (?) code to avoid collision loops
.map_or(None,|(face,dt)|{
.and_then(|(face,dt)|{
// this must be rounded to avoid the infinite loop when hitting the start zone
let time=relative_body.time+dt.into();
(state.time<time).then_some((time,face,dt))
@ -1210,7 +1167,8 @@ fn contact_normal(models:&PhysicsModels,hitbox_mesh:&HitboxMesh,contact:&Contact
let model_mesh=models.contact_mesh(contact);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
// TODO: normalize to i64::MAX>>1
minkowski.face_nd(contact.face_id).0.fix_1()
// wrap for speed
minkowski.face_nd(contact.face_id).0.wrap_1()
}
fn recalculate_touching(
@ -1242,8 +1200,8 @@ fn recalculate_touching(
aabb.grow(body.position);
aabb.inflate(hitbox_mesh.halfsize);
//relative to moving platforms
//let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
//let relative_body=state.body.relative_to(&Body::ZERO);
bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits.
let model_mesh=models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
@ -1294,7 +1252,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
let mut culled=false;
touching.contacts.retain(|contact|{
let n=contact_normal(models,hitbox_mesh,contact);
let r=(n.dot(v)>>52).is_positive();
let r=n.dot(v).is_positive();
if r{
culled=true;
}
@ -1304,7 +1262,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
culled
}
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);;
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);
}
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{
//This is not correct but is better than what I have
@ -1369,7 +1327,7 @@ fn teleport_to_spawn(
const EPSILON:Planar64=Planar64::raw((1<<32)/16);
let transform=models.get_model_transform(spawn_model_id).ok_or(TeleportToSpawnError::NoModel)?;
//TODO: transform.vertex.matrix3.col(1)+transform.vertex.translation
let point=transform.vertex.transform_point3(vec3::Y).fix_1()+Planar64Vec3::new([Planar64::ZERO,style.hitbox.halfsize.y+EPSILON,Planar64::ZERO]);
let point=transform.vertex.transform_point3(vec3::Y).clamp_1()+Planar64Vec3::new([Planar64::ZERO,style.hitbox.halfsize.y+EPSILON,Planar64::ZERO]);
teleport(point,move_state,body,touching,run,mode_state,Some(mode),models,hitbox_mesh,bvh,style,camera,input_state,time);
Ok(())
}
@ -1533,7 +1491,7 @@ fn collision_start_contact(
Some(gameplay_attributes::ContactingBehaviour::Surf)=>(),
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1();
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).wrap_1();
set_velocity(body,touching,models,hitbox_mesh,reflected_velocity);
},
Some(gameplay_attributes::ContactingBehaviour::Ladder(contacting_ladder))=>
@ -1667,10 +1625,14 @@ fn collision_end_contact(
//TODO do better
//this is inner code from move_state.cull_velocity
match move_state.get_walk_state(){
//did you stop touching the thing you were walking on?
// did you stop touching the thing you were walking on?
Some(walk_state)=>if walk_state.contact==contact{
move_state.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
}else{
// stopped touching something else while walking
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
},
// not walking, but stopped touching something
None=>move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
}
}
@ -1696,7 +1658,7 @@ fn collision_end_intersect(
}
}
}
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,TimeInner>){
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,Time>){
state.time=ins.time;
let (should_advance_body,goober_time)=match ins.instruction{
InternalInstruction::CollisionStart(_,dt)
@ -1756,7 +1718,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
let control_dir=state.style.get_control_dir(masked_controls);
if control_dir!=vec3::ZERO{
let camera_mat=state.camera.simulate_move_rotation_y(state.input_state.lerp_delta(state.time).x);
if let Some(ticked_velocity)=strafe_settings.tick_velocity(state.body.velocity,(camera_mat*control_dir).with_length(Planar64::ONE).divide().fix_1()){
if let Some(ticked_velocity)=strafe_settings.tick_velocity(state.body.velocity,(camera_mat*control_dir).with_length(Planar64::ONE).divide().wrap_1()){
//this is wrong but will work ig
//need to note which push planes activate in push solve and keep those
state.cull_velocity(data,ticked_velocity);
@ -1792,7 +1754,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
}
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,TimeInner>){
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,Time>){
state.time=ins.time;
let should_advance_body=match ins.instruction{
//the body may as well be a quantum wave function
@ -1859,14 +1821,18 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
},
Instruction::Mode(ModeInstruction::Restart(mode_id))=>{
//teleport to mode start zone
let mut spawn_point=vec3::ZERO;
let mode=data.modes.get_mode(mode_id);
let spawn_point=mode.and_then(|mode|
if let Some(mode)=mode{
// set style
state.style=mode.get_style().clone();
//TODO: spawn at the bottom of the start zone plus the hitbox size
//TODO: set camera andles to face the same way as the start zone
data.models.get_model_transform(mode.get_start().into()).map(|transform|
transform.vertex.translation
)
).unwrap_or(vec3::ZERO);
//TODO: set camera angles to face the same way as the start zone
if let Some(transform)=data.models.get_model_transform(mode.get_start()){
// NOTE: this value may be 0,0,0 on source maps
spawn_point=transform.vertex.translation;
}
}
set_position(spawn_point,&mut state.move_state,&mut state.body,&mut state.touching,&mut state.run,&mut state.mode_state,mode,&data.models,&data.hitbox_mesh,&data.bvh,&state.style,&state.camera,&state.input_state,state.time);
set_velocity(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
state.set_move_state(data,MoveState::Air);
@ -1876,6 +1842,9 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
Instruction::Mode(ModeInstruction::Spawn(mode_id,stage_id))=>{
//spawn at a particular stage
if let Some(mode)=data.modes.get_mode(mode_id){
// set style
state.style=mode.get_style().clone();
// teleport to stage in mode
if let Some(stage)=mode.get_stage(stage_id){
let _=teleport_to_spawn(
stage.spawn(),
@ -1912,8 +1881,6 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
#[cfg(test)]
mod test{
use crate::file;
use crate::body::VirtualBody;
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
use super::*;
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
@ -1922,7 +1889,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
@ -1940,7 +1907,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO,Time::from_secs(10));
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
@ -1994,111 +1961,111 @@ mod test{
}
#[test]
fn test_collision_parabola_edge_east_from_west(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(3,3,0),
int3(100,-1,0),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_south_from_north(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,3,3),
int3(0,-1,100),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_west_from_east(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(-3,3,0),
int3(-100,-1,0),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_north_from_south(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,3,-3),
int3(0,-1,-100),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_north_from_ne(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,6,-7)>>1,
int3(-10,-1,1),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_north_from_nw(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,6,-7)>>1,
int3(10,-1,1),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_east_from_se(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(7,6,0)>>1,
int3(-1,-1,-10),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_east_from_ne(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(7,6,0)>>1,
int3(-1,-1,10),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_south_from_se(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,6,7)>>1,
int3(-10,-1,-1),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_south_from_sw(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(0,6,7)>>1,
int3(10,-1,-1),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_west_from_se(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(-7,6,0)>>1,
int3(1,-1,-10),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_parabola_edge_west_from_ne(){
test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
test_collision(Body::new(
int3(-7,6,0)>>1,
int3(1,-1,10),
int3(0,-1,0),
Time::ZERO
)).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
}
#[test]
fn test_collision_oblique(){
@ -2127,202 +2094,4 @@ mod test{
Time::ZERO
),None);
}
#[test]
fn run_replay(){
println!("loading map file..");
let map=file::load("../tools/bhop_maps/5692113331.snfm");
println!("loading bot file..");
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
}
match physics.get_finish_time(){
Some(time)=>println!("finish time:{}",time),
None=>println!("simulation did not end in finished state"),
}
}else{
panic!("missing files");
}
}
enum DeterminismResult{
Deterministic,
NonDeterministic,
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{
Load(file::LoadError),
IO(std::io::Error),
}
impl From<file::LoadError> for ReplayError{
fn from(value:file::LoadError)->Self{
Self::Load(value)
}
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
// create default physics state
let mut physics_deterministic=PhysicsState::default();
// create a second physics state
let mut physics_filtered=PhysicsState::default();
// invent a new bot id and insert the replay
println!("simulating...");
let mut non_idle_count=0;
for (i,ins) in bot.instructions.into_iter().enumerate(){
let state_deterministic=physics_deterministic.clone();
let state_filtered=physics_filtered.clone();
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
match ins{
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
other=>{
non_idle_count+=1;
// run
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
// check if position matches
let b0=physics_deterministic.camera_body();
let b1=physics_filtered.camera_body();
if b0.position!=b1.position{
println!("desync at instruction #{}",i);
println!("non idle instructions completed={non_idle_count}");
println!("instruction #{i}={:?}",other);
println!("deterministic state0:\n{state_deterministic:?}");
println!("filtered state0:\n{state_filtered:?}");
println!("deterministic state1:\n{:?}",physics_deterministic);
println!("filtered state1:\n{:?}",physics_filtered);
return DeterminismResult::NonDeterministic;
}
},
}
}
match physics_deterministic.get_finish_time(){
Some(time)=>println!("[with idle] finish time:{}",time),
None=>println!("[with idle] simulation did not end in finished state"),
}
match physics_filtered.get_finish_time(){
Some(time)=>println!("[filtered] finish time:{}",time),
None=>println!("[filtered] simulation did not end in finished state"),
}
DeterminismResult::Deterministic
}
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
s.spawn(move ||{
let result=match file::load(file_path.as_path()){
Ok(file::LoadFormat::Bot(bot))=>{
println!("Running {:?}",file_path.file_stem());
Ok(Some(segment_determinism(bot,physics_data)))
},
Ok(_)=>{
println!("Provided bot file is not a bot file!");
Ok(None)
}
Err(e)=>{
println!("Load error");
Err(e)
},
};
// send when thread is complete
send.send(result).unwrap();
});
}
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
Ok(dir_entry.file_type()?.is_file().then_some(
dir_entry.path()
))
}
#[test]
fn test_determinism()->Result<(),ReplayError>{
let thread_limit=std::thread::available_parallelism()?.get();
println!("loading map file..");
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
panic!("Provided map file is not a map file!");
};
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let (send,recv)=std::sync::mpsc::channel();
let mut read_dir=std::fs::read_dir("../tools/replays")?;
// promise that &physics_data will outlive the spawned threads
let thread_results=std::thread::scope(|s|{
let mut thread_results=Vec::new();
// spawn threads
println!("spawning up to {thread_limit} threads...");
let mut active_thread_count=0;
while active_thread_count<thread_limit{
if let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
active_thread_count+=1;
do_thread(s,file_path,send.clone(),&physics_data);
}
}else{
break;
}
}
// spawn another thread every time a message is received from the channel
println!("riding parallelism wave...");
while let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
// wait for a thread to complete
thread_results.push(recv.recv().unwrap());
do_thread(s,file_path,send.clone(),&physics_data);
}
}
// wait for remaining threads to complete
println!("waiting for all threads to complete...");
for _ in 0..active_thread_count{
thread_results.push(recv.recv().unwrap());
}
println!("done.");
Ok::<_,ReplayError>(thread_results)
})?;
// tally results
#[derive(Default)]
struct Totals{
deterministic:u32,
nondeterministic:u32,
invalid:u32,
error:u32,
}
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
match result{
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
Ok(None)=>totals.invalid+=1,
Err(_)=>totals.error+=1,
}
totals
});
println!("deterministic={deterministic}");
println!("nondeterministic={nondeterministic}");
println!("invalid={invalid}");
println!("error={error}");
assert!(nondeterministic==0);
assert!(invalid==0);
assert!(error==0);
Ok(())
}
}

@ -1,4 +1,6 @@
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
use strafesnet_common::integer::vec3::{self,Vector3};
use strafesnet_common::integer::{Fixed,Planar64Vec3,Ratio};
use strafesnet_common::ray::Ray;
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
@ -12,24 +14,6 @@ type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
// hack to allow comparing ratios to zero
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
struct Ray{
origin:Planar64Vec3,
direction:Planar64Vec3,
}
impl Ray{
fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
}
}
/// Information about a contact restriction
pub struct Contact{
pub position:Planar64Vec3,
@ -168,18 +152,19 @@ const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::ZERO}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
let direction=solve1(c0)?.divide().fix_1();
//wrap for speed
let direction=solve1(c0)?.divide().wrap_1();
let [s0]=decompose1(direction,c0.velocity)?;
if s0.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve1(
&c0.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let direction=solve2(c0,c1)?.divide().fix_1();
let direction=solve2(c0,c1)?.divide().wrap_1();
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
return None;
@ -187,11 +172,11 @@ fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let origin=point+solve2(
&c0.relative_to(point),
&c1.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
let direction=solve3(c0,c1,c2)?.divide().fix_1();
let direction=solve3(c0,c1,c2)?.divide().wrap_1();
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
return None;
@ -200,7 +185,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
&c0.relative_to(point),
&c1.relative_to(point),
&c2.relative_to(point),
)?.divide().fix_1();
)?.divide().wrap_1();
Some(Ray{origin,direction})
}

12
engine/session/Cargo.toml Normal file

@ -0,0 +1,12 @@
[package]
name = "strafesnet_session"
version = "0.1.0"
edition = "2024"
[dependencies]
glam = "0.30.0"
replace_with = "0.1.7"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }

8
engine/session/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -0,0 +1,2 @@
mod mouse_interpolator;
pub mod session;

@ -5,13 +5,13 @@ use strafesnet_common::physics::{
TimeInner as PhysicsTimeInner,
Time as PhysicsTime,
};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::session::Time as SessionTime;
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTime>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTime>;
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTime>;
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
@ -89,14 +89,14 @@ pub struct MouseInterpolator{
// Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
type Time=SessionTime;
fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
}
}
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
type Time=SessionTime;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
self.buffered_instruction_with_timeout(time_limit)
}
}
@ -108,7 +108,7 @@ impl MouseInterpolator{
output:std::collections::VecDeque::new(),
}
}
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTime>){
self.buffer.push_front(TimedInstruction{
time:ins.time,
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
@ -219,7 +219,7 @@ impl MouseInterpolator{
}
}
}
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTime>>{
match self.get_mouse_timedout_at(time_limit){
Some(timeout)=>Some(TimedInstruction{
time:timeout,
@ -232,7 +232,7 @@ impl MouseInterpolator{
}),
}
}
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTime>)->Option<TimedPhysicsInstruction>{
match ins.instruction{
StepInstruction::Pop=>(),
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
@ -244,6 +244,7 @@ impl MouseInterpolator{
#[cfg(test)]
mod test{
use super::*;
use strafesnet_common::session::TimeInner as SessionTimeInner;
#[test]
fn test(){
let mut interpolator=MouseInterpolator::new();

@ -12,10 +12,11 @@ use strafesnet_common::physics::{
};
use strafesnet_common::timer::{Scaled,Timer};
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
use strafesnet_settings::directories::Directories;
use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
use crate::physics::{PhysicsContext,PhysicsData};
use crate::settings::UserSettings;
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
use strafesnet_settings::settings::UserSettings;
pub enum Instruction<'a>{
Input(SessionInputInstruction),
@ -57,19 +58,19 @@ pub enum SessionPlaybackInstruction{
}
pub struct FrameState{
pub body:crate::physics::Body,
pub camera:crate::physics::PhysicsCamera,
pub body:physics::Body,
pub camera:physics::PhysicsCamera,
pub time:PhysicsTime,
}
pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsState,
physics:physics::PhysicsState,
}
impl Simulation{
pub const fn new(
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:crate::physics::PhysicsState,
physics:physics::PhysicsState,
)->Self{
Self{
timer,
@ -87,11 +88,11 @@ impl Simulation{
#[derive(Default)]
pub struct Recording{
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
}
impl Recording{
pub fn new(
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
)->Self{
Self{instructions}
}
@ -149,11 +150,12 @@ enum ViewState{
}
pub struct Session{
directories:Directories,
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
view_state:ViewState,
//gui:GuiState
geometry_shared:crate::physics::PhysicsData,
geometry_shared:physics::PhysicsData,
simulation:Simulation,
// below fields not included in lite session
recording:Recording,
@ -163,10 +165,12 @@ pub struct Session{
impl Session{
pub fn new(
user_settings:UserSettings,
directories:Directories,
simulation:Simulation,
)->Self{
Self{
user_settings,
directories,
mouse_interpolator:MouseInterpolator::new(),
geometry_shared:Default::default(),
simulation,
@ -203,8 +207,8 @@ impl Session{
// Session emits DoStep
impl InstructionConsumer<Instruction<'_>> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
type Time=SessionTime;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::Time>){
// repetitive procedure macro
macro_rules! run_mouse_interpolator_instruction{
($instruction:expr)=>{
@ -295,11 +299,17 @@ impl InstructionConsumer<Instruction<'_>> for Session{
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
let file_name=format!("replays/{}.snfb",ins.time);
let mut replays_path=self.directories.replays.clone();
let file_name=format!("{}.snfb",ins.time);
std::thread::spawn(move ||{
std::fs::create_dir_all("replays").unwrap();
let file=std::fs::File::create(file_name).unwrap();
strafesnet_snf::bot::write_bot(std::io::BufWriter::new(file),crate::physics::VERSION.get(),replay.recording.instructions).unwrap();
std::fs::create_dir_all(replays_path.as_path()).unwrap();
replays_path.push(file_name);
let file=std::fs::File::create(replays_path).unwrap();
strafesnet_snf::bot::write_bot(
std::io::BufWriter::new(file),
strafesnet_physics::VERSION.get(),
replay.recording.instructions
).unwrap();
println!("Finished writing bot file!");
});
},
@ -415,8 +425,8 @@ impl InstructionConsumer<Instruction<'_>> for Session{
}
}
impl InstructionConsumer<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
type Time=SessionTime;
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::Time>){
let time=self.simulation.timer.time(ins.time);
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
//record
@ -426,8 +436,8 @@ impl InstructionConsumer<StepInstruction> for Session{
}
}
impl InstructionEmitter<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
type Time=SessionTime;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
self.mouse_interpolator.next_instruction(time_limit)
}
}

@ -0,0 +1,10 @@
[package]
name = "strafesnet_settings"
version = "0.1.0"
edition = "2024"
[dependencies]
configparser = "3.0.2"
directories = "6.0.0"
glam = "0.30.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

8
engine/settings/LICENSE Normal file

@ -0,0 +1,8 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -0,0 +1,32 @@
use std::path::PathBuf;
use crate::settings::{UserSettings,load_user_settings};
pub struct Directories{
pub settings:PathBuf,
pub maps:PathBuf,
pub replays:PathBuf,
}
impl Directories{
pub fn settings(&self)->UserSettings{
load_user_settings(&self.settings)
}
pub fn user()->Option<Self>{
let dirs=directories::ProjectDirs::from("net.strafes","StrafesNET","Strafe Client")?;
Some(Self{
settings:dirs.config_dir().join("settings.conf"),
maps:dirs.cache_dir().join("maps"),
// separate directory for remote downloaded replays (cache)
// bots:dirs.cache_dir().join("bots"),
replays:dirs.data_local_dir().join("replays"),
})
}
pub fn portable()->Result<Self,std::io::Error>{
let current_dir=std::env::current_dir()?;
Ok(Self{
settings:current_dir.join("settings.conf"),
maps:current_dir.join("maps"),
replays:current_dir.join("replays"),
})
}
}

@ -0,0 +1,2 @@
pub mod settings;
pub mod directories;

@ -74,9 +74,9 @@ sensitivity_y_from_x_ratio=1
Sensitivity::DeriveY{x:0.0.001,y:DerivedSensitivity{ratio:1.0}}
*/
pub fn read_user_settings()->UserSettings{
pub fn load_user_settings(path:&std::path::Path)->UserSettings{
let mut cfg=configparser::ini::Ini::new();
if let Ok(_)=cfg.load("settings.conf"){
if let Ok(_)=cfg.load(path){
let (cfg_fov_x,cfg_fov_y)=(cfg.getfloat("camera","fov_x"),cfg.getfloat("camera","fov_y"));
let fov=match(cfg_fov_x,cfg_fov_y){
(Ok(Some(fov_x)),Ok(Some(fov_y)))=>Fov::Exactly {
@ -136,4 +136,4 @@ pub fn read_user_settings()->UserSettings{
}else{
UserSettings::default()
}
}
}

@ -0,0 +1,9 @@
[package]
name = "integration-testing"
version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }

@ -0,0 +1,253 @@
use std::io::Cursor;
use std::path::Path;
use std::time::Instant;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
let arg=std::env::args().skip(1).next();
match arg.as_deref(){
Some("determinism")|None=>test_determinism().unwrap(),
Some("replay")=>run_replay().unwrap(),
_=>println!("invalid argument"),
}
}
#[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>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
println!("loading bot file..");
let data=read_entire_file("../tools/replays/535s+159764769ns.snfb")?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
}
match physics.get_finish_time(){
Some(time)=>println!("finish time:{}",time),
None=>println!("simulation did not end in finished state"),
}
Ok(())
}
enum DeterminismResult{
Deterministic,
NonDeterministic,
}
struct SegmentResult{
determinism:DeterminismResult,
ticks:u64,
nanoseconds:u64,
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->SegmentResult{
// create default physics state
let mut physics_deterministic=PhysicsState::default();
// create a second physics state
let mut physics_filtered=PhysicsState::default();
// invent a new bot id and insert the replay
println!("simulating...");
let mut non_idle_count=0;
let instruction_count=bot.instructions.len();
let start=Instant::now();
for (i,ins) in bot.instructions.into_iter().enumerate(){
let state_deterministic=physics_deterministic.clone();
let state_filtered=physics_filtered.clone();
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
match ins{
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
other=>{
non_idle_count+=1;
// run
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
// check if position matches
let b0=physics_deterministic.camera_body();
let b1=physics_filtered.camera_body();
if b0.position!=b1.position{
let nanoseconds=start.elapsed().as_nanos() as u64;
println!("desync at instruction #{}",i);
println!("non idle instructions completed={non_idle_count}");
println!("instruction #{i}={:?}",other);
println!("deterministic state0:\n{state_deterministic:?}");
println!("filtered state0:\n{state_filtered:?}");
println!("deterministic state1:\n{:?}",physics_deterministic);
println!("filtered state1:\n{:?}",physics_filtered);
return SegmentResult{
determinism:DeterminismResult::NonDeterministic,
ticks:1+i as u64+non_idle_count,
nanoseconds,
};
}
},
}
}
let nanoseconds=start.elapsed().as_nanos() as u64;
match physics_deterministic.get_finish_time(){
Some(time)=>println!("[with idle] finish time:{}",time),
None=>println!("[with idle] simulation did not end in finished state"),
}
match physics_filtered.get_finish_time(){
Some(time)=>println!("[filtered] finish time:{}",time),
None=>println!("[filtered] simulation did not end in finished state"),
}
SegmentResult{
determinism:DeterminismResult::Deterministic,
ticks:instruction_count as u64+non_idle_count,
nanoseconds,
}
}
type ThreadResult=Result<Option<SegmentResult>,ReplayError>;
fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{
let data=read_entire_file(file_path.as_path())?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
println!("Running {:?}",file_path.file_stem());
Ok(Some(segment_determinism(bot,physics_data)))
}
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
s.spawn(move ||{
let result=read_and_run(file_path,physics_data);
// send when thread is complete
send.send(result).unwrap();
});
}
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
Ok(dir_entry.file_type()?.is_file().then_some(
dir_entry.path()
))
}
fn test_determinism()->Result<(),ReplayError>{
let thread_limit=std::thread::available_parallelism()?.get();
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let (send,recv)=std::sync::mpsc::channel();
let mut read_dir=std::fs::read_dir("../tools/replays")?;
// promise that &physics_data will outlive the spawned threads
let thread_results=std::thread::scope(|s|{
let mut thread_results=Vec::new();
// spawn threads
println!("spawning up to {thread_limit} threads...");
let mut active_thread_count=0;
while active_thread_count<thread_limit{
if let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
active_thread_count+=1;
do_thread(s,file_path,send.clone(),&physics_data);
}
}else{
break;
}
}
// spawn another thread every time a message is received from the channel
println!("riding parallelism wave...");
while let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
// wait for a thread to complete
thread_results.push(recv.recv().unwrap());
do_thread(s,file_path,send.clone(),&physics_data);
}
}
// wait for remaining threads to complete
println!("waiting for all threads to complete...");
for _ in 0..active_thread_count{
thread_results.push(recv.recv().unwrap());
}
println!("done.");
Ok::<_,ReplayError>(thread_results)
})?;
// tally results
#[derive(Default)]
struct Totals{
deterministic:u32,
nondeterministic:u32,
invalid:u32,
error:u32,
}
let mut nanoseconds=0;
let mut ticks=0;
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
match result{
Ok(Some(segment_result))=>{
ticks+=segment_result.ticks;
nanoseconds+=segment_result.nanoseconds;
match segment_result.determinism{
DeterminismResult::Deterministic=>totals.deterministic+=1,
DeterminismResult::NonDeterministic=>totals.nondeterministic+=1,
}
},
Ok(None)=>totals.invalid+=1,
Err(_)=>totals.error+=1,
}
totals
});
println!("deterministic={deterministic}");
println!("nondeterministic={nondeterministic}");
println!("invalid={invalid}");
println!("error={error}");
println!("average ticks/s per core: {}",ticks*1_000_000_000/nanoseconds);
assert!(nondeterministic==0);
assert!(invalid==0);
assert!(error==0);
Ok(())
}

@ -1,7 +1,7 @@
[package]
name = "strafesnet_bsp_loader"
version = "0.2.2"
edition = "2021"
version = "0.3.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Convert Valve BSP files to StrafesNET data structures."
@ -10,7 +10,10 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
glam = "0.29.0"
strafesnet_common = { path = "../common", registry = "strafesnet" }
vbsp = "0.6.0"
glam = "0.30.0"
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vpk = "0.3.0"

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

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

@ -1,43 +1,142 @@
use strafesnet_common::{map,model,integer,gameplay_attributes};
use std::borrow::Cow;
const VALVE_SCALE:f32=1.0/16.0;
fn valve_transform([x,y,z]:[f32;3])->integer::Planar64Vec3{
integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
use vbsp_entities_css::Entity;
use strafesnet_common::{map,model,integer,gameplay_attributes as attr};
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
use strafesnet_common::gameplay_modes::{NormalizedMode,NormalizedModes,Mode,Stage};
use crate::valve_transform;
fn ingest_vertex(
mb:&mut model::MeshBuilder,
world_position:vbsp::Vector,
texture_transform_u:glam::Vec4,
texture_transform_v:glam::Vec4,
normal:model::NormalId,
color:model::ColorId,
)->model::VertexId{
//world_model.origin seems to always be 0,0,0
let vertex_xyz=world_position.into();
let pos=mb.acquire_pos_id(valve_transform(vertex_xyz));
//calculate texture coordinates
let pos_4d=glam::Vec3::from_array(vertex_xyz).extend(1.0);
let tex=glam::vec2(texture_transform_u.dot(pos_4d),texture_transform_v.dot(pos_4d));
let tex=mb.acquire_tex_id(tex);
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}
pub fn convert_bsp<AcquireRenderConfigId,AcquireMeshId>(
bsp:&vbsp::Bsp,
mut acquire_render_config_id:AcquireRenderConfigId,
mut acquire_mesh_id:AcquireMeshId
)->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
//figure out real attributes later
let mut unique_attributes=Vec::new();
unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
const TEMP_TOUCH_ME_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
let mut prop_mesh_count=0;
fn add_brush<'a>(
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
world_models:&mut Vec<model::Model>,
prop_models:&mut Vec<model::Model>,
model:&'a str,
origin:vbsp::Vector,
rendercolor:vbsp::Color,
attributes:attr::CollisionAttributesId,
){
let transform=integer::Planar64Affine3::from_translation(
valve_transform(origin.into())
);
let color=(glam::Vec3::from_array([
rendercolor.r as f32,
rendercolor.g as f32,
rendercolor.b as f32
])/255.0).extend(1.0);
match model.chars().next(){
// The first character of brush.model is '*'
Some('*')=>match model[1..].parse(){
Ok(mesh_id)=>{
let mesh=model::MeshId::new(mesh_id);
world_models.push(
model::Model{mesh,attributes,transform,color}
);
},
Err(e)=>{
println!("Brush model int parse error: {e} model={model}");
return;
},
},
_=>{
let mesh=mesh_deferred_loader.acquire_mesh_id(model);
prop_models.push(
model::Model{mesh,attributes,transform,color}
);
}
}
}
pub fn convert<'a>(
bsp:&'a crate::Bsp,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>,
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
)->PartialMap1{
let bsp=bsp.as_ref();
//figure out real attributes later
let unique_attributes=vec![
attr::CollisionAttributes::Decoration,
attr::CollisionAttributes::contact_default(),
attr::CollisionAttributes::intersect_default(),
// ladder
attr::CollisionAttributes::Contact(
attr::ContactAttributes{
contacting:attr::ContactingAttributes{
contact_behaviour:Some(attr::ContactingBehaviour::Ladder(
attr::ContactingLadder{sticky:true}
))
},
general:attr::GeneralAttributes::default(),
}
),
// water
attr::CollisionAttributes::Intersect(
attr::IntersectAttributes{
intersecting:attr::IntersectingAttributes{
water:Some(attr::IntersectingWater{
viscosity:integer::Planar64::ONE,
density:integer::Planar64::ONE,
velocity:integer::vec3::ZERO,
}),
},
general:attr::GeneralAttributes::default(),
}
),
];
const ATTRIBUTE_DECORATION:attr::CollisionAttributesId=attr::CollisionAttributesId::new(0);
const ATTRIBUTE_CONTACT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(1);
const ATTRIBUTE_INTERSECT_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(2);
const ATTRIBUTE_LADDER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(3);
const ATTRIBUTE_WATER_DEFAULT:attr::CollisionAttributesId=attr::CollisionAttributesId::new(4);
//declare all prop models to Loader
let prop_models=bsp.static_props().map(|prop|{
let mut prop_models=bsp.static_props().map(|prop|{
const DEG_TO_RAD:f32=std::f32::consts::TAU/360.0;
//get or create mesh_id
let mesh_id=acquire_mesh_id(prop.model());
//not the most failsafe code but this is just for the map tool lmao
if prop_mesh_count==mesh_id.get(){
prop_mesh_count+=1;
};
let placement=prop.as_prop_placement();
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
model::Model{
mesh:mesh_id,
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
attributes:ATTRIBUTE_DECORATION,
transform:integer::Planar64Affine3::new(
integer::mat3::try_from_f32_array_2d((
glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale))
integer::mat3::try_from_f32_array_2d(
//TODO: figure this out
*glam::Mat3A::from_quat(glam::Quat::from_array(placement.rotation.into()))
).to_cols_array_2d()).unwrap(),
valve_transform(placement.origin.into()),
glam::Mat3A::from_euler(
glam::EulerRot::XYZ,
prop.angles.pitch*DEG_TO_RAD,
prop.angles.yaw*DEG_TO_RAD,
prop.angles.roll*DEG_TO_RAD
).to_cols_array_2d()
).unwrap(),
valve_transform(prop.origin.into()),
),
color:glam::Vec4::ONE,
}
@ -47,14 +146,12 @@ where
//the generated MeshIds in here will collide with the Loader Mesh Ids
//but I can't think of a good workaround other than just remapping one later.
let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
//non-deduplicated
let mut spam_pos=Vec::new();
let mut spam_tex=Vec::new();
let mut spam_normal=Vec::new();
let mut spam_vertices=Vec::new();
let mut world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let mut graphics_groups=Vec::new();
let mut physics_group=model::IndexedPhysicsGroup::default();
let mut render_id_to_graphics_group_id=std::collections::HashMap::new();
let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let face_texture=face.texture();
@ -63,139 +160,200 @@ where
let texture_transform_u=glam::Vec4::from_array(face_texture.texture_transforms_u)/(face_texture_data.width as f32);
let texture_transform_v=glam::Vec4::from_array(face_texture.texture_transforms_v)/(face_texture_data.height as f32);
//this automatically figures out what the texture is trying to do and creates
//a render config for it, and then returns the id to that render config
let render_id=acquire_render_config_id(Some(face_texture_data.name()));
//normal
let normal=face.normal();
let normal_idx=spam_normal.len() as u32;
spam_normal.push(valve_transform(normal.into()));
let mut polygon_iter=face.vertex_positions().map(|vertex_position|{
//world_model.origin seems to always be 0,0,0
let vertex_xyz=(world_model.origin+vertex_position).into();
let pos_idx=spam_pos.len();
spam_pos.push(valve_transform(vertex_xyz));
//calculate texture coordinates
let pos=glam::Vec3::from_array(vertex_xyz).extend(1.0);
let tex=glam::vec2(texture_transform_u.dot(pos),texture_transform_v.dot(pos));
let tex_idx=spam_tex.len() as u32;
spam_tex.push(tex);
let vertex_id=model::VertexId::new(spam_vertices.len() as u32);
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(pos_idx as u32),
tex:model::TextureCoordinateId::new(tex_idx as u32),
normal:model::NormalId::new(normal_idx),
color:model::ColorId::new(0),
});
vertex_id
});
let normal=mb.acquire_normal_id(valve_transform(face.normal().into()));
let mut polygon_iter=face.vertex_positions().map(|vertex_position|
world_model.origin+vertex_position
);
let polygon_list=std::iter::from_fn(move||{
match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){
(Some(v1),Some(v2),Some(v3))=>Some(vec![v1,v2,v3]),
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
}).map(|triplet|{
triplet.map(|world_position|
ingest_vertex(&mut mb,world_position,texture_transform_u,texture_transform_v,normal,color)
).to_vec()
}).collect();
if face.is_visible(){
//TODO: deduplicate graphics groups by render id
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
})
//this automatically figures out what the texture is trying to do and creates
//a render config for it, and then returns the id to that render config
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(face_texture_data.name())));
//deduplicate graphics groups by render id
let graphics_group_id=*render_id_to_graphics_group_id.entry(render_id).or_insert_with(||{
let graphics_group_id=graphics_groups.len();
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![],
});
graphics_group_id
});
graphics_groups[graphics_group_id].groups.push(polygon_group_id);
}
physics_group.groups.push(polygon_group_id);
model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))
}).collect();
model::Mesh{
unique_pos:spam_pos,
unique_tex:spam_tex,
unique_normal:spam_normal,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups:vec![physics_group],
}
mb.build(polygon_groups,graphics_groups,vec![])
}).collect();
let world_models:Vec<model::Model>=
//one instance of the main world mesh
std::iter::once((
//world_model
model::MeshId::new(0),
//model_origin
vbsp::Vector::from([0.0,0.0,0.0]),
//model_color
vbsp::Color{r:255,g:255,b:255},
)).chain(
//entities sprinkle instances of the other meshes around
bsp.entities.iter()
.flat_map(|ent|ent.parse())//ignore entity parsing errors
.filter_map(|ent|match ent{
vbsp::Entity::Brush(brush)=>Some(brush),
vbsp::Entity::BrushIllusionary(brush)=>Some(brush),
vbsp::Entity::BrushWall(brush)=>Some(brush),
vbsp::Entity::BrushWallToggle(brush)=>Some(brush),
_=>None,
}).flat_map(|brush|
//The first character of brush.model is '*'
brush.model[1..].parse().map(|mesh_id|//ignore parse int errors
(model::MeshId::new(mesh_id),brush.origin,brush.color)
)
)
).map(|(mesh_id,model_origin,vbsp::Color{r,g,b})|{
model::Model{
mesh:mesh_id,
attributes:TEMP_TOUCH_ME_ATTRIBUTE,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
valve_transform(model_origin.into())
),
color:(glam::Vec3::from_array([r as f32,g as f32,b as f32])/255.0).extend(1.0),
let mut found_spawn=None;
let mut world_models=Vec::new();
// the one and only world model 0
world_models.push(model::Model{
mesh:model::MeshId::new(0),
attributes:ATTRIBUTE_DECORATION,
transform:integer::Planar64Affine3::IDENTITY,
color:glam::Vec4::W,
});
const WHITE:vbsp::Color=vbsp::Color{r:255,g:255,b:255};
for raw_ent in &bsp.entities{
match raw_ent.parse(){
Ok(Entity::Cycler(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::EnvSprite(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncBreakable(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncBrush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncDoor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncIllusionary(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncMonitor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncMovelinear(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncPhysbox(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncPhysboxMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncRotButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::FuncRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTracktrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWall(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWallToggle(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWaterAnalog(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ATTRIBUTE_DECORATION),
Ok(Entity::PropDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamic(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamicOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysics(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropRagdoll(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerGravity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerHurt(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerLook(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerMultiple(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerOnce(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerProximity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerPush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerSoundscape(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerTeleport(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerVphysicsMotion(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerWind(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::InfoPlayerCounterterrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Ok(Entity::InfoPlayerTerrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Err(e)=>{
println!("Bsp Entity parse error: {e}");
},
_=>(),
}
}).collect();
}
// physics models
for brush in &bsp.brushes{
const RELEVANT:vbsp::BrushFlags=
vbsp::BrushFlags::SOLID
.union(vbsp::BrushFlags::PLAYERCLIP)
.union(vbsp::BrushFlags::WATER)
.union(vbsp::BrushFlags::MOVEABLE)
.union(vbsp::BrushFlags::LADDER);
if !brush.flags.intersects(RELEVANT){
continue;
}
let is_ladder=brush.flags.contains(vbsp::BrushFlags::LADDER);
let is_water=brush.flags.contains(vbsp::BrushFlags::WATER);
let attributes=match (is_ladder,is_water){
(true,false)=>ATTRIBUTE_LADDER_DEFAULT,
(false,true)=>ATTRIBUTE_WATER_DEFAULT,
(false,false)=>ATTRIBUTE_CONTACT_DEFAULT,
(true,true)=>{
// water ladder? wtf
println!("brush is a water ladder o_o defaulting to ladder");
ATTRIBUTE_LADDER_DEFAULT
}
};
let mesh_result=crate::brush::brush_to_mesh(bsp,brush);
match mesh_result{
Ok(mesh)=>{
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(mesh);
world_models.push(model::Model{
mesh:mesh_id,
attributes,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
integer::vec3::ZERO,
),
color:glam::Vec4::ONE,
});
},
Err(e)=>println!("Brush mesh error: {e}"),
}
}
let mut modes_list=Vec::new();
if let Some(spawn_point)=found_spawn{
// create a new mesh
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(crate::brush::unit_cube());
// create a new model
let model_id=model::ModelId::new(world_models.len() as u32);
world_models.push(model::Model{
mesh:mesh_id,
attributes:ATTRIBUTE_INTERSECT_DEFAULT,
transform:integer::Planar64Affine3::from_translation(spawn_point),
color:glam::Vec4::W,
});
let first_stage=Stage::empty(model_id);
let main_mode=Mode::new(
strafesnet_common::gameplay_style::StyleModifiers::source_bhop(),
model_id,
std::collections::HashMap::new(),
vec![first_stage],
std::collections::HashMap::new(),
);
modes_list.push(NormalizedMode::new(main_mode));
}
PartialMap1{
attributes:unique_attributes,
world_meshes,
prop_models,
world_models,
modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
modes:NormalizedModes::new(modes_list),
}
}
//partially constructed map types
pub struct PartialMap1{
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
attributes:Vec<attr::CollisionAttributes>,
prop_models:Vec<model::Model>,
world_meshes:Vec<model::Mesh>,
world_models:Vec<model::Model>,
modes:strafesnet_common::gameplay_modes::Modes,
modes:NormalizedModes,
}
impl PartialMap1{
pub fn add_prop_meshes<AcquireRenderConfigId>(
pub fn add_prop_meshes<'a>(
self,
prop_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::ModelData)>,
mut acquire_render_config_id:AcquireRenderConfigId,
)->PartialMap2
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
prop_meshes:Meshes,
)->PartialMap2{
PartialMap2{
attributes:self.attributes,
prop_meshes:prop_meshes.into_iter().filter_map(|(mesh_id,model_data)|
//this will generate new render ids and texture ids
match convert_mesh(model_data,&mut acquire_render_config_id){
Ok(mesh)=>Some((mesh_id,mesh)),
Err(e)=>{
println!("error converting mesh: {e}");
None
}
}
).collect(),
prop_meshes:prop_meshes.consume().collect(),
prop_models:self.prop_models,
world_meshes:self.world_meshes,
world_models:self.world_models,
@ -204,18 +362,17 @@ impl PartialMap1{
}
}
pub struct PartialMap2{
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
attributes:Vec<attr::CollisionAttributes>,
prop_meshes:Vec<(model::MeshId,model::Mesh)>,
prop_models:Vec<model::Model>,
world_meshes:Vec<model::Mesh>,
world_models:Vec<model::Model>,
modes:strafesnet_common::gameplay_modes::Modes,
modes:NormalizedModes,
}
impl PartialMap2{
pub fn add_render_configs_and_textures(
mut self,
render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
render_configs:RenderConfigs,
)->map::CompleteMap{
//merge mesh and model lists, flatten and remap all ids
let mesh_id_offset=self.world_meshes.len();
@ -234,13 +391,14 @@ impl PartialMap2{
})
));
//let mut models=Vec::new();
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>)
=textures.into_iter()
//.filter_map(f) cull unused textures
.enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
.enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
}).unzip();
let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{
//this may generate duplicate no-texture render configs but idc
render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied()
@ -257,77 +415,3 @@ impl PartialMap2{
}
}
}
fn convert_mesh<AcquireRenderConfigId>(
model_data:crate::data::ModelData,
acquire_render_config_id:&mut AcquireRenderConfigId,
)->Result<model::Mesh,vmdl::ModelError>
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
let model=model_data.read_model()?;
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut spam_pos=Vec::with_capacity(model.vertices().len());
let mut spam_normal=Vec::with_capacity(model.vertices().len());
let mut spam_tex=Vec::with_capacity(model.vertices().len());
let mut spam_vertices=Vec::with_capacity(model.vertices().len());
for (i,vertex) in model.vertices().iter().enumerate(){
spam_pos.push(valve_transform(vertex.position.into()));
spam_normal.push(valve_transform(vertex.normal.into()));
spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(i as u32),
tex:model::TextureCoordinateId::new(i as u32),
normal:model::NormalId::new(i as u32),
color:model::ColorId::new(0),
});
}
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
acquire_render_config_id(path.as_os_str().to_str())
}else{
acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|
std::iter::from_fn(move||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
).collect()
))
}).collect();
Ok(model::Mesh{
unique_pos:spam_pos,
unique_normal:spam_normal,
unique_tex:spam_tex,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups,
})
}

@ -1,60 +0,0 @@
pub struct Bsp(vbsp::Bsp);
impl Bsp{
pub const fn new(value:vbsp::Bsp)->Self{
Self(value)
}
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.0
}
}
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for MdlData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VtxData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VvdData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
impl ModelData{
pub fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
))
}
}

@ -1,7 +1,20 @@
mod bsp;
pub mod data;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use data::Bsp;
mod bsp;
mod mesh;
mod brush;
pub mod loader;
const VALVE_SCALE:f32=1.0/16.0;
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
(d*VALVE_SCALE).try_into().unwrap()
}
pub(crate) fn valve_transform_normal([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x,z,-y]).unwrap()
}
pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
}
#[derive(Debug)]
pub enum ReadError{
@ -15,6 +28,38 @@ impl std::fmt::Display for ReadError{
}
impl std::error::Error for ReadError{}
#[derive(Debug)]
pub enum LoadError{
Texture(loader::TextureError),
Mesh(loader::MeshError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadError{}
impl From<loader::TextureError> for LoadError{
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
pub struct Bsp{
bsp:vbsp::Bsp,
case_folded_file_names:std::collections::HashMap<String,String>,
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.bsp
}
}
pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
let mut s=Vec::new();
@ -23,15 +68,66 @@ pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
vbsp::Bsp::read(s.as_slice()).map(Bsp::new).map_err(ReadError::Bsp)
}
impl Bsp{
pub fn new(bsp:vbsp::Bsp)->Self{
let case_folded_file_names=bsp.pack.clone().into_zip().lock().unwrap().file_names().map(|s|{
(s.to_lowercase(),s.to_owned())
}).collect();
Self{
bsp,
case_folded_file_names,
}
}
pub fn pack_get(&self,name_lowercase:&str)->Result<Option<Vec<u8>>,vbsp::BspError>{
match self.case_folded_file_names.get(name_lowercase){
Some(name_folded)=>self.bsp.pack.get(name_folded),
None=>Ok(None),
}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[Vpk])->Result<strafesnet_common::map::CompleteMap,LoadError>{
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
bsp:&Bsp,
acquire_render_config_id:AcquireRenderConfigId,
acquire_mesh_id:AcquireMeshId
)->bsp::PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
{
bsp::convert_bsp(bsp.as_ref(),acquire_render_config_id,acquire_mesh_id)
}
let map_step1=bsp::convert(
self,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader);
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
let map_step2=map_step1.add_prop_meshes(prop_meshes);
let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
}
}
pub struct Vpk{
vpk:vpk::VPK,
case_folded_file_names:std::collections::HashMap<String,String>,
}
impl AsRef<vpk::VPK> for Vpk{
fn as_ref(&self)->&vpk::VPK{
&self.vpk
}
}
impl Vpk{
pub fn new(vpk:vpk::VPK)->Vpk{
let case_folded_file_names=vpk.tree.keys().map(|s|{
(s.to_lowercase(),s.to_owned())
}).collect();
Vpk{
vpk,
case_folded_file_names,
}
}
pub fn tree_get(&self,name_lowercase:&str)->Option<&vpk::entry::VPKEntry>{
let name_folded=self.case_folded_file_names.get(name_lowercase)?;
self.vpk.tree.get(name_folded)
}
}

@ -0,0 +1,165 @@
use std::{borrow::Cow, io::Read};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk};
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
pub struct TextureLoader;
impl TextureLoader{
pub fn new()->Self{
Self
}
}
impl Loader for TextureLoader{
type Error=TextureError;
type Index<'a>=Cow<'a,str>;
type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
VMDL(vmdl::ModelError),
VBSP(vbsp::BspError),
MissingMdl(String),
MissingVtx,
MissingVvd,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<vmdl::ModelError> for MeshError{
fn from(value:vmdl::ModelError)->Self{
Self::VMDL(value)
}
}
impl From<vbsp::BspError> for MeshError{
fn from(value:vbsp::BspError)->Self{
Self::VBSP(value)
}
}
#[derive(Clone,Copy)]
pub struct BspFinder<'bsp,'vpk>{
pub bsp:&'bsp Bsp,
pub vpks:&'vpk [Vpk],
}
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
where
'bsp:'a,
'vpk:'a,
{
// search bsp
if let Some(data)=self.bsp.pack_get(path)?{
return Ok(Some(Cow::Owned(data)));
}
//search each vpk
for vpk in self.vpks{
if let Some(vpk_entry)=vpk.tree_get(path){
return Ok(Some(vpk_entry.get()?));
}
}
Ok(None)
}
}
pub struct ModelLoader<'bsp,'vpk>{
finder:BspFinder<'bsp,'vpk>,
}
impl ModelLoader<'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk>(
finder:BspFinder<'bsp,'vpk>,
)->ModelLoader<'bsp,'vpk>{
ModelLoader{
finder,
}
}
}
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{
type Error=MeshError;
type Index<'a>=&'a str where Self:'a;
type Resource=vmdl::Model;
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let mdl_path_lower=index.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mdl_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path;
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
// TODO: search more packs, possibly using an index of multiple packs
let mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl(mdl_path_lower))?;
let vtx=self.finder.find(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
let vvd=self.finder.find(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(mdl.as_ref())?,
vmdl::vtx::Vtx::read(vtx.as_ref())?,
vmdl::vvd::Vvd::read(vvd.as_ref())?,
))
}
}
pub struct MeshLoader<'bsp,'vpk,'load,'str>{
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
}
impl MeshLoader<'_,'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'load,'str>(
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>,
)->MeshLoader<'bsp,'vpk,'load,'str>{
MeshLoader{
finder,
deferred_loader
}
}
}
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{
type Error=MeshError;
type Index<'a>=&'a str where Self:'a;
type Resource=Mesh;
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh)
}
}

@ -0,0 +1,78 @@
use std::borrow::Cow;
use strafesnet_common::model;
use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
use crate::valve_transform;
fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{
let pos=mb.acquire_pos_id(valve_transform(vertex.position.into()));
let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into()));
let tex=mb.acquire_tex_id(glam::Vec2::from_array(vertex.texture_coordinates));
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let model_vertices=model.vertices();
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned()));
deferred_loader.acquire_render_config_id(index)
}else{
deferred_loader.acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|{
std::iter::from_fn(move ||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
}).flat_map(|[v1,v2,v3]|{
// this should probably be a fatal error :D
let v1=model_vertices.get(v1)?;
let v2=model_vertices.get(v2)?;
let v3=model_vertices.get(v3)?;
Some(vec![
ingest_vertex(&mut mb,v1,color),
ingest_vertex(&mut mb,v2,color),
ingest_vertex(&mut mb,v3,color),
])
}).collect()
))
}).collect();
mb.build(polygon_groups,graphics_groups,physics_groups)
}

@ -1,7 +1,7 @@
[package]
name = "strafesnet_common"
version = "0.5.2"
edition = "2021"
version = "0.6.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Common types and helpers for Strafe Client associated projects."
@ -12,8 +12,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
arrayvec = "0.7.4"
bitflags = "2.6.0"
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { path = "../ratio_ops", registry = "strafesnet" }
glam = "0.29.0"
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }

@ -7,42 +7,57 @@ pub struct Aabb{
}
impl Default for Aabb{
#[inline]
fn default()->Self{
Self{min:vec3::MAX,max:vec3::MIN}
}
}
impl Aabb{
#[inline]
pub const fn new(min:Planar64Vec3,max:Planar64Vec3)->Self{
Self{min,max}
}
#[inline]
pub const fn max(&self)->Planar64Vec3{
self.max
}
#[inline]
pub const fn min(&self)->Planar64Vec3{
self.min
}
#[inline]
pub fn grow(&mut self,point:Planar64Vec3){
self.min=self.min.min(point);
self.max=self.max.max(point);
}
#[inline]
pub fn join(&mut self,aabb:&Aabb){
self.min=self.min.min(aabb.min);
self.max=self.max.max(aabb.max);
}
#[inline]
pub fn inflate(&mut self,hs:Planar64Vec3){
self.min-=hs;
self.max+=hs;
}
#[inline]
pub fn contains(&self,point:Planar64Vec3)->bool{
let bvec=self.min.lt(point)&point.lt(self.max);
bvec.all()
}
#[inline]
pub fn intersects(&self,aabb:&Aabb)->bool{
let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max);
bvec.all()
}
#[inline]
pub fn size(&self)->Planar64Vec3{
self.max-self.min
}
#[inline]
pub fn center(&self)->Planar64Vec3{
self.min+(self.max-self.min)>>1
self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
}
//probably use floats for area & volume because we don't care about precision
// pub fn area_weight(&self)->f32{

@ -1,4 +1,10 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;
use crate::aabb::Aabb;
use crate::ray::Ray;
use crate::integer::{Ratio,Planar64};
use crate::instruction::{InstructionCollector,TimedInstruction};
//da algaritum
//lista boxens
@ -10,35 +16,117 @@ use crate::aabb::Aabb;
//sort the centerpoints on each axis (3 lists)
//bv is put into octant based on whether it is upper or lower in each list
pub enum RecursiveContent<R,T>{
Branch(Vec<R>),
Leaf(T),
pub fn intersect_aabb(ray:&Ray,aabb:&Aabb)->Option<Ratio<Planar64,Planar64>>{
// n.(o+d*t)==n.p
// n.o + n.d * t == n.p
// t == (n.p - n.o)/n.d
let mut hit=None;
match ray.direction.x.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_max.x*ray.direction.y;
let dz=rel_max.x*ray.direction.z;
// x is negative, so inequalities are flipped
if rel_min.y*ray.direction.x>dy&&dy>rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x>dz&&dz>rel_max.z*ray.direction.x{
let t=rel_max.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_min.x*ray.direction.y;
let dz=rel_min.x*ray.direction.z;
// x is positive, so inequalities are normal
if rel_min.y*ray.direction.x<dy&&dy<rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x<dz&&dz<rel_max.z*ray.direction.x{
let t=rel_min.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.z.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_max.z*ray.direction.x;
let dy=rel_max.z*ray.direction.y;
// z is negative, so inequalities are flipped
if rel_min.x*ray.direction.z>dx&&dx>rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z>dy&&dy>rel_max.y*ray.direction.z{
let t=rel_max.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_min.z*ray.direction.x;
let dy=rel_min.z*ray.direction.y;
// z is positive, so inequalities are normal
if rel_min.x*ray.direction.z<dx&&dx<rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z<dy&&dy<rel_max.y*ray.direction.z{
let t=rel_min.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.y.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_max.y*ray.direction.z;
let dx=rel_max.y*ray.direction.x;
// y is negative, so inequalities are flipped
if rel_min.z*ray.direction.y>dz&&dz>rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y>dx&&dx>rel_max.x*ray.direction.y{
let t=rel_max.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_min.y*ray.direction.z;
let dx=rel_min.y*ray.direction.x;
// y is positive, so inequalities are normal
if rel_min.z*ray.direction.y<dz&&dz<rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y<dx&&dx<rel_max.x*ray.direction.y{
let t=rel_min.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
hit
}
impl<R,T> Default for RecursiveContent<R,T>{
fn default()->Self{
pub enum RecursiveContent<N,L>{
Branch(Vec<N>),
Leaf(L),
}
impl<N,L> RecursiveContent<N,L>{
pub fn empty()->Self{
Self::Branch(Vec::new())
}
}
pub struct BvhNode<T>{
content:RecursiveContent<BvhNode<T>,T>,
pub struct BvhNode<L>{
content:RecursiveContent<BvhNode<L>,L>,
aabb:Aabb,
}
impl<T> Default for BvhNode<T>{
fn default()->Self{
impl<L> BvhNode<L>{
pub fn empty()->Self{
Self{
content:Default::default(),
content:RecursiveContent::empty(),
aabb:Aabb::default(),
}
}
}
pub struct BvhWeightNode<W,T>{
content:RecursiveContent<BvhWeightNode<W,T>,T>,
weight:W,
aabb:Aabb,
}
impl<T> BvhNode<T>{
pub fn the_tester<F:FnMut(&T)>(&self,aabb:&Aabb,f:&mut F){
pub fn sample_aabb<F:FnMut(&L)>(&self,aabb:&Aabb,f:&mut F){
match &self.content{
RecursiveContent::Leaf(model)=>f(model),
RecursiveContent::Branch(children)=>for child in children{
@ -47,51 +135,101 @@ impl<T> BvhNode<T>{
//you're probably not going to spend a lot of time outside the map,
//so the test is extra work for nothing
if aabb.intersects(&child.aabb){
child.the_tester(aabb,f);
child.sample_aabb(aabb,f);
}
},
}
}
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
match self.content{
RecursiveContent::Leaf(model)=>f(model),
fn populate_nodes<'a,T,F>(
&'a self,
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>,
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
ray:&Ray,
start_time:Ratio<Planar64,Planar64>,
f:&F,
)
where
T:Ord+Copy,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
match &self.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
RecursiveContent::Branch(children)=>for child in children{
child.into_visitor(f)
},
}
}
pub fn weigh_contents<W:Copy+std::iter::Sum<W>,F:Fn(&T)->W>(self,f:&F)->BvhWeightNode<W,T>{
match self.content{
RecursiveContent::Leaf(model)=>BvhWeightNode{
weight:f(&model),
content:RecursiveContent::Leaf(model),
aabb:self.aabb,
},
RecursiveContent::Branch(children)=>{
let branch:Vec<BvhWeightNode<W,T>>=children.into_iter().map(|child|
child.weigh_contents(f)
).collect();
BvhWeightNode{
weight:branch.iter().map(|node|node.weight).sum(),
content:RecursiveContent::Branch(branch),
aabb:self.aabb,
if child.aabb.contains(ray.origin){
child.populate_nodes(collector,nodes,ray,start_time,f);
}else{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){
nodes.insert(t,child);
}
}
}
},
}
}
}
pub fn sample_ray<T,F>(
&self,
ray:&Ray,
start_time:T,
time_limit:T,
f:F,
)->Option<(T,&L)>
where
T:Ord+Copy,
T:From<Ratio<Planar64,Planar64>>,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
// source of nondeterminism when Aabb boundaries are coplanar
let mut nodes=BTreeMap::new();
impl <W,T> BvhWeightNode<W,T>{
pub const fn weight(&self)->&W{
&self.weight
let start_time=start_time.into();
let time_limit=time_limit.into();
let mut collector=InstructionCollector::new(time_limit);
// break open all nodes that contain ray.origin and populate nodes with future intersection times
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
// swim through nodes one at a time
while let Some((t,node))=nodes.pop_first(){
if collector.time()<t{
break;
}
match &node.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
// this lower bound can also be omitted
// but it causes type inference errors lol
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
// break open the node and predict collisions with the child nodes
RecursiveContent::Branch(children)=>for child in children{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
// we don't need to check the lower bound
// because child aabbs are guaranteed to be within the parent bounds.
if t<collector.time(){
nodes.insert(t,child);
}
}
},
}
}
collector.take().map(|TimedInstruction{time,instruction:leaf}|(time.into(),leaf))
}
pub const fn aabb(&self)->&Aabb{
&self.aabb
pub fn into_inner(self)->(RecursiveContent<BvhNode<L>,L>,Aabb){
(self.content,self.aabb)
}
pub fn into_content(self)->RecursiveContent<BvhWeightNode<W,T>,T>{
self.content
}
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
pub fn into_visitor<F:FnMut(L)>(self,f:&mut F){
match self.content{
RecursiveContent::Leaf(model)=>f(model),
RecursiveContent::Branch(children)=>for child in children{
@ -130,9 +268,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
sort_y.push((i,center.y));
sort_z.push((i,center.z));
}
sort_x.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_y.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_z.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_x.sort_by_key(|&(_,c)|c);
sort_y.sort_by_key(|&(_,c)|c);
sort_z.sort_by_key(|&(_,c)|c);
let h=n/2;
let median_x=sort_x[h].1;
let median_y=sort_y[h].1;

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

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

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

@ -1,13 +1,11 @@
use crate::integer::Time;
#[derive(Clone,Debug)]
pub struct TimedInstruction<I,T>{
pub time:Time<T>,
pub time:T,
pub instruction:I,
}
impl<I,T> TimedInstruction<I,T>{
#[inline]
pub fn set_time<TimeInner>(self,new_time:Time<TimeInner>)->TimedInstruction<I,TimeInner>{
pub fn set_time<T2>(self,new_time:T2)->TimedInstruction<I,T2>{
TimedInstruction{
time:new_time,
instruction:self.instruction,
@ -17,21 +15,21 @@ impl<I,T> TimedInstruction<I,T>{
/// Ensure all emitted instructions are processed before consuming external instructions
pub trait InstructionEmitter<I>{
type TimeInner;
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
type Time;
fn next_instruction(&self,time_limit:Self::Time)->Option<TimedInstruction<I,Self::Time>>;
}
/// Apply an atomic state update
pub trait InstructionConsumer<I>{
type TimeInner;
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
type Time;
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::Time>);
}
/// If the object produces its own instructions, allow exhaustively feeding them back in
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>
where
Time<T>:Copy,
T:Copy,
{
#[inline]
fn process_exhaustive(&mut self,time_limit:Time<T>){
fn process_exhaustive(&mut self,time_limit:T){
while let Some(instruction)=self.next_instruction(time_limit){
self.process_instruction(instruction);
}
@ -39,39 +37,24 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+Instruction
}
impl<I,T,X> InstructionFeedback<I,T> for X
where
Time<T>:Copy,
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
T:Copy,
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
{}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{
time:Time<T>,
time:T,
instruction:Option<I>,
}
impl<I,T> InstructionCollector<I,T>
where Time<T>:Copy+PartialOrd,
{
impl<I,T> InstructionCollector<I,T>{
#[inline]
pub const fn new(time:Time<T>)->Self{
pub const fn new(time:T)->Self{
Self{
time,
instruction:None
}
}
#[inline]
pub const fn time(&self)->Time<T>{
self.time
}
#[inline]
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
if let Some(ins)=instruction{
if ins.time<self.time{
self.time=ins.time;
self.instruction=Some(ins.instruction);
}
}
}
#[inline]
pub fn take(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
self.instruction.map(|instruction|TimedInstruction{
@ -80,3 +63,20 @@ impl<I,T> InstructionCollector<I,T>
})
}
}
impl<I,T:Copy> InstructionCollector<I,T>{
#[inline]
pub const fn time(&self)->T{
self.time
}
}
impl<I,T:PartialOrd> InstructionCollector<I,T>{
#[inline]
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
if let Some(ins)=instruction{
if ins.time<self.time{
self.time=ins.time;
self.instruction=Some(ins.instruction);
}
}
}
}

@ -1,14 +1,14 @@
pub use fixed_wide::fixed::{Fixed,Fix};
pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide};
//integer units
/// specific example of a "default" time type
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub enum TimeInner{}
pub type AbsoluteTime=Time<TimeInner>;
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)]
pub struct Time<T>(i64,core::marker::PhantomData<T>);
impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN);
@ -60,18 +60,24 @@ impl<T> Time<T>{
impl<T> From<Planar64> for Time<T>{
#[inline]
fn from(value:Planar64)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_1().to_raw())
}
}
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
#[inline]
fn from(value:Time<T>)->Self{
value.to_ratio()
}
}
impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T>
where
Num:core::ops::Mul<Planar64,Output=N1>,
N1:Divide<Den,Output=T1>,
T1:Fix<Planar64>,
T1:Clamp<Planar64>,
{
#[inline]
fn from(value:Ratio<Num,Den>)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).divide().clamp().to_raw())
}
}
impl<T> std::fmt::Display for Time<T>{
@ -395,6 +401,10 @@ impl Angle32{
pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
pub const PI:Self=Self(-1<<31);
#[inline]
pub const fn raw(num:i32)->Self{
Self(num)
}
#[inline]
pub const fn wrap_from_i64(theta:i64)->Self{
//take lower bits
//note: this was checked on compiler explorer and compiles to 1 instruction!
@ -509,8 +519,8 @@ fn angle_sin_cos(){
println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
let (fs,fc)=f.sin_cos();
println!("float s={} c={}",fs,fc);
assert!(close_enough((c/h).divide().fix_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().fix_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
assert!(close_enough((c/h).divide().wrap_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().wrap_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
}
test_angle(1.0);
test_angle(std::f64::consts::PI/4.0);
@ -552,6 +562,10 @@ pub mod vec3{
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 Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
@ -619,8 +633,8 @@ pub mod mat3{
let (yc,ys)=y.cos_sin();
Planar64Mat3::from_cols([
Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
Planar64Vec3::new([(xs*ys).fix_1(),yc,(xc*ys).fix_1()]),
Planar64Vec3::new([(xs*yc).fix_1(),-ys,(xc*yc).fix_1()]),
Planar64Vec3::new([(xs*ys).wrap_1(),yc,(xc*ys).wrap_1()]),
Planar64Vec3::new([(xs*yc).wrap_1(),-ys,(xc*yc).wrap_1()]),
])
}
#[inline]
@ -648,13 +662,21 @@ pub struct Planar64Affine3{
pub translation:Planar64Vec3,
}
impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
#[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation}
}
#[inline]
pub const fn from_translation(translation:Planar64Vec3)->Self{
Self{
matrix3:mat3::identity(),
translation,
}
}
#[inline]
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
self.translation.fix_2()+self.matrix3*point
self.translation.widen_2()+self.matrix3*point
}
}
impl Into<glam::Mat4> for Planar64Affine3{

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

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

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::integer::{Planar64Vec3,Planar64Affine3};
use crate::gameplay_attributes;
@ -123,6 +125,87 @@ pub struct Mesh{
pub physics_groups:Vec<IndexedPhysicsGroup>,
}
#[derive(Default)]
pub struct MeshBuilder{
unique_pos:Vec<Planar64Vec3>,//Unit32Vec3
unique_normal:Vec<Planar64Vec3>,//Unit32Vec3
unique_tex:Vec<TextureCoordinate>,
unique_color:Vec<Color4>,
unique_vertices:Vec<IndexedVertex>,
pos_id_from:HashMap<Planar64Vec3,PositionId>,//Unit32Vec3
normal_id_from:HashMap<Planar64Vec3,NormalId>,//Unit32Vec3
tex_id_from:HashMap<[u32;2],TextureCoordinateId>,
color_id_from:HashMap<[u32;4],ColorId>,
vertex_id_from:HashMap<IndexedVertex,VertexId>,
}
impl MeshBuilder{
pub fn new()->Self{
Self::default()
}
pub fn build(
self,
polygon_groups:Vec<PolygonGroup>,
graphics_groups:Vec<IndexedGraphicsGroup>,
physics_groups:Vec<IndexedPhysicsGroup>,
)->Mesh{
let MeshBuilder{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
..
}=self;
Mesh{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
polygon_groups,
graphics_groups,
physics_groups,
}
}
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{
*self.pos_id_from.entry(pos).or_insert_with(||{
let pos_id=PositionId::new(self.unique_pos.len() as u32);
self.unique_pos.push(pos);
pos_id
})
}
pub fn acquire_normal_id(&mut self,normal:Planar64Vec3)->NormalId{
*self.normal_id_from.entry(normal).or_insert_with(||{
let normal_id=NormalId::new(self.unique_normal.len() as u32);
self.unique_normal.push(normal);
normal_id
})
}
pub fn acquire_tex_id(&mut self,tex:TextureCoordinate)->TextureCoordinateId{
let h=tex.to_array().map(f32::to_bits);
*self.tex_id_from.entry(h).or_insert_with(||{
let tex_id=TextureCoordinateId::new(self.unique_tex.len() as u32);
self.unique_tex.push(tex);
tex_id
})
}
pub fn acquire_color_id(&mut self,color:Color4)->ColorId{
let h=color.to_array().map(f32::to_bits);
*self.color_id_from.entry(h).or_insert_with(||{
let color_id=ColorId::new(self.unique_color.len() as u32);
self.unique_color.push(color);
color_id
})
}
pub fn acquire_vertex_id(&mut self,vertex:IndexedVertex)->VertexId{
*self.vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=VertexId::new(self.unique_vertices.len() as u32);
self.unique_vertices.push(vertex);
vertex_id
})
}
}
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
pub struct ModelId(u32);
pub struct Model{

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

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

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

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

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

@ -23,7 +23,7 @@ impl PauseState for Unpaused{
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Inner{}
pub enum Inner{}
type InnerTime=Time<Inner>;
#[derive(Clone,Copy,Debug)]

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

@ -1,7 +1,7 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.4.1"
edition = "2021"
version = "0.5.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Acquire IDs for objects before loading them in bulk."
@ -9,13 +9,5 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["legacy"]
legacy = ["dep:url","dep:vbsp"]
#roblox = ["dep:lazy-regex"]
#source = ["dep:vbsp"]
[dependencies]
strafesnet_common = { path = "../common", registry = "strafesnet" }
url = { version = "2.5.2", optional = true }
vbsp = { version = "0.6.0", optional = true }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }

@ -0,0 +1,116 @@
use std::collections::HashMap;
use crate::loader::Loader;
use crate::mesh::Meshes;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{Mesh,MeshId,RenderConfig,RenderConfigId,TextureId};
#[derive(Clone,Copy,Debug)]
pub enum LoadFailureMode{
DefaultToNone,
Fatal,
}
pub struct RenderConfigDeferredLoader<H>{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<H>,RenderConfigId>,
}
impl<H> RenderConfigDeferredLoader<H>{
pub fn new()->Self{
Self{
texture_count:0,
render_configs:Vec::new(),
render_config_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
pub fn acquire_render_config_id(&mut self,index:Option<H>)->RenderConfigId{
let some_texture=index.is_some();
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if some_texture{
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
let render_id=RenderConfigId::new(self.render_configs.len() as u32);
self.render_configs.push(render_config);
render_id
})
}
pub fn into_indices(self)->impl Iterator<Item=H>{
self.render_config_id_from_asset_id.into_keys().flatten()
}
pub fn into_render_configs<'a,L:Loader<Resource=Texture,Index<'a>=H>+'a>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
let mut sorted_textures=vec![None;self.texture_count as usize];
for (index_option,render_config_id) in self.render_config_id_from_asset_id{
let render_config=&mut self.render_configs[render_config_id.get() as usize];
if let (Some(index),Some(texture_id))=(index_option,render_config.texture){
let resource_result=loader.load(index);
let texture=match failure_mode{
// if texture fails to load, use no texture
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(texture)=>Some(texture),
Err(e)=>{
render_config.texture=None;
println!("Error loading texture: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
sorted_textures[texture_id.get() as usize]=texture;
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_configs,
))
}
}
pub struct MeshDeferredLoader<H>{
mesh_id_from_asset_id:HashMap<H,MeshId>,
}
impl<H> MeshDeferredLoader<H>{
pub fn new()->Self{
Self{
mesh_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn acquire_mesh_id(&mut self,index:H)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn into_indices(self)->impl Iterator<Item=H>{
self.mesh_id_from_asset_id.into_keys()
}
pub fn into_meshes<'a,L:Loader<Resource=Mesh,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index);
let mesh=match failure_mode{
// if mesh fails to load, use no mesh
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(mesh)=>Some(mesh),
Err(e)=>{
println!("Error loading mesh: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
mesh_list[mesh_id.get() as usize]=mesh;
}
Ok(Meshes::new(mesh_list))
}
}

@ -1,34 +1,5 @@
#[cfg(feature="legacy")]
mod roblox_legacy;
#[cfg(feature="legacy")]
mod source_legacy;
#[cfg(feature="roblox")]
mod roblox;
#[cfg(feature="source")]
mod source;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod rbxassetid;
pub mod mesh;
pub mod loader;
pub mod texture;
#[cfg(any(feature="source",feature="legacy"))]
pub mod valve_mesh;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod roblox_mesh;
#[cfg(feature="legacy")]
pub fn roblox_legacy()->roblox_legacy::Loader{
roblox_legacy::Loader::new()
}
#[cfg(feature="legacy")]
pub fn source_legacy()->source_legacy::Loader{
source_legacy::Loader::new()
}
#[cfg(feature="roblox")]
pub fn roblox()->roblox::Loader{
roblox::Loader::new()
}
#[cfg(feature="source")]
pub fn source()->source::Loader{
source::Loader::new()
}
pub mod deferred_loader;

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

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

@ -1,48 +0,0 @@
#[derive(Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
#[allow(dead_code)]
pub struct StringWithError{
string:String,
error:RobloxAssetIdParseErr,
}
impl std::fmt::Display for StringWithError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for StringWithError{}
impl StringWithError{
const fn new(
string:String,
error:RobloxAssetIdParseErr,
)->Self{
Self{string,error}
}
}
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
}
impl std::str::FromStr for RobloxAssetId{
type Err=StringWithError;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::Url(e)))?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?;
asset_id.parse()
},
_=>Err(StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::UnknownScheme))?,
};
Ok(Self(parsed_asset_id.map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::ParseInt(e)))?))
}
}

@ -1,112 +0,0 @@
use std::io::Read;
use std::collections::HashMap;
use crate::roblox_mesh;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId};
use crate::rbxassetid::RobloxAssetId;
#[derive(Default)]
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<RobloxAssetId>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.render_config_id_from_asset_id.len() as u32);
let index=name.and_then(|name|{
match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
}
});
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
#[derive(Default)]
pub struct MeshLoader{
mesh_id_from_asset_id:HashMap<Option<RobloxAssetId>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
let index=match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
};
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn load_meshes(&mut self)->Result<roblox_mesh::Meshes,std::io::Error>{
let mut mesh_data=vec![None;self.mesh_id_from_asset_id.len()];
for (asset_id_option,mesh_id) in &self.mesh_id_from_asset_id{
if let Some(asset_id)=asset_id_option{
if let Ok(mut file)=std::fs::File::open(format!("meshes/{}",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
mesh_data[mesh_id.get() as usize]=Some(roblox_mesh::RobloxMeshData::new(data));
}else{
println!("[roblox_legacy] no mesh name={}",asset_id.0);
}
}
}
Ok(roblox_mesh::Meshes::new(mesh_data))
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader::default(),
mesh_loader:MeshLoader::default(),
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (asset_id_option,render_config_id) in self.render_config_loader.render_config_id_from_asset_id{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(asset_id),Some(texture_id))=(asset_id_option,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -1,30 +0,0 @@
use strafesnet_common::model::MeshId;
#[derive(Clone)]
pub struct RobloxMeshData(Vec<u8>);
impl RobloxMeshData{
pub(crate) fn new(data:Vec<u8>)->Self{
Self(data)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
pub struct Meshes{
meshes:Vec<Option<RobloxMeshData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<RobloxMeshData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&RobloxMeshData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,RobloxMeshData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,102 +0,0 @@
use std::io::Read;
use std::collections::HashMap;
use crate::valve_mesh;
use crate::texture::{Texture,RenderConfigs};
use strafesnet_common::model::{MeshId,TextureId,RenderConfig,RenderConfigId};
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
texture_paths:HashMap<Option<Box<str>>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.texture_paths.len() as u32);
*self.texture_paths.entry(name.map(Into::into)).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
pub struct MeshLoader{
mesh_paths:HashMap<Box<str>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_paths.len() as u32);
*self.mesh_paths.entry(name.into()).or_insert(mesh_id)
}
//load_meshes should look like load_textures
pub fn load_meshes(&mut self,bsp:&vbsp::Bsp)->valve_mesh::Meshes{
let mut mesh_data=vec![None;self.mesh_paths.len()];
for (mesh_path,mesh_id) in &self.mesh_paths{
let mesh_path_lower=mesh_path.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mesh_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path.clone();
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
match (bsp.pack.get(mesh_path_lower.as_str()),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
mesh_data[mesh_id.get() as usize]=Some(valve_mesh::ModelData{
mdl:valve_mesh::MdlData::new(mdl_file),
vtx:valve_mesh::VtxData::new(vtx_file),
vvd:valve_mesh::VvdData::new(vvd_file),
});
},
_=>println!("no model name={}",mesh_path),
}
}
valve_mesh::Meshes::new(mesh_data)
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader{
texture_count:0,
texture_paths:HashMap::new(),
render_configs:Vec::new(),
},
mesh_loader:MeshLoader{mesh_paths:HashMap::new()},
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (texture_path,render_config_id) in self.render_config_loader.texture_paths{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(texture_path),Some(texture_id))=(texture_path,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",texture_path)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -1,60 +0,0 @@
use strafesnet_common::model::MeshId;
//duplicate this code for now
#[derive(Clone)]
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
//meshes is more prone to failure
pub struct Meshes{
meshes:Vec<Option<ModelData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<ModelData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&ModelData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,ModelData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,7 +1,7 @@
[package]
name = "fixed_wide"
version = "0.1.1"
edition = "2021"
version = "0.2.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Fixed point numbers with optional widening Mul operator."
@ -14,7 +14,7 @@ wide-mul=[]
zeroes=["dep:arrayvec"]
[dependencies]
bnum = "0.12.0"
bnum = "0.13.0"
arrayvec = { version = "0.7.6", optional = true }
paste = "1.0.15"
ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }

@ -33,6 +33,14 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
self.bits
}
#[inline]
pub const fn as_bits(&self)->&BInt<N>{
&self.bits
}
#[inline]
pub const fn as_bits_mut(&mut self)->&mut BInt<N>{
&mut self.bits
}
#[inline]
pub const fn raw_digit(value:i64)->Self{
let mut digits=[0u64;N];
digits[0]=value.abs() as u64;
@ -56,6 +64,10 @@ impl<const N:usize,const F:usize> Fixed<N,F>{
pub const fn abs(self)->Self{
Self::from_bits(self.bits.abs())
}
#[inline]
pub const fn midpoint(self,other:Self)->Self{
Self::from_bits(self.bits.midpoint(other.bits))
}
}
impl<const F:usize> Fixed<1,F>{
/// My old code called this function everywhere so let's provide it
@ -651,74 +663,94 @@ macro_repeated!(
1,2,3,4,5,6,7,8
);
pub trait Fix<Out>{
fn fix(self)->Out;
#[derive(Debug,Eq,PartialEq)]
pub enum NarrowError{
Overflow,
Underflow,
}
macro_rules! impl_fix_rhs_lt_lhs_not_const_generic{
pub trait Wrap<Output>{
fn wrap(self)->Output;
}
pub trait Clamp<Output>{
fn clamp(self)->Output;
}
impl<const N:usize,const F:usize> Clamp<Fixed<N,F>> for Result<Fixed<N,F>,NarrowError>{
fn clamp(self)->Fixed<N,F>{
match self{
Ok(fixed)=>fixed,
Err(NarrowError::Overflow)=>Fixed::MAX,
Err(NarrowError::Underflow)=>Fixed::MIN,
}
}
}
macro_rules! impl_narrow_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
paste::item!{
impl Fixed<$lhs,{$lhs*32}>
{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
pub fn [<wrap_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits.shr(($lhs-$rhs)*32)))
}
#[inline]
pub fn [<narrow_ $rhs>](self)->Result<Fixed<$rhs,{$rhs*32}>,NarrowError>{
if Fixed::<$rhs,{$rhs*32}>::MAX.[<widen_ $lhs>]().bits<self.bits{
return Err(NarrowError::Overflow);
}
if self.bits<Fixed::<$rhs,{$rhs*32}>::MIN.[<widen_ $lhs>]().bits{
return Err(NarrowError::Underflow);
}
Ok(self.[<wrap_ $rhs>]())
}
#[inline]
pub fn [<clamp_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
self.[<narrow_ $rhs>]().clamp()
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
impl Wrap<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
fn wrap(self)->Fixed<$rhs,{$rhs*32}>{
self.[<wrap_ $rhs>]()
}
}
impl TryInto<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
type Error=NarrowError;
#[inline]
fn try_into(self)->Result<Fixed<$rhs,{$rhs*32}>,Self::Error>{
self.[<narrow_ $rhs>]()
}
}
impl Clamp<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
fn clamp(self)->Fixed<$rhs,{$rhs*32}>{
self.[<clamp_ $rhs>]()
}
}
}
}
}
macro_rules! impl_fix_lhs_lt_rhs_not_const_generic{
macro_rules! impl_widen_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
paste::item!{
impl Fixed<$lhs,{$lhs*32}>
{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
pub fn [<widen_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
Fixed::from_bits(bnum::cast::As::as_::<BInt::<$rhs>>(self.bits).shl(($rhs-$lhs)*32))
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
}
}
}
}
}
macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
(
(),
($lhs:expr,$rhs:expr)
)=>{
impl Fixed<$lhs,{$lhs*32}>
{
paste::item!{
impl Into<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
#[inline]
pub fn [<fix_ $rhs>](self)->Fixed<$rhs,{$rhs*32}>{
self
}
}
}
impl Fix<Fixed<$rhs,{$rhs*32}>> for Fixed<$lhs,{$lhs*32}>{
fn fix(self)->Fixed<$rhs,{$rhs*32}>{
paste::item!{
self.[<fix_ $rhs>]()
fn into(self)->Fixed<$rhs,{$rhs*32}>{
self.[<widen_ $rhs>]()
}
}
}
@ -728,7 +760,7 @@ macro_rules! impl_fix_lhs_eq_rhs_not_const_generic{
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
macro_repeated!(
impl_fix_rhs_lt_lhs_not_const_generic,(),
impl_narrow_not_const_generic,(),
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
@ -746,7 +778,7 @@ macro_repeated!(
(16,15)
);
macro_repeated!(
impl_fix_lhs_lt_rhs_not_const_generic,(),
impl_widen_not_const_generic,(),
(1,2),
(1,3),(2,3),
(1,4),(2,4),(3,4),
@ -761,11 +793,8 @@ macro_repeated!(
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16)
);
macro_repeated!(
impl_fix_lhs_eq_rhs_not_const_generic,(),
(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10),(11,11),(12,12),(13,13),(14,14),(15,15),(16,16)
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
(1,17)
);
macro_rules! impl_not_const_generic{
@ -785,16 +814,13 @@ macro_rules! impl_not_const_generic{
let mut result=Self::ZERO;
//resize self to match the wide mul output
let wide_self=self.[<fix_ $_2n>]();
let wide_self=self.[<widen_ $_2n>]();
//descend down the bits and check if flipping each bit would push the square over the input value
for shift in (0..=max_shift).rev(){
let new_result={
let mut bits=result.to_bits().to_bits();
bits.set_bit(shift,true);
Self::from_bits(BInt::from_bits(bits))
};
if new_result.[<wide_mul_ $n _ $n>](new_result)<=wide_self{
result=new_result;
result.as_bits_mut().as_bits_mut().set_bit(shift,true);
if wide_self<result.[<wide_mul_ $n _ $n>](result){
// put it back lol
result.as_bits_mut().as_bits_mut().set_bit(shift,false);
}
}
result

@ -57,11 +57,11 @@ fn from_f32(){
assert_eq!(b,Ok(a));
//I32F32::MIN hits a special case since it's not representable as a positive signed integer
//TODO: don't return an overflow because this is technically possible
let a=I32F32::MIN;
let _a=I32F32::MIN;
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
//16 is within the 24 bits of float precision
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.fix_2()).try_into();
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.widen_2()).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
let b:Result<I32F32,_>=f32::MIN_POSITIVE.try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Underflow));
@ -136,11 +136,24 @@ fn test_bint(){
}
#[test]
fn test_fix(){
assert_eq!(I32F32::ONE.fix_8(),I256F256::ONE);
assert_eq!(I32F32::ONE,I256F256::ONE.fix_1());
assert_eq!(I32F32::NEG_ONE.fix_8(),I256F256::NEG_ONE);
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.fix_1());
fn test_wrap(){
assert_eq!(I32F32::ONE,I256F256::ONE.wrap_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.wrap_1());
}
#[test]
fn test_narrow(){
assert_eq!(Ok(I32F32::ONE),I256F256::ONE.narrow_1());
assert_eq!(Ok(I32F32::NEG_ONE),I256F256::NEG_ONE.narrow_1());
}
#[test]
fn test_widen(){
assert_eq!(I32F32::ONE.widen_8(),I256F256::ONE);
assert_eq!(I32F32::NEG_ONE.widen_8(),I256F256::NEG_ONE);
}
#[test]
fn test_clamp(){
assert_eq!(I32F32::ONE,I256F256::ONE.clamp_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.clamp_1());
}
#[test]
fn test_sqrt(){

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

@ -1,7 +1,7 @@
[package]
name = "linear_ops"
version = "0.1.0"
edition = "2021"
version = "0.1.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Vector/Matrix operations using trait bounds."
@ -14,8 +14,8 @@ fixed-wide=["dep:fixed_wide","dep:paste"]
deferred-division=["dep:ratio_ops"]
[dependencies]
ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", optional = true }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", optional = true }
paste = { version = "1.0.15", optional = true }
[dev-dependencies]

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

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

@ -58,6 +58,15 @@ macro_rules! impl_vector {
}
}
impl<const N:usize,T,E:std::fmt::Debug> Vector<N,Result<T,E>>{
#[inline]
pub fn unwrap(self)->Vector<N,T>{
Vector{
array:self.array.map(Result::unwrap)
}
}
}
impl<const N:usize,T:Ord> Vector<N,T>{
#[inline]
pub fn min(self,rhs:Self)->Self{
@ -321,13 +330,19 @@ macro_rules! impl_vector_named_fields {
type Target=$struct<T>;
#[inline]
fn deref(&self)->&Self::Target{
unsafe{core::mem::transmute(&self.array)}
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&self.array as *const [T;$size] as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
}
}
impl<T> core::ops::DerefMut for Vector<$size,T>{
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
unsafe{core::mem::transmute(&mut self.array)}
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}
}
}

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

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

@ -1,7 +1,7 @@
[package]
name = "ratio_ops"
version = "0.1.0"
edition = "2021"
version = "0.1.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Ratio operations using trait bounds for avoiding division like the plague."

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

@ -1,7 +1,7 @@
[package]
name = "strafesnet_rbx_loader"
version = "0.5.2"
edition = "2021"
version = "0.6.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Convert Roblox place and model files to StrafesNET data structures."
@ -11,12 +11,14 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
bytemuck = "1.14.3"
glam = "0.29.0"
glam = "0.30.0"
lazy-regex = "3.1.0"
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.1.2"
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
roblox_emulator = { path = "../roblox_emulator", registry = "strafesnet" }
strafesnet_common = { path = "../common", registry = "strafesnet" }
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_mesh = "0.3.1"
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.5.0", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }

@ -1,8 +1,12 @@
use std::io::Read;
use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod rbx;
mod mesh;
mod union;
pub mod loader;
mod primitives;
pub mod data{
@ -24,11 +28,8 @@ impl Model{
fn new(dom:WeakDom)->Self{
Self{dom}
}
pub fn into_place(self)->Place{
let Self{mut dom}=self;
let context=roblox_emulator::context::Context::from_mut(&mut dom);
let services=context.convert_into_place();
Place{dom,services}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode)
}
}
impl AsRef<WeakDom> for Model{
@ -38,33 +39,41 @@ impl AsRef<WeakDom> for Model{
}
pub struct Place{
dom:WeakDom,
services:roblox_emulator::context::Services,
context:Context,
}
impl Place{
fn new(dom:WeakDom)->Option<Self>{
let context=roblox_emulator::context::Context::from_ref(&dom);
Some(Self{
services:context.find_services()?,
dom,
pub fn new(dom:WeakDom)->Result<Self,roblox_emulator::context::ServicesError>{
let context=Context::from_place(dom)?;
Ok(Self{
context,
})
}
pub fn run_scripts(&mut self){
let Place{dom,services}=self;
let Place{context}=self;
let runner=roblox_emulator::runner::Runner::new().unwrap();
let context=roblox_emulator::context::Context::from_mut(dom);
let scripts=context.scripts();
let runnable=runner.runnable_context_with_services(context,services).unwrap();
let runnable=runner.runnable_context(context).unwrap();
for script in scripts{
if let Err(e)=runnable.run_script(script){
println!("runner error: {e}");
}
}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode)
}
}
impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{
&self.dom
self.context.as_ref()
}
}
impl From<Model> for Place{
fn from(model:Model)->Self{
let context=Context::from_model(model.dom);
Self{
context,
}
}
}
@ -85,23 +94,56 @@ impl std::error::Error for ReadError{}
pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match &peek[0..8]{
b"<roblox!"=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
b"<roblox "=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
match peek.get(0..8){
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
_=>Err(ReadError::UnknownFileFormat),
}
}
//ConvertError
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:impl AsRef<WeakDom>,
acquire_render_config_id:AcquireRenderConfigId,
acquire_mesh_id:AcquireMeshId
)->rbx::PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
{
rbx::convert(&dom.as_ref(),acquire_render_config_id,acquire_mesh_id)
#[derive(Debug)]
pub enum LoadError{
Texture(loader::TextureError),
Mesh(loader::MeshError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadError{}
impl From<loader::TextureError> for LoadError{
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
let dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=rbx::convert(
dom,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=loader::MeshLoader::new();
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
}

@ -0,0 +1,196 @@
use std::io::Read;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(data)
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for TextureError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
pub struct TextureLoader;
impl TextureLoader{
pub fn new()->Self{
Self
}
}
impl Loader for TextureLoader{
type Error=TextureError;
type Index<'a>=&'a str;
type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
Mesh(crate::mesh::Error),
Union(crate::union::Error),
DecodeBinary(rbx_binary::DecodeError),
OneChildPolicy,
MissingInstance,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for MeshError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
impl From<crate::mesh::Error> for MeshError{
fn from(value:crate::mesh::Error)->Self{
Self::Mesh(value)
}
}
impl From<crate::union::Error> for MeshError{
fn from(value:crate::union::Error)->Self{
Self::Union(value)
}
}
impl From<rbx_binary::DecodeError> for MeshError{
fn from(value:rbx_binary::DecodeError)->Self{
Self::DecodeBinary(value)
}
}
#[derive(Hash,Eq,PartialEq)]
pub enum MeshType<'a>{
FileMesh,
Union{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size_float_bits:[u32;3],
part_texture_description:RobloxPartDescription,
},
}
#[derive(Hash,Eq,PartialEq)]
pub struct MeshIndex<'a>{
mesh_type:MeshType<'a>,
content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
}
}
pub fn union<'a>(
content:&'a str,
mesh_data:&'a [u8],
physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3,
part_texture_description:RobloxPartDescription,
)->MeshIndex<'a>{
MeshIndex{
mesh_type:MeshType::Union{
mesh_data,
physics_data,
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
part_texture_description,
},
content,
}
}
}
pub struct MeshLoader;
impl MeshLoader{
pub fn new()->Self{
Self
}
}
impl Loader for MeshLoader{
type Error=MeshError;
type Index<'a>=MeshIndex<'a>;
type Resource=Mesh;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("meshes/{}",asset_id);
let data=read_entire_file(file_name)?;
crate::mesh::convert(RobloxMeshBytes::new(data))?
},
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
// decode asset
let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits));
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("unions/{}",asset_id);
let data=read_entire_file(file_name)?;
let dom=rbx_binary::from_reader(std::io::Cursor::new(data))?;
let &[referent]=dom.root().children()else{
return Err(MeshError::OneChildPolicy);
};
let Some(instance)=dom.get_by_ref(referent)else{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
}
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
}else{
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
}
},
};
Ok(mesh)
}
}

@ -1,8 +1,9 @@
use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2, Vertex2Truncated};
use strafesnet_common::{integer::vec3,model::{self, ColorId, IndexedVertex, NormalId, PolygonGroup, PolygonList, PositionId, TextureCoordinateId, VertexId}};
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated};
use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}};
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
@ -83,13 +84,13 @@ where
fn ingest_faces2_lods3(
polygon_groups:&mut Vec<PolygonGroup>,
vertex_id_map:&HashMap<rbx_mesh::mesh::VertexId2,VertexId>,
faces:&Vec<rbx_mesh::mesh::Face2>,
lods:&Vec<rbx_mesh::mesh::Lod3>
faces:&[rbx_mesh::mesh::Face2],
lods:&[rbx_mesh::mesh::Lod3],
){
//faces have to be split into polygon groups based on lod
polygon_groups.extend(lods.windows(2).map(|lod_pair|
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
vec![vertex_id_map[&v0],vertex_id_map[&v1],vertex_id_map[&v2]]
).collect()))
))
}
@ -204,7 +205,13 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
unique_vertices,
polygon_groups,
//these should probably be moved to the model...
graphics_groups:Vec::new(),
//but what if models want to use the same texture
graphics_groups:vec![model::IndexedGraphicsGroup{
render:RenderConfigId::new(0),
//the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)]
}],
//disable physics
physics_groups:Vec::new(),
})
}

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

File diff suppressed because it is too large Load Diff

178
lib/rbx_loader/src/union.rs Normal file

@ -0,0 +1,178 @@
use rbx_mesh::mesh_data::{NormalId2 as MeshDataNormalId2,VertexId as MeshDataVertexId};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::integer::vec3;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Block,
MissingVertexId(u32),
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RobloxPhysicsData(rbx_mesh::physics_data::Error),
RobloxMeshData(rbx_mesh::mesh_data::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
// wacky state machine to make sure all vertices in a face agree upon what NormalId to use.
// Roblox duplicates this information per vertex when it should only exist per-face.
enum MeshDataNormalStatus{
Agree(MeshDataNormalId2),
Conflicting,
}
struct MeshDataNormalChecker{
status:Option<MeshDataNormalStatus>,
}
impl MeshDataNormalChecker{
fn new()->Self{
Self{status:None}
}
fn check(&mut self,normal:MeshDataNormalId2){
self.status=match self.status.take(){
None=>Some(MeshDataNormalStatus::Agree(normal)),
Some(MeshDataNormalStatus::Agree(old_normal))=>{
if old_normal==normal{
Some(MeshDataNormalStatus::Agree(old_normal))
}else{
Some(MeshDataNormalStatus::Conflicting)
}
},
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
};
}
fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
self.status.and_then(|status|match status{
MeshDataNormalStatus::Agree(normal)=>Some(normal),
MeshDataNormalStatus::Conflicting=>None,
})
}
}
impl std::error::Error for Error{}
pub fn convert(
roblox_physics_data:&[u8],
roblox_mesh_data:&[u8],
size:glam::Vec3,
crate::rbx::RobloxPartDescription(part_texture_description):crate::rbx::RobloxPartDescription,
)->Result<model::Mesh,Error>{
const NORMAL_FACES:usize=6;
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
// build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new();
// graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){
// create per-face texture coordinate affine transforms
let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
t.transform.set_size(1.0,1.0);
t.to_face_description()
}));
let mesh_data=rbx_mesh::read_mesh_data_versioned(
std::io::Cursor::new(roblox_mesh_data)
).map_err(Error::RobloxMeshData)?;
let graphics_mesh=match mesh_data{
rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
};
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|{
model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
}
}).collect()
}else{
Vec::new()
};
//physics
let physics_convex_meshes=if !roblox_physics_data.is_empty(){
let physics_data=rbx_mesh::read_physics_data_versioned(
std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?;
let physics_convex_meshes=match physics_data{
rbx_mesh::physics_data::PhysicsData::CSGK(_)
// have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::Block),
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
=>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
};
physics_convex_meshes
}else{
Vec::new()
};
let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces|
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
).chain(physics_convex_meshes.into_iter().map(|mesh|{
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let face=[
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
})).collect::<Result<_,_>>()?;
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)]
}).collect();
Ok(mb.build(
polygon_groups,
graphics_groups,
physics_groups,
))
}

11
lib/rbxassetid/Cargo.toml Normal file

@ -0,0 +1,11 @@
[package]
name = "rbxassetid"
version = "0.1.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Parse Roblox asset id from 'Content' urls."
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
url = "2.5.4"

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

26
lib/rbxassetid/README.md Normal file

@ -0,0 +1,26 @@
Roblox Asset Id
===============
## Example
```rust
use rbxassetid::RobloxAssetId;
let content="rbxassetid://255299419";
let RobloxAssetId(asset_id)=content.parse()?;
```
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>

41
lib/rbxassetid/src/lib.rs Normal file

@ -0,0 +1,41 @@
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
MissingIDQueryParam,
}
impl std::fmt::Display for RobloxAssetIdParseErr{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for RobloxAssetIdParseErr{}
impl std::str::FromStr for RobloxAssetId{
type Err=RobloxAssetIdParseErr;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(RobloxAssetIdParseErr::Url)?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or(RobloxAssetIdParseErr::MissingAssetId)?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or(RobloxAssetIdParseErr::MissingIDQueryParam)?;
asset_id.parse()
},
_=>Err(RobloxAssetIdParseErr::UnknownScheme)?,
};
Ok(Self(parsed_asset_id.map_err(RobloxAssetIdParseErr::ParseInt)?))
}
}
#[test]
fn test_rbxassetid(){
let content="rbxassetid://255299419";
let RobloxAssetId(_asset_id)=content.parse().unwrap();
}

@ -1,7 +1,7 @@
[package]
name = "roblox_emulator"
version = "0.4.7"
edition = "2021"
version = "0.5.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Run embedded Luau scripts which manipulate the DOM."
@ -12,10 +12,10 @@ default=["run-service"]
run-service=[]
[dependencies]
glam = "0.29.0"
glam = "0.30.0"
mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.11.2", features = ["macros"] }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection = { version = "4.7.0", registry = "strafesnet" }
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_types = { version = "1.10.0", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_types = "2.0.0"

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

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

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

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

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

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

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

@ -2,48 +2,47 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref;
use rbx_dom_weak::{InstanceBuilder,WeakDom};
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use crate::runner::vector3::Vector3;
use crate::runner::number::Number;
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store
lua.set_app_data(ClassMethodsStore::default());
lua.set_app_data(InstanceValueStore::default());
let instance_table=lua.create_table()?;
let table=lua.create_table()?;
//Instance.new
instance_table.raw_set("new",
table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
let class_name_str=&*class_name.to_str()?;
let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{
//TODO: Nil instances
Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
})
})?
)?;
globals.set("Instance",instance_table)?;
globals.set("Instance",table)?;
Ok(())
}
// LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{
match value{
&mlua::Value::Integer(i)=>Some(i as f32),
&mlua::Value::Number(f)=>Some(f as f32),
_=>None,
}
pub fn class_is_a(class:&str,superclass:&str)->bool{
let db=rbx_reflection_database::get();
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
return false;
};
db.has_superclass(class,superclass)
}
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
let mut full_name=instance.name.clone();
let mut pref=instance.parent();
@ -58,7 +57,7 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->S
pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
dom_mut(lua,|dom|{
let instance=script.get(dom)?;
let source=match instance.properties.get("Source"){
let source=match instance.properties.get(&static_ustr("Source")){
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
_=>Err(mlua::Error::external("Missing script.Source"))?,
};
@ -80,14 +79,32 @@ pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance
dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
}
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
dom.descendants_of(instance.referent()).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
#[derive(Clone,Copy)]
pub struct Instance{
referent:Ref,
}
impl Instance{
pub const fn new(referent:Ref)->Self{
pub const fn new_unchecked(referent:Ref)->Self{
Self{referent}
}
pub fn new(referent:Ref)->Option<Self>{
referent.is_some().then_some(Self{referent})
}
pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{
dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
}
@ -97,40 +114,25 @@ impl Instance{
}
type_from_lua_userdata!(Instance);
//TODO: update rbx_reflection and use dom.superclasses_iter
pub struct SuperClassIter<'a> {
database: &'a rbx_reflection::ReflectionDatabase<'a>,
descriptor: Option<&'a rbx_reflection::ClassDescriptor<'a>>,
}
impl<'a> SuperClassIter<'a> {
fn next_descriptor(&self) -> Option<&'a rbx_reflection::ClassDescriptor<'a>> {
let superclass = self.descriptor?.superclass.as_ref()?;
self.database.classes.get(superclass)
}
}
impl<'a> Iterator for SuperClassIter<'a> {
type Item = &'a rbx_reflection::ClassDescriptor<'a>;
fn next(&mut self) -> Option<Self::Item> {
let next_descriptor = self.next_descriptor();
std::mem::replace(&mut self.descriptor, next_descriptor)
}
}
impl mlua::UserData for Instance{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Parent",|lua,this|{
fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(Instance::new(instance.parent()))
})
});
fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{
let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
}
fields.add_field_method_get("parent",get_parent);
fields.add_field_method_get("Parent",get_parent);
fn set_parent(lua:&mlua::Lua,this:&mut Instance,new_parent:Option<Instance>)->mlua::Result<()>{
let parent_ref=new_parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{
dom.transfer_within(this.referent,parent.referent);
dom.transfer_within(this.referent,parent_ref);
Ok(())
})
});
}
fields.add_field_method_set("parent",set_parent);
fields.add_field_method_set("Parent",set_parent);
fields.add_field_method_get("Name",|lua,this|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
@ -148,22 +150,38 @@ impl mlua::UserData for Instance{
fields.add_field_method_get("ClassName",|lua,this|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(instance.class.clone())
Ok(instance.class.to_owned())
})
});
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("GetChildren",|lua,this,_:()|
fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{
dom_mut(lua,|dom|{
let instance_ref=dom.clone_within(this.referent);
Ok(Instance::new_unchecked(instance_ref))
})
}
methods.add_method("clone",clone);
methods.add_method("Clone",clone);
fn get_children(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Vec<Instance>>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
let children:Vec<_>=instance
.children()
.iter()
.copied()
.map(Instance::new)
.map(Instance::new_unchecked)
.collect();
Ok(children)
})
}
methods.add_method("children",get_children);
methods.add_method("GetChildren",get_children);
methods.add_method("GetFullName",|lua,this,()|
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(get_full_name(dom,instance))
})
);
fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
let name_str=&*name.to_str()?;
@ -175,7 +193,7 @@ impl mlua::UserData for Instance{
false=>find_first_child(dom,instance,name_str),
}
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
)
})
@ -185,13 +203,29 @@ impl mlua::UserData for Instance{
methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok(
match search_descendants.unwrap_or(false){
true=>find_first_descendant_of_class(dom,this.get(dom)?,class_str),
false=>find_first_child_of_class(dom,this.get(dom)?,class_str),
true=>find_first_descendant_of_class(dom,inst,class_str),
false=>find_first_child_of_class(dom,inst,class_str),
}
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
)
})
});
methods.add_method("FindFirstChildWhichIsA",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok(
match search_descendants.unwrap_or(false){
true=>find_first_descendant_which_is_a(dom,inst,class_str),
false=>find_first_child_which_is_a(dom,inst,class_str),
}
.map(|instance|
Instance::new_unchecked(instance.referent())
)
)
})
@ -201,24 +235,42 @@ impl mlua::UserData for Instance{
let children:Vec<_>=dom
.descendants_of(this.referent)
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
.collect();
Ok(children)
})
);
methods.add_method("IsA",|lua,this,classname:mlua::String|
methods.add_method("IsAncestorOf",|lua,this,descendant:Instance|
dom_mut(lua,|dom|{
let instance=descendant.get(dom)?;
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==this.referent))
})
);
methods.add_method("IsDescendantOf",|lua,this,ancestor:Instance|
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
})
);
methods.add_method("Destroy",|lua,this,()|
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
dom_mut(lua,|dom|{
dom.destroy(this.referent);
let instance=this.get(dom)?;
Ok(class_is_a(instance.class.as_str(),&*classname.to_str()?))
})
}
methods.add_method("isA",is_a);
methods.add_method("IsA",is_a);
fn destroy(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<()>{
dom_mut(lua,|dom|{
dom.transfer_within(this.referent,Ref::none());
Ok(())
})
);
}
methods.add_method("remove",destroy);
methods.add_method("Remove",destroy);
methods.add_method("destroy",destroy);
methods.add_method("Destroy",destroy);
methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
@ -232,39 +284,38 @@ impl mlua::UserData for Instance{
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
//Find existing property
match instance.properties.get(index_str)
.cloned()
// Find existing property
// Interestingly, ustr can know ahead of time if
// a property does not exist in any runtime instance
match Ustr::from_existing(index_str)
.and_then(|index_ustr|
instance.properties.get(&index_ustr).cloned()
)
//Find default value
.or_else(||db.find_default_property(class,index_str).cloned())
//Find virtual property
.or_else(||{
SuperClassIter{
database:db,
descriptor:Some(class),
}
.find_map(|class|
find_virtual_property(&instance.properties,class,index_str)
)
})
.or_else(||db.superclasses_iter(class).find_map(|class|
find_virtual_property(&instance.properties,class,index_str)
))
{
Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Ref(val))=>return Instance::new(val).into_lua(lua),
Some(rbx_types::Variant::CFrame(cf))=>return Into::<crate::runner::cframe::CFrame>::into(cf).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).into_lua(lua),
Some(rbx_types::Variant::String(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).into_lua(lua),
Some(rbx_types::Variant::Color3(c))=>return crate::runner::color3::Color3::from(c).into_lua(lua),
Some(rbx_types::Variant::CFrame(cf))=>return crate::runner::cframe::CFrame::from(cf).into_lua(lua),
Some(rbx_types::Variant::Vector2(v))=>return crate::runner::vector2::Vector2::from(v).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return crate::runner::vector3::Vector3::from(v).into_lua(lua),
None=>(),
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
}
//find a function with a matching name
if let Some(function)=class_methods_store_mut(lua,|cf|{
let mut iter=SuperClassIter{
database:db,
descriptor:Some(class),
};
iter.find_map(|class|{
db.superclasses_iter(class).find_map(|class|{
let mut class_methods=cf.get_or_create_class_methods(&class.name)?;
class_methods.get_or_create_function(lua,index_str)
.transpose()
@ -274,80 +325,114 @@ impl mlua::UserData for Instance{
}
//find or create an associated userdata object
if let Some(value)=instance_value_store_mut(lua,|ivs|{
//TODO: walk class tree somehow
match ivs.get_or_create_instance_values(&instance){
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
None=>Ok(None)
}
})?{
let instance=this.get_mut(dom)?;
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
return value.into_lua(lua);
}
// drop mutable borrow
//find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str)
.map(|instance|Instance::new(instance.referent()))
.map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua)
})
});
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?;
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
let index_str=&*index.to_str()?;
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let mut iter=SuperClassIter{
database:db,
descriptor:Some(class),
};
let property=iter.find_map(|cls|cls.properties.get(index_str)).ok_or_else(||mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)))?;
match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Vector3(typed_value.into()));
},
let property=db.superclasses_iter(class).find_map(|cls|
cls.properties.get(index_str)
).ok_or_else(||
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
)?;
let value=match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Float32(typed_value));
let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
rbx_types::Variant::Float32(typed_value)
},
rbx_reflection::DataType::Enum(enum_name)=>{
let typed_value=match &value{
&mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)),
&mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)),
mlua::Value::String(s)=>{
let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?))
},
mlua::Value::UserData(any_user_data)=>{
let e:crate::runner::r#enum::Enum=*any_user_data.borrow()?;
let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?;
Ok(e.into())
},
_=>Err(mlua::Error::runtime("Expected Enum")),
}?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Enum(typed_value));
rbx_types::Variant::Enum(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{
let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Color3(typed_value.into()));
rbx_types::Variant::Color3(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Bool(typed_value));
rbx_types::Variant::Bool(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
rbx_types::Variant::Int32(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::String(typed_value.to_owned()));
let typed_value=match &value{
mlua::Value::Integer(i)=>i.to_string(),
mlua::Value::Number(n)=>n.to_string(),
mlua::Value::String(s)=>s.to_str()?.to_owned(),
_=>return Err(mlua::Error::runtime("Expected string")),
};
rbx_types::Variant::String(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::UDim2)=>{
let typed_value:&crate::runner::udim2::UDim2=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected UDim2"))?.borrow()?;
rbx_types::Variant::UDim2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberRange)=>{
let typed_value:&crate::runner::number_range::NumberRange=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberRange"))?.borrow()?;
rbx_types::Variant::NumberRange(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{
let typed_value:crate::runner::number_sequence::NumberSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::NumberSequence(typed_value.into()));
let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
rbx_types::Variant::NumberSequence(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{
let typed_value:crate::runner::color_sequence::ColorSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::ColorSequence(typed_value.into()));
let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
rbx_types::Variant::ColorSequence(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector2)=>{
let typed_value:crate::runner::vector2::Vector2=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector2"))?.borrow()?;
rbx_types::Variant::Vector2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:crate::runner::vector3::Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector3"))?.borrow()?;
rbx_types::Variant::Vector3(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::CFrame)=>{
let typed_value:crate::runner::cframe::CFrame=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected CFrame"))?.borrow()?;
rbx_types::Variant::CFrame(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
rbx_types::Variant::ContentId(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
// why clone?
let typed_value=Option::<Instance>::from_lua(value.clone(),lua)?;
rbx_types::Variant::Ref(typed_value.map_or(Ref::none(),|instance|instance.referent))
},
other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))),
}
};
// the index is known to be a real property at this point
// allow creating a permanent ustr (memory leak)
let index_ustr=rbx_dom_weak::ustr(index_str);
instance.properties.insert(index_ustr,value);
Ok(())
})
});
@ -372,28 +457,53 @@ type CFD=phf::Map<&'static str,// Class name
ClassFunctionPointer
>
>;
const GET_SERVICE:ClassFunctionPointer=cf!(|lua,_this,service:mlua::String|{
dom_mut(lua,|dom|{
//dom.root_ref()==this.referent ?
let service=&*service.to_str()?;
match service{
"Lighting"|"RunService"|"Players"|"Workspace"|"MaterialService"|"TweenService"=>{
let referent=find_first_child_of_class(dom,dom.root(),service)
.map(|instance|instance.referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(Instance::new_unchecked(referent))
},
other=>Err(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
});
const GET_PLAYERS:ClassFunctionPointer=cf!(|_lua,_this,()|->mlua::Result<_>{
Ok(Vec::<Instance>::new())
});
const NO_OP:ClassFunctionPointer=cf!(|_lua,_this,_:mlua::MultiValue|->mlua::Result<_>{Ok(())});
static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
"DataModel"=>phf::phf_map!{
"GetService"=>cf!(|lua,_this,service:mlua::String|{
dom_mut(lua,|dom|{
//dom.root_ref()==this.referent ?
let service=&*service.to_str()?;
match service{
"Lighting"|"RunService"=>{
let referent=find_first_child_of_class(dom,dom.root(),service)
.map(|instance|instance.referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(Instance::new(referent))
},
other=>Err::<Instance,_>(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
}),
"service"=>GET_SERVICE,
"GetService"=>GET_SERVICE,
},
"Terrain"=>phf::phf_map!{
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(()))
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
},
"Players"=>phf::phf_map!{
"players"=>GET_PLAYERS,
"GetPlayers"=>GET_PLAYERS,
},
"Sound"=>phf::phf_map!{
"Play"=>NO_OP,
},
"TweenService"=>phf::phf_map!{
"Create"=>cf!(|_lua,_,(instance,tween_info,goal):(Instance,crate::runner::tween_info::TweenInfo,mlua::Table)|->mlua::Result<_>{
Ok(crate::runner::tween::Tween::create(
instance,
tween_info,
goal,
))
}),
},
};
@ -480,78 +590,70 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
};
fn find_virtual_property(
properties:&HashMap<String,rbx_types::Variant>,
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor,
index:&str
index:&str,
)->Option<rbx_types::Variant>{
//Find virtual property
let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
let virtual_property=class_virtual_properties.get(index)?;
//Get source property
let variant=properties.get(virtual_property.property)?;
let variant=properties.get(&static_ustr(virtual_property.property))?;
//Transform Source property with provided function
(virtual_property.pointer)(variant)
}
// 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 LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name
CreateUserData
>
>;
fn create_script_signal(lua:&mlua::Lua)->mlua::Result<mlua::AnyUserData>{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
}
static LAZY_USER_DATA:LUD=phf::phf_map!{
"RunService"=>phf::phf_map!{
"RenderStepped"=>|lua|{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
},
"Stepped"=>create_script_signal,
"Heartbeat"=>create_script_signal,
"RenderStepped"=>create_script_signal,
},
"Players"=>phf::phf_map!{
"PlayerAdded"=>create_script_signal,
},
"BasePart"=>phf::phf_map!{
"Touched"=>create_script_signal,
"TouchEnded"=>create_script_signal,
},
"Instance"=>phf::phf_map!{
"ChildAdded"=>create_script_signal,
"ChildRemoved"=>create_script_signal,
"DescendantAdded"=>create_script_signal,
"DescendantRemoved"=>create_script_signal,
},
"ClickDetector"=>phf::phf_map!{
"MouseClick"=>create_script_signal,
},
};
#[derive(Default)]
pub struct InstanceValueStore{
values:HashMap<Ref,
HashMap<&'static str,
mlua::AnyUserData
>
>,
}
pub struct InstanceValues<'a>{
named_values:&'static phf::Map<&'static str,CreateUserData>,
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
}
impl InstanceValueStore{
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<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()),
}
)
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
use std::collections::hash_map::Entry;
let db=rbx_reflection_database::get();
let Some(class)=db.classes.get(instance.class.as_str())else{
return Ok(None)
};
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
// find pair (class,index)
LAZY_USER_DATA.get(&superclass.name)
.and_then(|map|map.get_entry(index))
){
let index_ustr=static_ustr(static_str);
return Ok(Some(match instance.userdata.entry(index_ustr){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
}));
}
}
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)
Ok(None)
}

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

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

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

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

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