Compare commits

..

282 Commits

Author SHA1 Message Date
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
871aadb78a physics version 2025-01-21 09:43:29 -08:00
65b49d2726 bodge surfs 2025-01-21 09:42:07 -08:00
ed70b7c484 tests: fix thread limit 2025-01-21 09:00:12 -08:00
c7575f2e21 deref bool 2025-01-21 08:49:44 -08:00
c2f78eab48 CrawlResult impls 2025-01-21 08:30:11 -08:00
195014400f apply setvelocity before teleport and jump 2025-01-21 07:47:39 -08:00
ee9585a4c2 accept slice in push_solve 2025-01-21 07:20:58 -08:00
77012d6caa remove unnecessary reference 2025-01-21 07:20:34 -08:00
f9509353dd change constrain_{velocity|acceleration} function signature 2025-01-21 07:20:22 -08:00
5bce4a84cf tweak ReachWalkTargetVelocity 2025-01-21 06:05:47 -08:00
4bbccd68ed tweak version plan 2025-01-21 05:47:39 -08:00
8be3be4002 write comment about handling identical timestamps implicitly 2025-01-21 05:39:12 -08:00
76bafa4d0a fix divide by zero crashes when mouse has not moved 2025-01-21 05:34:45 -08:00
a612d1f864 quiet down physics 2025-01-21 05:18:30 -08:00
ad9ef141bf print when bot file finishes writing 2025-01-21 05:18:30 -08:00
4c11980989 headless replay test 2025-01-21 05:18:30 -08:00
091da88b5c roblox_emulator: name macro variable 2025-01-20 15:46:26 -08:00
045e540020 roblox_emulator: use match guard 2025-01-20 15:46:13 -08:00
c14f8975fc snf: bot: fix lint SegmentId 2025-01-20 10:37:21 -08:00
8e228033e0 snf: bot: version const 2025-01-20 10:37:21 -08:00
eaecbc5b73 physics versioning plan 2025-01-20 10:36:40 -08:00
91a1b3d65e read entire file 2025-01-20 09:14:01 -08:00
e1d51ff2da rename stupidly named file enums 2025-01-20 09:11:09 -08:00
c5f2395b3a tools: copy all bash args in map scripts 2025-01-20 07:57:56 -08:00
77aa83d6ac physics: explicit start_time 2025-01-20 07:51:39 -08:00
184b12a9cc tools: copy all bash args in run script 2025-01-20 05:56:26 -08:00
9ba3484e25 create replays folder + write replay using spawned thread :) 2025-01-20 05:40:11 -08:00
adcd7db4f1 common: timer: the time of a paused timer does not depend on the parent time 2025-01-20 05:25:13 -08:00
710670d78e don't use mold
It makes compilation slightly more difficult for non-experts.  It can still be enabled the same way in the system-wide config located at ~/.cargo/config.toml
2025-01-18 22:43:12 -08:00
70e1514ef9 v0.11.0 replays 2025-01-18 09:24:34 -08:00
45e9b3d0ce use existing replay timer 2025-01-18 09:24:34 -08:00
1eac734c35 don't make a new replay if you are spectating 2025-01-18 09:24:34 -08:00
25e1312fe4 document another bind 2025-01-18 09:24:34 -08:00
cc8f6b059d SessionControlInstruction::LoadIntoReplayState (J) 2025-01-18 09:24:34 -08:00
4cd287dbb8 unpause sim on bot file export 2025-01-18 09:24:34 -08:00
40ea0e6c7d rename last_instruction_id to next_instruction_id 2025-01-18 09:24:34 -08:00
b45b92c627 allow writing idle instructions 2025-01-18 09:24:34 -08:00
e90f53a111 integrate replay save/load 2025-01-18 09:24:34 -08:00
6beb6c5f9a implement bot file 2025-01-18 09:24:21 -08:00
a1507d4f26 implement newtypes for bot files 2025-01-18 09:24:05 -08:00
77db5a7a6b tweak instruction collector 2025-01-18 05:22:02 -08:00
75c8bc2bbb fix timescale 2025-01-17 21:47:02 -08:00
d49a6b2f0a file::Header::calculate_size 2025-01-17 21:29:04 -08:00
19778ac7aa update wgpu 2025-01-17 11:47:23 -08:00
5b62052222 snf: move newtypes.rs into newtypes/mod.rs 2025-01-16 21:29:46 -08:00
5a8bc141d3 tweak ModeInstruction and document meaning 2025-01-16 20:54:02 -08:00
83a067320b fix usage of Cow in model_physics 2025-01-16 19:31:49 -08:00
2faa61225f refactor physics to use shared context for multiple simulations 2025-01-16 18:51:22 -08:00
28499800cb comment about bad algorithm 2025-01-16 07:40:08 -08:00
57cc49dc1a explicitly reset and spawn instead of implicitly 2025-01-16 06:35:36 -08:00
d517b78a8c replay controls 2025-01-16 05:10:39 -08:00
e6d1d69241 delete replay on StopSpectate 2025-01-16 05:10:39 -08:00
b28fa25279 comment about determinism 2025-01-16 05:10:39 -08:00
713b235816 maintain replay state according to real time 2025-01-16 05:10:39 -08:00
d2002383cb idle is special 2025-01-16 00:17:39 -08:00
52f7de809d rename variable 2025-01-16 00:17:39 -08:00
4efe1209b8 implement copy instruction into replay 2025-01-15 23:44:42 -08:00
15a9136fc4 session instruction changes for control and playback 2025-01-15 22:59:59 -08:00
035736e7af record 2025-01-15 21:41:44 -08:00
7f9a16a56d refactor physics enums so Mouse-NonMouse distinction is private to mouse_interpolator 2025-01-15 21:09:08 -08:00
814e573d91 rename physics instructions 2025-01-15 20:19:20 -08:00
6fa0f1c83e model_physics: use entry or_insert_with pattern 2025-01-15 08:46:26 -08:00
870cb56dac do not use ResourceType 2025-01-15 08:46:15 -08:00
1aac2a9303 model_physics: remove pointless unsafe usage from the before times 2025-01-15 08:46:06 -08:00
38661b3a68 fixed_wide: another derivable trait 2025-01-15 04:15:41 -08:00
7ad76740d4 common: tidy instruction module 2025-01-15 03:29:48 -08:00
c2d6af8bda TimedInstruction::set_time 2025-01-15 03:29:48 -08:00
fbacef83b9 fix non-determinism bug 2025-01-15 03:16:00 -08:00
2a9e848541 explain non-determinism bug 2025-01-15 03:03:34 -08:00
3413ec8740 mouse_interpolator tweaks 2025-01-15 02:59:34 -08:00
168d6708d1 fix some physics lints 2025-01-15 01:34:10 -08:00
13cae4c7c5 rewrite mouse_interpolator, introduce session 2025-01-15 01:01:24 -08:00
0dc462a3b1 comment infinite loop avoidance 2025-01-09 20:38:32 -08:00
ca003edbc3 reintroduce generics to Instruction traits 2025-01-09 20:11:00 -08:00
16abe23e97 push solve tweaks 2025-01-09 06:27:50 -08:00
67f8569178 push_solve infallible type signature 2025-01-09 05:56:11 -08:00
121c9c5258 resize immediately 2025-01-09 05:36:55 -08:00
411b997b87 use mold linker because it's faster 2025-01-09 05:36:55 -08:00
4d587b6c0b fixed_wide: clippy angry about derivable traits 2025-01-08 23:33:39 -08:00
6ff74f08ab roblox_emulator: deref returns correct type 2025-01-08 23:31:56 -08:00
08f419f931 rename crawl_fev to crawl 2025-01-08 21:09:52 -08:00
6066e82fd2 MeshQuery trait FEV associated types 2025-01-08 21:09:52 -08:00
ca8035cdfc fixed wide 2025-01-08 21:09:52 -08:00
ff5d954cfb push solve! 2025-01-08 18:17:40 -08:00
a967f31004 rename RawTime to AbsoluteTime 2025-01-08 01:15:52 -08:00
8ad5d28e51 impl AsRef<str> for FlagReason 2025-01-07 23:57:22 -08:00
ab05893813 A BUG HAS BEEN FOUND!!!! 2025-01-07 23:43:54 -08:00
2f7597146e fixup strafe client for strongly typed time 2025-01-07 23:43:54 -08:00
004e0d3776 common: session Time 2025-01-07 23:43:54 -08:00
120d8197b7 common 2025-01-07 23:43:54 -08:00
36ba73a892 timers 2025-01-07 23:43:54 -08:00
86cf7e74b1 typed Time 2025-01-07 22:36:58 -08:00
24787fede5 improve get_model_transform readability 2025-01-07 20:19:44 -08:00
3797408bc8 pull out named variables in checkpoint_check 2025-01-07 06:03:29 -08:00
47c9b77b00 style 2025-01-07 06:03:29 -08:00
479e657251 notes 2025-01-07 06:03:29 -08:00
63fbc94287 snf: demo file brainstorming 2025-01-06 23:14:08 -08:00
1318ae20ca snf: session file brainstorming 2025-01-06 23:14:08 -08:00
851d9c935d clear mode state in teleport_to_spawn 2025-01-06 21:50:43 -08:00
d0a190861c implement NoJump and jump_limit 2025-01-06 21:45:48 -08:00
4dca7fc369 try_increment_jump_count monolithic function 2025-01-06 21:45:48 -08:00
62dfe23539 prevent hitting side of spawn from updating current stage 2025-01-06 21:05:37 -08:00
3991cb5064 document unclear function 2025-01-06 21:05:37 -08:00
1dc2556d85 factor out immutable checkpoint_check logic 2025-01-06 21:05:37 -08:00
4f21985290 fix print grammar 2025-01-06 00:36:14 -08:00
ccce54c1a3 calculate title at compile time 2025-01-05 21:39:48 -08:00
02bb2d797c functions not needed 2025-01-05 03:46:15 -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' (#2) from Jeftaei/map-tool:master into master
Reviewed-on: StrafesNET/map-tool#2
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' (#1) from Jeftaei/map-tool:master into master
Reviewed-on: StrafesNET/map-tool#1
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
115 changed files with 8542 additions and 2874 deletions

View File

@ -1,2 +1,2 @@
[registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"

2493
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,14 @@
[package]
name = "strafesnet_graphics"
version = "0.1.0"
edition = "2021"
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
ddsfile = "0.5.1"
glam = "0.29.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 = "24.0.0"

8
engine/graphics/LICENSE Normal file
View 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
*******************************************************/

View File

@ -1,10 +1,15 @@
use std::borrow::Cow;
use std::collections::{HashSet,HashMap};
use strafesnet_common::map;
use strafesnet_common::integer;
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,
@ -98,12 +103,6 @@ impl std::default::Default for GraphicsCamera{
}
}
pub struct FrameState{
pub body:crate::physics::Body,
pub camera:crate::physics::PhysicsCamera,
pub time:integer::Time,
}
pub struct GraphicsState{
pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups,
@ -143,7 +142,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){
@ -455,7 +454,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(){
@ -615,7 +614,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
@ -643,10 +642,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!(),
};
@ -689,7 +688,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();
@ -871,7 +870,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);
@ -882,7 +881,7 @@ impl GraphicsState{
view:&wgpu::TextureView,
device:&wgpu::Device,
queue:&wgpu::Queue,
frame_state:FrameState,
frame_state:session::FrameState,
){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input

View File

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

10
engine/physics/Cargo.toml Normal file
View File

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

8
engine/physics/LICENSE Normal file
View 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
*******************************************************/

160
engine/physics/src/body.rs Normal file
View File

@ -0,0 +1,160 @@
use strafesnet_common::aabb;
use strafesnet_common::integer::{self,vec3,Time,Planar64,Planar64Vec3};
#[derive(Clone,Copy,Debug,Hash)]
pub struct Body<T>{
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
pub time:Time<T>,//nanoseconds x xxxxD!
}
impl<T> std::ops::Neg for Body<T>{
type Output=Self;
fn neg(self)->Self::Output{
Self{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
time:-self.time,
}
}
}
impl<T> Body<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
velocity,
acceleration,
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())
}
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())
}
pub fn advance_time(&mut self,time:Time<T>){
self.position=self.extrapolated_position(time);
self.velocity=self.extrapolated_velocity(time);
self.time=time;
}
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
// Why?
// All of this can be removed with const generics because the type can be specified as
// Ratio<Fixed<N,NF>,Fixed<D,DF>>
// which is known to implement all the necessary traits
Num:Copy,
Den:Copy+core::ops::Mul<i64,Output=D1>,
D1:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<D1,Output=N2>,
N1:core::ops::Add<N2,Output=N3>,
Num:core::ops::Mul<N3,Output=N4>,
Den:core::ops::Mul<D1,Output=D2>,
D2:Copy,
Planar64:core::ops::Mul<D2,Output=N4>,
N4:integer::Divide<D2,Output=T1>,
T1:integer::Fix<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
}
pub fn extrapolated_velocity_ratio_dt<Num,Den,N1,T1>(&self,dt:integer::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>,
{
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
}
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();
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
None
}else{
Some(self.acceleration)
}
}else{
Some(self.velocity)
}
}
pub fn grow_aabb(&self,aabb:&mut aabb::Aabb,t0:Time<T>,t1:Time<T>){
aabb.grow(self.extrapolated_position(t0));
aabb.grow(self.extrapolated_position(t1));
//v+a*t==0
//goober code
if !self.acceleration.x.is_zero(){
let t=-self.velocity.x/self.acceleration.x;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.y.is_zero(){
let t=-self.velocity.y/self.acceleration.y;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
if !self.acceleration.z.is_zero(){
let t=-self.velocity.z/self.acceleration.z;
if t0.to_ratio().lt_ratio(t)&&t.lt_ratio(t1.to_ratio()){
aabb.grow(self.extrapolated_position_ratio_dt(t));
}
}
}
}
impl<T> std::fmt::Display for Body<T>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
}
}
pub struct VirtualBody<'a,T>{
body0:&'a Body<T>,
body1:&'a Body<T>,
}
impl<T> VirtualBody<'_,T>
where Time<T>:Copy,
{
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
}
pub fn acceleration(&self)->Planar64Vec3{
self.body1.acceleration-self.body0.acceleration
}
pub fn body(&self,time:Time<T>)->Body<T>{
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
}
}

View File

@ -1,23 +1,48 @@
use crate::physics::Body;
use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge,MinkowskiMesh,MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert};
use strafesnet_common::integer::{Time,Fixed,Ratio};
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
#[derive(Debug)]
enum Transition<F,E:DirectedEdge,V>{
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<F,E,V>,GigaTime),
Hit(F,GigaTime),
Next(FEV<M>,GigaTime),
Hit(M::Face,GigaTime),
}
type MinkowskiFEV=FEV<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
pub enum CrawlResult<M:MeshQuery>{
Miss(FEV<M>),
Hit(M::Face,GigaTime),
}
impl<M:MeshQuery> CrawlResult<M>{
pub fn hit(self)->Option<(M::Face,GigaTime)>{
match self{
CrawlResult::Miss(_)=>None,
CrawlResult::Hit(face,time)=>Some((face,time)),
}
}
pub fn miss(self)->Option<FEV<M>>{
match self{
CrawlResult::Miss(fev)=>Some(fev),
CrawlResult::Hit(_,_)=>None,
}
}
}
fn next_transition(fev:&MinkowskiFEV,body_time:GigaTime,mesh:&MinkowskiMesh,body:&Body,mut best_time:GigaTime)->MinkowskiTransition{
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=MinkowskiTransition::Miss;
match fev{
&MinkowskiFEV::Face(face_id)=>{
let mut best_transition=Transition::Miss;
match self{
&FEV::Face(face_id)=>{
//test own face collision time, ignoring roots with zero or conflicting derivative
//n=face.normal d=face.dot
//n.a t^2+n.v t+n.p-d==0
@ -27,7 +52,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Hit(face_id,dt);
best_transition=Transition::Hit(face_id,dt);
break;
}
}
@ -41,14 +66,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(verts[0])+mesh.vert(verts[1]))).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
}
//if none:
},
&MinkowskiFEV::Edge(edge_id)=>{
&FEV::Edge(edge_id)=>{
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
@ -61,7 +86,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).fix_4(),n.dot(body.velocity).fix_4()*2,n.dot(body.acceleration).fix_4()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Face(edge_face_id),dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break;
}
}
@ -74,14 +99,14 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Vert(vert_id),dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
}
}
}
//if none:
},
&MinkowskiFEV::Vert(vert_id)=>{
&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(){
//edge is directed away from vertex, but we want the dot product to turn out negative
@ -90,7 +115,7 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.fix_4(),dt.den.fix_4());
best_time=dt;
best_transition=MinkowskiTransition::Next(MinkowskiFEV::Edge(directed_edge_id.as_undirected()),dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
@ -100,28 +125,24 @@ type MinkowskiTransition=Transition<MinkowskiFace,MinkowskiDirectedEdge,Minkowsk
}
best_transition
}
pub enum CrawlResult<F,E:DirectedEdge,V>{
Miss(FEV<F,E,V>),
Hit(F,GigaTime),
}
type MinkowskiCrawlResult=CrawlResult<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>;
pub fn crawl_fev(mut fev:MinkowskiFEV,mesh:&MinkowskiMesh,relative_body:&Body,start_time:Time,time_limit:Time)->MinkowskiCrawlResult{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
for _ in 0..20{
match next_transition(&fev,body_time,mesh,relative_body,time_limit){
Transition::Miss=>return CrawlResult::Miss(fev),
Transition::Next(next_fev,next_time)=>(fev,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut body_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
};
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
}
}
//TODO: fix all bugs
//println!("Too many iterations! Using default behaviour instead of crashing...");
CrawlResult::Miss(self)
}
//TODO: fix all bugs
//println!("Too many iterations! Using default behaviour instead of crashing...");
CrawlResult::Miss(fev)
}

45
engine/physics/src/lib.rs Normal file
View 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)
}
}

View File

@ -1,8 +1,12 @@
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};
use strafesnet_common::physics::Time;
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
pub trait UndirectedEdge{
type DirectedEdge:Copy+DirectedEdge;
@ -51,10 +55,10 @@ impl DirectedEdge for SubmeshDirectedEdgeId{
//Vertex <-> Edge <-> Face -> Collide
#[derive(Debug)]
pub enum FEV<F,E:DirectedEdge,V>{
Face(F),
Edge(E::UndirectedEdge),
Vert(V),
pub enum FEV<M:MeshQuery>{
Face(M::Face),
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
Vert(M::Vert),
}
//use Unit32 #[repr(C)] for map files
@ -64,25 +68,28 @@ struct Face{
dot:Planar64,
}
struct Vert(Planar64Vec3);
pub trait MeshQuery<FACE:Clone,EDGE:Clone+DirectedEdge,VERT:Clone>{
pub trait MeshQuery{
type Face:Clone;
type Edge:Clone+DirectedEdge;
type Vert:Clone;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
fn edge_n(&self,edge_id:EDGE::UndirectedEdge)->Planar64Vec3{
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())
}
fn directed_edge_n(&self,directed_edge_id:EDGE)->Planar64Vec3{
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)
}
fn vert(&self,vert_id:VERT)->Planar64Vec3;
fn face_nd(&self,face_id:FACE)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:FACE)->Cow<Vec<EDGE>>;
fn edge_faces(&self,edge_id:EDGE::UndirectedEdge)->Cow<[FACE;2]>;
fn edge_verts(&self,edge_id:EDGE::UndirectedEdge)->Cow<[VERT;2]>;
fn vert_edges(&self,vert_id:VERT)->Cow<Vec<EDGE>>;
fn vert_faces(&self,vert_id:VERT)->Cow<Vec<FACE>>;
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]>;
}
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
@ -273,15 +280,12 @@ struct EdgePool{
}
impl EdgePool{
fn push(&mut self,edge_ref_verts:EdgeRefVerts)->(&mut EdgeRefFaces,SubmeshEdgeId){
let edge_id=if let Some(&edge_id)=self.edge_id_from_guy.get(&edge_ref_verts){
edge_id
}else{
let edge_id=*self.edge_id_from_guy.entry(edge_ref_verts.clone()).or_insert_with(||{
let edge_id=SubmeshEdgeId::new(self.edge_guys.len() as u32);
self.edge_guys.push((edge_ref_verts.clone(),EdgeRefFaces::new()));
self.edge_id_from_guy.insert(edge_ref_verts,edge_id);
self.edge_guys.push((edge_ref_verts,EdgeRefFaces::new()));
edge_id
};
(&mut unsafe{self.edge_guys.get_unchecked_mut(edge_id.get() as usize)}.1,edge_id)
});
(&mut self.edge_guys[edge_id.get() as usize].1,edge_id)
}
}
@ -316,16 +320,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
let mut submesh_verts=Vec::new();
let mut submesh_vert_id_from_mesh_vert_id=HashMap::<MeshVertId,SubmeshVertId>::new();
//lazy closure
let mut get_submesh_vert_id=|vert_id:MeshVertId|{
if let Some(&submesh_vert_id)=submesh_vert_id_from_mesh_vert_id.get(&vert_id){
submesh_vert_id
}else{
let submesh_vert_id=SubmeshVertId::new(submesh_verts.len() as u32);
submesh_verts.push(vert_id);
submesh_vert_id_from_mesh_vert_id.insert(vert_id,submesh_vert_id);
submesh_vert_id
}
};
let mut get_submesh_vert_id=|vert_id:MeshVertId|*submesh_vert_id_from_mesh_vert_id.entry(vert_id).or_insert_with(||{
let submesh_vert_id=SubmeshVertId::new(submesh_verts.len() as u32);
submesh_verts.push(vert_id);
submesh_vert_id
});
let mut edge_pool=EdgePool::default();
let mut vert_ref_guys=vec![VertRefGuy::default();mesh.unique_pos.len()];
let mut face_ref_guys=Vec::new();
@ -358,10 +357,10 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
edge_ref_faces.push(!is_sorted as usize,submesh_face_id);
//index edges & face into vertices
{
let vert_ref_guy=unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert0_id.get() as usize)};
let vert_ref_guy=&mut vert_ref_guys[submesh_vert0_id.get() as usize];
vert_ref_guy.edges.insert(edge_id.as_directed(is_sorted));
vert_ref_guy.faces.insert(submesh_face_id);
unsafe{vert_ref_guys.get_unchecked_mut(submesh_vert1_id.get() as usize)}.edges.insert(edge_id.as_directed(!is_sorted));
vert_ref_guys[submesh_vert1_id.get() as usize].edges.insert(edge_id.as_directed(!is_sorted));
}
//return directed_edge_id
edge_id.as_directed(is_sorted)
@ -421,7 +420,10 @@ pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
}
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMeshView<'_>{
impl MeshQuery for PhysicsMeshView<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Normal=Planar64Vec3;
type Offset=Planar64;
fn face_nd(&self,face_id:SubmeshFaceId)->(Planar64Vec3,Planar64){
@ -433,7 +435,7 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMes
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<Vec<SubmeshDirectedEdgeId>>{
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.face_topology[face_id.get() as usize].edges)
}
fn edge_faces(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshFaceId;2]>{
@ -442,10 +444,10 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for PhysicsMes
fn edge_verts(&self,edge_id:SubmeshEdgeId)->Cow<[SubmeshVertId;2]>{
Cow::Borrowed(&self.topology.edge_topology[edge_id.get() as usize].verts)
}
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].edges)
}
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
Cow::Borrowed(&self.topology.vert_topology[vert_id.get() as usize].faces)
}
}
@ -495,7 +497,10 @@ impl TransformedMesh<'_>{
)
}
}
impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for TransformedMesh<'_>{
impl MeshQuery for TransformedMesh<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
@ -508,7 +513,7 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for Transforme
self.transform.vertex.transform_point3(self.view.vert(vert_id)).fix_1()
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn face_edges(&self,face_id:SubmeshFaceId)->Cow<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
}
#[inline]
@ -520,11 +525,11 @@ impl MeshQuery<SubmeshFaceId,SubmeshDirectedEdgeId,SubmeshVertId> for Transforme
self.view.edge_verts(edge_id)
}
#[inline]
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshDirectedEdgeId>>{
fn vert_edges(&self,vert_id:SubmeshVertId)->Cow<[SubmeshDirectedEdgeId]>{
self.view.vert_edges(vert_id)
}
#[inline]
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<Vec<SubmeshFaceId>>{
fn vert_faces(&self,vert_id:SubmeshVertId)->Cow<[SubmeshFaceId]>{
self.view.vert_faces(vert_id)
}
}
@ -669,13 +674,13 @@ impl MinkowskiMesh<'_>{
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>{
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Vert(vert_id),
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if the boundary is not crossable and we are on the wrong side
let edge_n=self.edge_n(edge_id);
@ -693,55 +698,61 @@ impl MinkowskiMesh<'_>{
//infinity_dir can always be treated as a velocity
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
//both faces cannot pass this condition, return early if one does.
return FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Face(face_id);
return FEV::Face(face_id);
}
}
FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>::Edge(edge_id)
FEV::Edge(edge_id)
},
}
}
fn closest_fev_not_inside(&self,mut infinity_body:crate::physics::Body)->Option<FEV::<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert>>{
infinity_body.infinity_dir().map_or(None,|dir|{
// TODO: fundamentally improve this algorithm.
// All it needs to do is find the closest point on the mesh
// and return the FEV which the point resides on.
//
// What it actually does is use the above functions to trace a ray in from infinity,
// crawling the closest point along the mesh surface until the ray reaches
// the starting point to discover the final FEV.
//
// The actual collision prediction probably does a single test
// and then immediately returns with 0 FEV transitions on average,
// because of the strict time_limit constraint.
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values?
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
}
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&crate::physics::Body,time_limit:integer::Time)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone()).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
match crate::face_crawler::crawl_fev(fev,self,relative_body,relative_body.time,time_limit){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
fev.crawl(self,relative_body,start_time,time_limit).hit()
})
}
pub fn predict_collision_out(&self,relative_body:&crate::physics::Body,time_limit:integer::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=crate::physics::Body::new(
relative_body.extrapolated_position(time_limit),
-relative_body.extrapolated_velocity(time_limit),
relative_body.acceleration,
-time_limit,
);
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
let infinity_body=-relative_body.clone();
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
//continue backwards along the body parabola
match crate::face_crawler::crawl_fev(fev,self,&-relative_body.clone(),-time_limit,-relative_body.time){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step
}
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:&crate::physics::Body,time_limit:integer::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={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4())
@ -757,7 +768,7 @@ impl MinkowskiMesh<'_>{
//WARNING! d outside of *2
//WARNING: truncated precision
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
if Ratio::new(Planar64::ZERO,Planar64::EPSILON).le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
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);
break;
@ -766,15 +777,12 @@ impl MinkowskiMesh<'_>{
}
best_edge.map(|e|(e.as_undirected(),best_time))
}
fn infinity_in(&self,infinity_body:crate::physics::Body)->Option<(MinkowskiFace,GigaTime)>{
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
match crate::face_crawler::crawl_fev(infinity_fev,self,&infinity_body,integer::Time::MIN/4,infinity_body.time){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=crate::physics::Body::new(point,vec3::Y,vec3::ZERO,integer::Time::ZERO);
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
@ -784,9 +792,13 @@ impl MinkowskiMesh<'_>{
)
}
}
impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiMesh<'_>{
impl MeshQuery for MinkowskiMesh<'_>{
type Face=MinkowskiFace;
type Edge=MinkowskiDirectedEdge;
type Vert=MinkowskiVert;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
// TODO: relative d
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
@ -816,7 +828,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
},
}
}
fn face_edges(&self,face_id:MinkowskiFace)->Cow<Vec<MinkowskiDirectedEdge>>{
fn face_edges(&self,face_id:MinkowskiFace)->Cow<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
Cow::Owned(self.mesh1.face_edges(f1).iter().map(|&edge_id1|{
@ -917,7 +929,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
},
}
}
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<Vec<MinkowskiDirectedEdge>>{
fn vert_edges(&self,vert_id:MinkowskiVert)->Cow<[MinkowskiDirectedEdge]>{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
@ -959,7 +971,7 @@ impl MeshQuery<MinkowskiFace,MinkowskiDirectedEdge,MinkowskiVert> for MinkowskiM
},
}
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<Vec<MinkowskiFace>>{
fn vert_faces(&self,_vert_id:MinkowskiVert)->Cow<[MinkowskiFace]>{
unimplemented!()
}
}
@ -973,7 +985,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
for k in 0..len{
if k!=i&&k!=j{
let d=n.dot(normals[k]).is_negative();
if let Some(comp)=&d_comp{
if let &Some(comp)=&d_comp{
// This is testing if d_comp*d < 0
if comp^d{
return true;
@ -993,9 +1005,3 @@ 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);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,349 @@
use strafesnet_common::integer::{self,vec3::{self,Vector3},Fixed,Planar64,Planar64Vec3,Ratio};
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
// EPSILON=1/2^10
// A stack-allocated variable-size list that holds up to 4 elements
// Direct references are used instead of indices i0, i1, i2, i3
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,
pub velocity:Planar64Vec3,
pub normal:Planar64Vec3,
}
impl Contact{
fn relative_to(&self,point:Planar64Vec3)->Self{
Self{
position:self.position-point,
velocity:self.velocity,
normal:self.normal,
}
}
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
(direction-self.velocity).dot(self.normal)
}
/// Calculate the time of intersection. (previously get_touch_time)
fn solve(&self,ray:&Ray)->Ratio<Fixed<2,64>,Fixed<2,64>>{
(self.position-ray.origin).dot(self.normal)/(ray.direction-self.velocity).dot(self.normal)
}
}
//note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
let det=c0.normal.dot(c0.velocity);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det)
}
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
let u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
let d1=c1.normal.dot(c1.position);
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
}
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs()<EPSILON{
return None;
}
let d0=c0.normal.dot(c0.position);
let d1=c1.normal.dot(c1.position);
let d2=c2.normal.dot(c2.position);
Some((c1.normal.cross(c2.normal)*d0+c2.normal.cross(c0.normal)*d1+c0.normal.cross(c1.normal)*d2)/det)
}
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fixed<2,64>>;1]>{
let det=u0.dot(u0);
if det==Fixed::ZERO{
return None;
}
let s0=u0.dot(point)/det;
Some([s0])
}
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<Fixed<4,128>,Fixed<4,128>>;2]>{
let u0_u1=u0.cross(u1);
let det=u0_u1.dot(u0_u1);
if det==Fixed::ZERO{
return None;
}
let s0=u0_u1.dot(point.cross(u1))/det;
let s1=u0_u1.dot(u0.cross(point))/det;
Some([s0,s1])
}
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<Fixed<3,96>,Fixed<3,96>>;3]>{
let det=u0.cross(u1).dot(u2);
if det==Fixed::ZERO{
return None;
}
let s0=point.cross(u1).dot(u2)/det;
let s1=u0.cross(point).dot(u2)/det;
let s2=u0.cross(u1).dot(point)/det;
Some([s0,s1,s2])
}
fn is_space_enclosed_2(
a:Planar64Vec3,
b:Planar64Vec3,
)->bool{
a.cross(b)==Vector3::new([Fixed::ZERO;3])
&&a.dot(b).is_negative()
}
fn is_space_enclosed_3(
a:Planar64Vec3,
b:Planar64Vec3,
c:Planar64Vec3
)->bool{
a.cross(b).dot(c)==Fixed::ZERO
&&{
let det_abac=a.cross(b).dot(a.cross(c));
let det_abbc=a.cross(b).dot(b.cross(c));
let det_acbc=a.cross(c).dot(b.cross(c));
return!( det_abac*det_abbc).is_positive()
&&!( det_abbc*det_acbc).is_positive()
&&!(-det_acbc*det_abac).is_positive()
||is_space_enclosed_2(a,b)
||is_space_enclosed_2(a,c)
||is_space_enclosed_2(b,c)
}
}
fn is_space_enclosed_4(
a:Planar64Vec3,
b:Planar64Vec3,
c:Planar64Vec3,
d:Planar64Vec3,
)->bool{
let det_abc=a.cross(b).dot(c);
let det_abd=a.cross(b).dot(d);
let det_acd=a.cross(c).dot(d);
let det_bcd=b.cross(c).dot(d);
return( det_abc*det_abd).is_negative()
&&(-det_abc*det_acd).is_negative()
&&( det_abd*det_acd).is_negative()
&&( det_abc*det_bcd).is_negative()
&&(-det_abd*det_bcd).is_negative()
&&( det_acd*det_bcd).is_negative()
||is_space_enclosed_3(a,b,c)
||is_space_enclosed_3(a,b,d)
||is_space_enclosed_3(a,c,d)
||is_space_enclosed_3(b,c,d)
}
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();
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();
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 [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve2(
&c0.relative_to(point),
&c1.relative_to(point),
)?.divide().fix_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 [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;
}
let origin=point+solve3(
&c0.relative_to(point),
&c1.relative_to(point),
&c2.relative_to(point),
)?.divide().fix_1();
Some(Ray{origin,direction})
}
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const())
}
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
get_push_ray_1(point,c0)
.map(|ray|(ray,Conts::from_iter([c0])))
}
fn get_best_push_ray_and_conts_2<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_2(c0.normal,c1.normal){
return None;
}
if let Some(ray)=get_push_ray_2(point,c0,c1){
return Some((ray,Conts::from_iter([c0,c1])));
}
if let Some(ray)=get_push_ray_1(point,c0){
if !c1.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0])));
}
}
return None;
}
fn get_best_push_ray_and_conts_3<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_3(c0.normal,c1.normal,c2.normal){
return None;
}
if let Some(ray)=get_push_ray_3(point,c0,c1,c2){
return Some((ray,Conts::from_iter([c0,c1,c2])));
}
if let Some(ray)=get_push_ray_2(point,c0,c1){
if !c2.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0,c1])));
}
}
if let Some(ray)=get_push_ray_2(point,c0,c2){
if !c1.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0,c2])));
}
}
if let Some(ray)=get_push_ray_1(point,c0){
if !c1.relative_dot(ray.direction).is_negative()
&&!c2.relative_dot(ray.direction).is_negative(){
return Some((ray,Conts::from_iter([c0])));
}
}
return None;
}
fn get_best_push_ray_and_conts_4<'a>(point:Planar64Vec3,c0:&'a Contact,c1:&'a Contact,c2:&'a Contact,c3:&'a Contact)->Option<(Ray,Conts<'a>)>{
if is_space_enclosed_4(c0.normal,c1.normal,c2.normal,c3.normal){
return None;
}
let (ray012,conts012)=get_best_push_ray_and_conts_3(point,c0,c1,c2)?;
let (ray013,conts013)=get_best_push_ray_and_conts_3(point,c0,c1,c3)?;
let (ray023,conts023)=get_best_push_ray_and_conts_3(point,c0,c2,c3)?;
let err012=c3.relative_dot(ray012.direction);
let err013=c2.relative_dot(ray013.direction);
let err023=c1.relative_dot(ray023.direction);
let best_err=err012.max(err013).max(err023);
if best_err==err012{
return Some((ray012,conts012))
}else if best_err==err013{
return Some((ray013,conts013))
}else if best_err==err023{
return Some((ray023,conts023))
}
unreachable!()
}
fn get_best_push_ray_and_conts<'a>(
point:Planar64Vec3,
conts:&[&'a Contact],
)->Option<(Ray,Conts<'a>)>{
match conts{
&[c0,c1,c2,c3]=>get_best_push_ray_and_conts_4(point,c0,c1,c2,c3),
&[c0,c1,c2]=>get_best_push_ray_and_conts_3(point,c0,c1,c2),
&[c0,c1]=>get_best_push_ray_and_conts_2(point,c0,c1),
&[c0]=>get_best_push_ray_and_conts_1(point,c0),
&[]=>Some(get_best_push_ray_and_conts_0(point)),
_=>unreachable!(),
}
}
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
contacts.iter()
.filter(|&contact|
!conts.iter().any(|&c|std::ptr::eq(c,contact))
&&contact.relative_dot(ray.direction).is_negative()
)
.map(|contact|(contact.solve(ray),contact))
.min_by_key(|&(t,_)|t)
}
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
loop{
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
Some((t,cont))=>(t,cont),
None=>return ray.origin,
};
if RATIO_ZERO.le_ratio(next_t){
return ray.origin;
}
//push_front
if conts.len()==conts.capacity(){
//this is a dead case, new_conts never has more than 3 elements
conts.rotate_right(1);
conts[0]=next_cont;
}else{
conts.push(next_cont);
conts.rotate_right(1);
}
let meet_point=ray.extrapolate(next_t);
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
None=>return meet_point,
}
}
}
#[cfg(test)]
mod tests{
use super::*;
#[test]
fn test_push_solve(){
let contacts=vec![
Contact{
position:vec3::ZERO,
velocity:vec3::Y,
normal:vec3::Y,
}
];
assert_eq!(
vec3::ZERO,
push_solve(&contacts,vec3::NEG_Y)
);
}
}

12
engine/session/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "strafesnet_session"
version = "0.1.0"
edition = "2021"
[dependencies]
glam = "0.29.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
View 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
*******************************************************/

View File

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

View File

@ -0,0 +1,281 @@
use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::{
MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction,
Instruction as PhysicsInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime,
};
use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
/// To be fed into MouseInterpolator
#[derive(Clone,Debug)]
pub(crate) enum Instruction{
MoveMouse(glam::IVec2),
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
Idle,
}
#[derive(Clone,Debug)]
enum UnbufferedInstruction{
MoveMouse(glam::IVec2),
NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
enum BufferedInstruction{
Mouse(MouseInstruction),
NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
pub(crate) enum NonMouseInstruction{
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
Idle,
}
impl From<Instruction> for UnbufferedInstruction{
#[inline]
fn from(value:Instruction)->Self{
match value{
Instruction::MoveMouse(mouse_instruction)=>UnbufferedInstruction::MoveMouse(mouse_instruction),
Instruction::SetControl(set_control_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::SetControl(set_control_instruction)),
Instruction::Mode(mode_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Mode(mode_instruction)),
Instruction::Misc(misc_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Misc(misc_instruction)),
Instruction::Idle=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Idle),
}
}
}
impl From<BufferedInstruction> for PhysicsInstruction{
#[inline]
fn from(value:BufferedInstruction)->Self{
match value{
BufferedInstruction::Mouse(mouse_instruction)=>PhysicsInstruction::Mouse(mouse_instruction),
BufferedInstruction::NonMouse(non_mouse_instruction)=>match non_mouse_instruction{
NonMouseInstruction::SetControl(set_control_instruction)=>PhysicsInstruction::SetControl(set_control_instruction),
NonMouseInstruction::Mode(mode_instruction)=>PhysicsInstruction::Mode(mode_instruction),
NonMouseInstruction::Misc(misc_instruction)=>PhysicsInstruction::Misc(misc_instruction),
NonMouseInstruction::Idle=>PhysicsInstruction::Idle,
},
}
}
}
pub(crate) enum StepInstruction{
Pop,
Timeout,
}
#[derive(Clone,Debug)]
enum BufferState{
Unbuffered,
Initializing(SessionTime,MouseState<PhysicsTimeInner>),
Buffered(SessionTime,MouseState<PhysicsTimeInner>),
}
pub struct MouseInterpolator{
buffer_state:BufferState,
// double timestamped timeline?
buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
output:std::collections::VecDeque<TimedPhysicsInstruction>,
}
// Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
type TimeInner=SessionTimeInner;
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>>{
self.buffered_instruction_with_timeout(time_limit)
}
}
impl MouseInterpolator{
pub fn new()->MouseInterpolator{
MouseInterpolator{
buffer_state:BufferState::Unbuffered,
buffer:std::collections::VecDeque::new(),
output:std::collections::VecDeque::new(),
}
}
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
self.buffer.push_front(TimedInstruction{
time:ins.time,
instruction:BufferedInstruction::Mouse(ins.instruction).into(),
});
// flush buffer to output
if self.output.len()==0{
// swap buffers
core::mem::swap(&mut self.buffer,&mut self.output);
}else{
// append buffer contents to output
self.output.append(&mut self.buffer);
}
}
fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
match &self.buffer_state{
BufferState::Unbuffered=>None,
BufferState::Initializing(time,_mouse_state)
|BufferState::Buffered(time,_mouse_state)=>{
let timeout=*time+MOUSE_TIMEOUT;
(timeout<time_limit).then_some(timeout)
}
}
}
fn timeout_mouse(&mut self,timeout_time:PhysicsTime){
// the state always changes to unbuffered
let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
match buffer_state{
BufferState::Unbuffered=>(),
BufferState::Initializing(_time,mouse_state)=>{
// only a single mouse move was sent in 10ms, this is very much an edge case!
self.push_mouse_and_flush_buffer(TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m1:MouseState{pos:mouse_state.pos,time:timeout_time},
m0:mouse_state,
},
});
}
BufferState::Buffered(_time,mouse_state)=>{
// duplicate the currently buffered mouse state but at a later (future, from the physics perspective) time
self.push_mouse_and_flush_buffer(TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time:timeout_time}),
});
},
}
}
fn push_unbuffered_input(&mut self,session_time:SessionTime,physics_time:PhysicsTime,ins:UnbufferedInstruction){
// new input
// if there is zero instruction buffered, it means the mouse is not moving
// case 1: unbuffered
// no mouse event is buffered
// - ins is mouse event? change to buffered
// - ins other -> write to timeline
// case 2: buffered
// a mouse event is buffered, and exists within the last 10ms
// case 3: stop
// a mouse event is buffered, but no mouse events have transpired within 10ms
// replace_with allows the enum variant to safely be replaced
// from behind a mutable reference, but a panic in the closure means that
// the entire program terminates rather than completing an unwind.
let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
match ins{
UnbufferedInstruction::MoveMouse(pos)=>{
let next_mouse_state=MouseState{pos,time:physics_time};
match buffer_state{
BufferState::Unbuffered=>{
((None,None),BufferState::Initializing(session_time,next_mouse_state))
},
BufferState::Initializing(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::ReplaceMouse{
m0:mouse_state,
m1:next_mouse_state.clone(),
},
};
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
},
BufferState::Buffered(_time,mouse_state)=>{
let ins_mouse=TimedInstruction{
time:mouse_state.time,
instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
};
((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
},
}
},
UnbufferedInstruction::NonMouse(other_instruction)=>((None,Some(TimedInstruction{
time:physics_time,
instruction:other_instruction,
})),buffer_state),
}
});
if let Some(ins)=ins_mouse{
self.push_mouse_and_flush_buffer(ins);
}
if let Some(ins)=ins_other{
let instruction=TimedInstruction{
time:ins.time,
instruction:BufferedInstruction::NonMouse(ins.instruction).into(),
};
if matches!(self.buffer_state,BufferState::Unbuffered){
self.output.push_back(instruction);
}else{
self.buffer.push_back(instruction);
}
}
}
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
match self.get_mouse_timedout_at(time_limit){
Some(timeout)=>Some(TimedInstruction{
time:timeout,
instruction:StepInstruction::Timeout,
}),
None=>(self.output.len()!=0).then_some(TimedInstruction{
// this timestamp should not matter
time:time_limit,
instruction:StepInstruction::Pop,
}),
}
}
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
match ins.instruction{
StepInstruction::Pop=>(),
StepInstruction::Timeout=>self.timeout_mouse(ins.time),
}
self.output.pop_front()
}
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn test(){
let mut interpolator=MouseInterpolator::new();
let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));
macro_rules! push{
($time:expr,$ins:expr)=>{
println!("in={:?}",$ins);
interpolator.push_unbuffered_input(
$time,
timer.time($time),
$ins,
);
while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
let ins_retimed=TimedInstruction{
time:timer.time(ins.time),
instruction:ins.instruction,
};
let out=interpolator.pop_buffered_instruction(ins_retimed);
println!("out={out:?}");
}
};
}
// test each buffer_state transition
let mut t=SessionTime::ZERO;
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(5);
push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
t+=SessionTime::from_millis(1);
}
}

View File

@ -0,0 +1,443 @@
use std::collections::HashMap;
use strafesnet_common::gameplay_modes::{ModeId,StageId};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
// session represents the non-hardware state of the client.
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
use strafesnet_common::physics::{
ModeInstruction,MiscInstruction,
Instruction as PhysicsInputInstruction,
TimeInner as PhysicsTimeInner,
Time as PhysicsTime
};
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 strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
use strafesnet_settings::settings::UserSettings;
pub enum Instruction<'a>{
Input(SessionInputInstruction),
Control(SessionControlInstruction),
Playback(SessionPlaybackInstruction),
ChangeMap(&'a strafesnet_common::map::CompleteMap),
LoadReplay(strafesnet_snf::bot::Segment),
Idle,
}
pub enum SessionInputInstruction{
Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction),
Misc(strafesnet_common::physics::MiscInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
#[derive(Clone,Debug)]
pub enum ImplicitModeInstruction{
ResetAndRestart,
ResetAndSpawn(ModeId,StageId),
}
pub enum SessionControlInstruction{
SetPaused(bool),
// copy the current session simulation recording into a replay and view it
CopyRecordingIntoReplayAndSpectate,
StopSpectate,
SaveReplay,
LoadIntoReplayState,
}
pub enum SessionPlaybackInstruction{
SkipForward,
SkipBack,
TogglePaused,
DecreaseTimescale,
IncreaseTimescale,
}
pub struct FrameState{
pub body:physics::Body,
pub camera:physics::PhysicsCamera,
pub time:PhysicsTime,
}
pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:physics::PhysicsState,
}
impl Simulation{
pub const fn new(
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
physics:physics::PhysicsState,
)->Self{
Self{
timer,
physics,
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
FrameState{
body:self.physics.camera_body(),
camera:self.physics.camera(),
time:self.timer.time(time),
}
}
}
#[derive(Default)]
pub struct Recording{
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
}
impl Recording{
pub fn new(
instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTimeInner>>,
)->Self{
Self{instructions}
}
fn clear(&mut self){
self.instructions.clear();
}
}
pub struct Replay{
next_instruction_id:usize,
recording:Recording,
simulation:Simulation,
}
impl Replay{
pub const fn new(
recording:Recording,
simulation:Simulation,
)->Self{
Self{
next_instruction_id:0,
recording,
simulation,
}
}
pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
let mut time=self.simulation.timer.time(time_limit);
loop{
if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
if ins.time<time{
PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
self.next_instruction_id+=1;
}else{
break;
}
}else{
// loop playback
self.next_instruction_id=0;
// No need to reset physics because the very first instruction is 'Reset'
let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
self.simulation.timer.set_time(time_limit,new_time);
time=new_time;
}
}
}
}
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
struct BotId(u32);
//#[derive(Clone,Copy,Hash,PartialEq,Eq)]
//struct PlayerId(u32);
enum ViewState{
Play,
//Spectate(PlayerId),
Replay(BotId),
}
pub struct Session{
directories:Directories,
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
view_state:ViewState,
//gui:GuiState
geometry_shared:physics::PhysicsData,
simulation:Simulation,
// below fields not included in lite session
recording:Recording,
//players:HashMap<PlayerId,Simulation>,
replays:HashMap<BotId,Replay>,
}
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,
view_state:ViewState::Play,
recording:Default::default(),
replays:HashMap::new(),
}
}
fn clear_recording(&mut self){
self.recording.clear();
}
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.clear();
self.geometry_shared.generate_models(map);
}
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
match &self.view_state{
ViewState::Play=>Some(self.simulation.get_frame_state(time)),
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
replay.simulation.get_frame_state(time)
),
}
}
pub fn user_settings(&self)->&UserSettings{
&self.user_settings
}
}
// mouseinterpolator consumes RawInputInstruction
// mouseinterpolator emits PhysicsInputInstruction
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
// Session emits DoStep
impl InstructionConsumer<Instruction<'_>> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::TimeInner>){
// repetitive procedure macro
macro_rules! run_mouse_interpolator_instruction{
($instruction:expr)=>{
self.mouse_interpolator.process_instruction(TimedInstruction{
time:ins.time,
instruction:TimedInstruction{
time:self.simulation.timer.time(ins.time),
instruction:$instruction,
},
});
};
}
// process any timeouts that occured since the last instruction
self.process_exhaustive(ins.time);
match ins.instruction{
// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
},
Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::SetControl(set_control_instruction));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
self.clear_recording();
let mode_id=self.simulation.physics.mode();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
self.clear_recording();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
},
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
},
Instruction::Control(SessionControlInstruction::SetPaused(paused))=>{
// don't flush the buffered instructions in the mouse interpolator
// until the mouse is confirmed to be not moving at a later time
// what if they pause for 5ms lmao
_=self.simulation.timer.set_paused(ins.time,paused);
},
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
// Bind: B
// pause simulation
_=self.simulation.timer.set_paused(ins.time,true);
// create recording
let mut recording=Recording::default();
recording.instructions.extend(self.recording.instructions.iter().cloned());
// create timer starting at first instruction (or zero if the list is empty)
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
let timer=Timer::unpaused(ins.time,new_time);
// create default physics state
let simulation=Simulation::new(timer,Default::default());
// invent a new bot id and insert the replay
let bot_id=BotId(self.replays.len() as u32);
self.replays.insert(bot_id,Replay::new(
recording,
simulation,
));
// begin spectate
self.view_state=ViewState::Replay(bot_id);
},
Instruction::Control(SessionControlInstruction::StopSpectate)=>{
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
// delete the bot, otherwise it's inaccessible and wastes CPU
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>{
self.replays.remove(&bot_id);
},
}
_=self.simulation.timer.set_paused(ins.time,false);
},
Instruction::Control(SessionControlInstruction::SaveReplay)=>{
// Bind: N
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
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_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!");
});
},
}
_=self.simulation.timer.set_paused(ins.time,false);
},
Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
// Bind: J
let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
match view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
self.simulation=replay.simulation;
},
}
// don't unpause -- use the replay timer state whether it is pasued or unpaused
},
Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
match &self.view_state{
ViewState::Play=>{
// allow simulation timescale for fun
let scale=self.simulation.timer.get_scale();
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
},
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let scale=replay.simulation.timer.get_scale();
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
},
}
},
Instruction::Playback(SessionPlaybackInstruction::DecreaseTimescale)=>{
match &self.view_state{
ViewState::Play=>{
// allow simulation timescale for fun
let scale=self.simulation.timer.get_scale();
self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
},
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let scale=replay.simulation.timer.get_scale();
replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
},
}
},
Instruction::Playback(SessionPlaybackInstruction::SkipForward)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
replay.simulation.timer.set_time(ins.time,time);
},
}
},
Instruction::Playback(SessionPlaybackInstruction::SkipBack)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
replay.simulation.timer.set_time(ins.time,time);
// resimulate the entire playback lol
replay.next_instruction_id=0;
},
}
},
Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
},
}
}
Instruction::ChangeMap(complete_map)=>{
self.clear_recording();
self.change_map(complete_map);
},
Instruction::LoadReplay(bot)=>{
// pause simulation
_=self.simulation.timer.set_paused(ins.time,true);
// create recording
let recording=Recording::new(bot.instructions);
// create timer starting at first instruction (or zero if the list is empty)
let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
let timer=Timer::unpaused(ins.time,new_time);
// create default physics state
let simulation=Simulation::new(timer,Default::default());
// invent a new bot id and insert the replay
let bot_id=BotId(self.replays.len() as u32);
self.replays.insert(bot_id,Replay::new(
recording,
simulation,
));
// begin spectate
self.view_state=ViewState::Replay(bot_id);
},
Instruction::Idle=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
// this just refreshes the replays
for replay in self.replays.values_mut(){
// TODO: filter idles from recording, inject new idles in real time
replay.advance(&self.geometry_shared,ins.time);
}
}
};
// process all emitted output instructions
self.process_exhaustive(ins.time);
}
}
impl InstructionConsumer<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::TimeInner>){
let time=self.simulation.timer.time(ins.time);
if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
//record
self.recording.instructions.push(instruction.clone());
PhysicsContext::run_input_instruction(&mut self.simulation.physics,&self.geometry_shared,instruction);
}
}
}
impl InstructionEmitter<StepInstruction> for Session{
type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.mouse_interpolator.next_instruction(time_limit)
}
}

View File

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

8
engine/settings/LICENSE Normal file
View 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
*******************************************************/

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,221 @@
use std::{io::{Cursor,Read},path::Path};
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
test_determinism().unwrap();
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}
fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{
println!("loading map file..");
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,
}
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>,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 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(())
}

View File

@ -1,6 +1,6 @@
[package]
name = "strafesnet_bsp_loader"
version = "0.2.2"
version = "0.3.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -11,6 +11,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
glam = "0.29.0"
strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
vbsp = "0.6.0"
vmdl = "0.2.0"
vpk = "0.2.0"

View File

@ -1,32 +1,52 @@
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 strafesnet_common::{map,model,integer,gameplay_attributes};
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
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,
{
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 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;
//declare all prop models to Loader
let prop_models=bsp.static_props().map(|prop|{
//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 mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
let placement=prop.as_prop_placement();
model::Model{
mesh:mesh_id,
@ -48,13 +68,11 @@ 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 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,62 +81,41 @@ 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>=
@ -176,26 +173,13 @@ pub struct PartialMap1{
modes:strafesnet_common::gameplay_modes::Modes,
}
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,
@ -214,8 +198,7 @@ pub struct PartialMap2{
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 +217,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 +241,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,
})
}

View File

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

View File

@ -1,7 +1,13 @@
mod bsp;
pub mod data;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use data::Bsp;
mod bsp;
mod mesh;
pub mod loader;
const VALVE_SCALE:f32=1.0/16.0;
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 +21,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 +61,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)
}
}

View File

@ -0,0 +1,175 @@
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<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=Cow<'a,str>;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
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,
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,'a>{
finder:BspFinder<'bsp,'vpk>,
life:core::marker::PhantomData<&'a ()>,
}
impl ModelLoader<'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'a>(
finder:BspFinder<'bsp,'vpk>,
)->ModelLoader<'bsp,'vpk,'a>{
ModelLoader{
finder,
life:core::marker::PhantomData,
}
}
}
impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError;
type Index=&'a str;
type Resource=vmdl::Model;
fn load(&mut self,index:Self::Index)->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)?;
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,'a>{
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
}
impl MeshLoader<'_,'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'load,'a>(
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
)->MeshLoader<'bsp,'vpk,'load,'a>{
MeshLoader{
finder,
deferred_loader
}
}
}
impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError;
type Index=&'a str;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh)
}
}

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "strafesnet_common"
version = "0.5.2"
version = "0.6.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -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" }
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.0", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }

View File

@ -18,13 +18,8 @@ bitflags::bitflags!{
const Use=1<<14;//Interact with object
const PrimaryAction=1<<15;//LBM/Shoot/Melee
const SecondaryAction=1<<16;//RMB/ADS/Block
const WASD=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).bits();
const WASDQE=Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown).bits();
}
}
impl Controls{
pub const fn wasd()->Self{
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight)
}
pub const fn wasdqe()->Self{
Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown)
}
}

View File

@ -1,5 +1,5 @@
use crate::model;
use crate::integer::{Time,Planar64,Planar64Vec3};
use crate::integer::{AbsoluteTime,Planar64,Planar64Vec3};
//you have this effect while in contact
#[derive(Clone,Hash,Eq,PartialEq)]
@ -31,7 +31,7 @@ pub enum Booster{
//Affine(crate::integer::Planar64Affine3),//capable of SetVelocity,DotVelocity,normal booster,bouncy part,redirect velocity, and much more
Velocity(Planar64Vec3),//straight up boost velocity adds to your current velocity
Energy{direction:Planar64Vec3,energy:Planar64},//increase energy in direction
AirTime(Time),//increase airtime, invariant across mass and gravity changes
AirTime(AbsoluteTime),//increase airtime, invariant across mass and gravity changes
Height(Planar64),//increase height, invariant across mass and gravity changes
}
impl Booster{
@ -57,13 +57,13 @@ pub enum TrajectoryChoice{
#[derive(Clone,Hash,Eq,PartialEq)]
pub enum SetTrajectory{
//Speed-type SetTrajectory
AirTime(Time),//air time (relative to gravity direction) is invariant across mass and gravity changes
AirTime(AbsoluteTime),//air time (relative to gravity direction) is invariant across mass and gravity changes
Height(Planar64),//boost height (relative to gravity direction) is invariant across mass and gravity changes
DotVelocity{direction:Planar64Vec3,dot:Planar64},//set your velocity in a specific direction without touching other directions
//Velocity-type SetTrajectory
TargetPointTime{//launch on a trajectory that will land at a target point in a set amount of time
target_point:Planar64Vec3,
time:Time,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
time:AbsoluteTime,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
},
TargetPointSpeed{//launch at a fixed speed and land at a target point
target_point:Planar64Vec3,

View File

@ -110,6 +110,7 @@ impl Stage{
pub fn into_inner(self)->(HashMap<CheckpointId,ModelId>,HashSet<ModelId>){
(self.ordered_checkpoints,self.unordered_checkpoints)
}
/// Returns true if the stage has no checkpoints.
#[inline]
pub const fn is_empty(&self)->bool{
self.is_complete(0,0)

View File

@ -1,7 +1,8 @@
const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
use crate::integer::{int,vec3::int as int3,Time,Ratio64,Planar64,Planar64Vec3};
use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
use crate::controls_bitflag::Controls;
use crate::physics::Time as PhysicsTime;
#[derive(Clone,Debug)]
pub struct StyleModifiers{
@ -48,7 +49,7 @@ pub enum JumpCalculation{
#[derive(Clone,Debug)]
pub enum JumpImpulse{
Time(Time),//jump time is invariant across mass and gravity changes
Time(AbsoluteTime),//jump time is invariant across mass and gravity changes
Height(Planar64),//jump height is invariant across mass and gravity changes
Linear(Planar64),//jump velocity is invariant across mass and gravity changes
Energy(Planar64),// :)
@ -199,8 +200,8 @@ impl ControlsActivation{
}
pub const fn full_3d()->Self{
Self{
controls_mask:Controls::wasdqe(),
controls_intersects:Controls::wasdqe(),
controls_mask:Controls::WASDQE,
controls_intersects:Controls::WASDQE,
controls_contains:Controls::empty(),
}
}
@ -208,8 +209,8 @@ impl ControlsActivation{
//Normal
pub const fn full_2d()->Self{
Self{
controls_mask:Controls::wasd(),
controls_intersects:Controls::wasd(),
controls_mask:Controls::WASD,
controls_intersects:Controls::WASD,
controls_contains:Controls::empty(),
}
}
@ -272,8 +273,8 @@ impl StrafeSettings{
false=>None,
}
}
pub fn next_tick(&self,time:Time)->Time{
Time::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
}
pub const fn activates(&self,controls:Controls)->bool{
self.enable.activates(controls)
@ -435,7 +436,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:None,
mv:int(3),
tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(64,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Energy(int(512)),
@ -477,10 +478,10 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:None,
mv:int(27)/10,
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Time(Time::from_micros(715_588)),
impulse:JumpImpulse::Time(AbsoluteTime::from_micros(715_588)),
calculation:JumpCalculation::Max,
limit_minimum:true,
}),
@ -534,7 +535,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some(Planar64::raw(150<<28)*100),
mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
@ -575,7 +576,7 @@ impl StyleModifiers{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
mv:(int(30)*VALVE_SCALE).fix_1(),
tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),

View File

@ -1,53 +1,82 @@
use crate::integer::Time;
#[derive(Debug)]
pub struct TimedInstruction<I>{
pub time:Time,
#[derive(Clone,Debug)]
pub struct TimedInstruction<I,T>{
pub time: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>{
TimedInstruction{
time:new_time,
instruction:self.instruction,
}
}
}
/// Ensure all emitted instructions are processed before consuming external instructions
pub trait InstructionEmitter<I>{
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<I>>;
type TimeInner;
fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
}
/// Apply an atomic state update
pub trait InstructionConsumer<I>{
fn process_instruction(&mut self, instruction:TimedInstruction<I>);
type TimeInner;
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
}
/// 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>
where
Time<T>:Copy,
{
#[inline]
fn process_exhaustive(&mut self,time_limit:Time<T>){
while let Some(instruction)=self.next_instruction(time_limit){
self.process_instruction(instruction);
}
}
}
impl<I,T,X> InstructionFeedback<I,T> for X
where
Time<T>:Copy,
X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
{}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I>{
time:Time,
pub struct InstructionCollector<I,T>{
time:Time<T>,
instruction:Option<I>,
}
impl<I> InstructionCollector<I>{
pub const fn new(time:Time)->Self{
impl<I,T> InstructionCollector<I,T>
where Time<T>:Copy+PartialOrd,
{
#[inline]
pub const fn new(time:Time<T>)->Self{
Self{
time,
instruction:None
}
}
#[inline]
pub const fn time(&self)->Time{
pub const fn time(&self)->Time<T>{
self.time
}
pub fn collect(&mut self,instruction:Option<TimedInstruction<I>>){
match instruction{
Some(unwrap_instruction)=>{
if unwrap_instruction.time<self.time {
self.time=unwrap_instruction.time;
self.instruction=Some(unwrap_instruction.instruction);
}
},
None=>(),
#[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);
}
}
}
pub fn instruction(self)->Option<TimedInstruction<I>>{
#[inline]
pub fn take(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
match self.instruction{
Some(instruction)=>Some(TimedInstruction{
time:self.time,
instruction
}),
None=>None,
}
self.instruction.map(|instruction|TimedInstruction{
time:self.time,
instruction
})
}
}

View File

@ -2,19 +2,26 @@ pub use fixed_wide::fixed::{Fixed,Fix};
pub use ratio_ops::ratio::{Ratio,Divide};
//integer units
/// specific example of a "default" time type
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub struct Time(i64);
impl Time{
pub const MIN:Self=Self(i64::MIN);
pub const MAX:Self=Self(i64::MAX);
pub const ZERO:Self=Self(0);
pub const ONE_SECOND:Self=Self(1_000_000_000);
pub const ONE_MILLISECOND:Self=Self(1_000_000);
pub const ONE_MICROSECOND:Self=Self(1_000);
pub const ONE_NANOSECOND:Self=Self(1);
pub enum TimeInner{}
pub type AbsoluteTime=Time<TimeInner>;
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub struct Time<T>(i64,core::marker::PhantomData<T>);
impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN);
pub const MAX:Self=Self::raw(i64::MAX);
pub const ZERO:Self=Self::raw(0);
pub const EPSILON:Self=Self::raw(1);
pub const ONE_SECOND:Self=Self::raw(1_000_000_000);
pub const ONE_MILLISECOND:Self=Self::raw(1_000_000);
pub const ONE_MICROSECOND:Self=Self::raw(1_000);
pub const ONE_NANOSECOND:Self=Self::raw(1);
#[inline]
pub const fn raw(num:i64)->Self{
Self(num)
Self(num,core::marker::PhantomData)
}
#[inline]
pub const fn get(self)->i64{
@ -22,19 +29,19 @@ impl Time{
}
#[inline]
pub const fn from_secs(num:i64)->Self{
Self(Self::ONE_SECOND.0*num)
Self::raw(Self::ONE_SECOND.0*num)
}
#[inline]
pub const fn from_millis(num:i64)->Self{
Self(Self::ONE_MILLISECOND.0*num)
Self::raw(Self::ONE_MILLISECOND.0*num)
}
#[inline]
pub const fn from_micros(num:i64)->Self{
Self(Self::ONE_MICROSECOND.0*num)
Self::raw(Self::ONE_MICROSECOND.0*num)
}
#[inline]
pub const fn from_nanos(num:i64)->Self{
Self(Self::ONE_NANOSECOND.0*num)
Self::raw(Self::ONE_NANOSECOND.0*num)
}
//should I have checked subtraction? force all time variables to be positive?
#[inline]
@ -45,14 +52,18 @@ impl Time{
pub const fn to_ratio(self)->Ratio<Planar64,Planar64>{
Ratio::new(Planar64::raw(self.0),Planar64::raw(1_000_000_000))
}
}
impl From<Planar64> for Time{
#[inline]
fn from(value:Planar64)->Self{
Time((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
pub const fn coerce<U>(self)->Time<U>{
Time::raw(self.0)
}
}
impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
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())
}
}
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>,
@ -60,34 +71,34 @@ impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
{
#[inline]
fn from(value:Ratio<Num,Den>)->Self{
Time((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
}
}
impl std::fmt::Display for Time{
impl<T> std::fmt::Display for Time<T>{
#[inline]
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
}
}
impl std::default::Default for Time{
impl<T> std::default::Default for Time<T>{
fn default()->Self{
Self(0)
Self::raw(0)
}
}
impl std::ops::Neg for Time{
type Output=Time;
impl<T> std::ops::Neg for Time<T>{
type Output=Self;
#[inline]
fn neg(self)->Self::Output {
Time(-self.0)
Self::raw(-self.0)
}
}
macro_rules! impl_time_additive_operator {
($trait:ty, $method:ident) => {
impl $trait for Time{
type Output=Time;
impl<T> $trait for Time<T>{
type Output=Self;
#[inline]
fn $method(self,rhs:Self)->Self::Output {
Time(self.0.$method(rhs.0))
Self::raw(self.0.$method(rhs.0))
}
}
};
@ -97,7 +108,7 @@ impl_time_additive_operator!(core::ops::Sub,sub);
impl_time_additive_operator!(core::ops::Rem,rem);
macro_rules! impl_time_additive_assign_operator {
($trait:ty, $method:ident) => {
impl $trait for Time{
impl<T> $trait for Time<T>{
#[inline]
fn $method(&mut self,rhs:Self){
self.0.$method(rhs.0)
@ -108,53 +119,58 @@ macro_rules! impl_time_additive_assign_operator {
impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
impl std::ops::Mul for Time{
impl<T> std::ops::Mul for Time<T>{
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
#[inline]
fn mul(self,rhs:Self)->Self::Output{
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
}
}
impl std::ops::Div<i64> for Time{
type Output=Time;
impl<T> std::ops::Div<i64> for Time<T>{
type Output=Self;
#[inline]
fn div(self,rhs:i64)->Self::Output{
Time(self.0/rhs)
Self::raw(self.0/rhs)
}
}
impl std::ops::Mul<i64> for Time{
type Output=Time;
impl<T> std::ops::Mul<i64> for Time<T>{
type Output=Self;
#[inline]
fn mul(self,rhs:i64)->Self::Output{
Time(self.0*rhs)
Self::raw(self.0*rhs)
}
}
impl core::ops::Mul<Time> for Planar64{
impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time)->Self::Output{
fn mul(self,rhs:Time<T>)->Self::Output{
Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
}
}
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_from_ratio(){
let a:Time=Ratio::new(Planar64::from(1),Planar64::from(1)).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_squared(){
let a=Time::from_secs(2);
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
}
#[test]
fn time_times_planar64(){
let a=Time::from_secs(2);
let b=Planar64::from(2);
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
#[cfg(test)]
mod test_time{
use super::*;
type Time=super::AbsoluteTime;
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_from_ratio(){
let a:Time=Ratio::new(Planar64::from(1),Planar64::from(1)).into();
assert_eq!(a,Time::ONE_SECOND);
}
#[test]
fn time_squared(){
let a=Time::from_secs(2);
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
}
#[test]
fn time_times_planar64(){
let a=Time::from_secs(2);
let b=Planar64::from(2);
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
}
}
#[inline]

View File

@ -7,6 +7,7 @@ pub mod mouse;
pub mod timer;
pub mod integer;
pub mod physics;
pub mod session;
pub mod updatable;
pub mod instruction;
pub mod gameplay_attributes;

View File

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

View File

@ -1,11 +1,11 @@
use crate::integer::Time;
#[derive(Clone,Debug)]
pub struct MouseState{
pub struct MouseState<T>{
pub pos:glam::IVec2,
pub time:Time,
pub time:Time<T>,
}
impl Default for MouseState{
impl<T> Default for MouseState<T>{
fn default()->Self{
Self{
time:Time::ZERO,
@ -13,8 +13,10 @@ impl Default for MouseState{
}
}
}
impl MouseState{
pub fn lerp(&self,target:&MouseState,time:Time)->glam::IVec2{
impl<T> MouseState<T>
where Time<T>:Copy,
{
pub fn lerp(&self,target:&MouseState<T>,time:Time<T>)->glam::IVec2{
let m0=self.pos.as_i64vec2();
let m1=target.pos.as_i64vec2();
//these are deltas

View File

@ -1,7 +1,33 @@
use crate::mouse::MouseState;
use crate::gameplay_modes::{ModeId,StageId};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;
#[derive(Clone,Debug)]
pub enum Instruction{
ReplaceMouse(crate::mouse::MouseState,crate::mouse::MouseState),
SetNextMouse(crate::mouse::MouseState),
Mouse(MouseInstruction),
SetControl(SetControlInstruction),
Mode(ModeInstruction),
Misc(MiscInstruction),
/// Idle: there were no input events, but the simulation is safe to advance to this timestep
Idle,
}
impl Instruction{
pub const IDLE:Self=Self::Idle;
}
#[derive(Clone,Debug)]
pub enum MouseInstruction{
/// Replace the entire interpolation state to avoid dividing by zero when replacing twice
ReplaceMouse{
m0:MouseState<TimeInner>,
m1:MouseState<TimeInner>,
},
SetNextMouse(MouseState<TimeInner>),
}
#[derive(Clone,Debug)]
pub enum SetControlInstruction{
SetMoveRight(bool),
SetMoveUp(bool),
SetMoveBack(bool),
@ -10,18 +36,21 @@ pub enum Instruction{
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
}
#[derive(Clone,Debug)]
pub enum ModeInstruction{
/// Reset: fully replace the physics state.
/// This forgets all inputs and settings which need to be reapplied.
Reset,
/// Restart: Teleport to the start zone.
Restart,
/// This runs when you press R or teleport to a bonus
Restart(ModeId),
/// Spawn: Teleport to a specific mode's spawn
/// Sets current mode & spawn
Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
Idle,
//Idle: there were no input events, but the simulation is safe to advance to this timestep
//for interpolation / networking / playback reasons, most playback heads will always want
//to be 1 instruction ahead to generate the next state for interpolation.
/// This runs when the map loads to put you at the map lobby
Spawn(ModeId,StageId),
}
#[derive(Clone,Debug)]
pub enum MiscInstruction{
PracticeFly,
SetSensitivity(crate::integer::Ratio64Vec2),
}

View File

@ -1,5 +1,10 @@
use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
use crate::integer::Time;
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>;
#[derive(Clone,Copy,Debug)]
pub enum FlagReason{
@ -15,6 +20,11 @@ pub enum FlagReason{
}
impl ToString for FlagReason{
fn to_string(&self)->String{
self.as_ref().to_owned()
}
}
impl AsRef<str> for FlagReason{
fn as_ref(&self)->&str{
match self{
FlagReason::Anticheat=>"Passed through anticheat zone.",
FlagReason::StyleChange=>"Changed style.",
@ -25,7 +35,7 @@ impl ToString for FlagReason{
FlagReason::Timescale=>"Timescale is not allowed in this style.",
FlagReason::TimeTravel=>"Time travel is not allowed in this style.",
FlagReason::Teleport=>"Illegal teleport.",
}.to_owned()
}
}
}
@ -45,8 +55,8 @@ impl std::error::Error for Error{}
#[derive(Clone,Copy,Debug)]
enum RunState{
Created,
Started{timer:TimerFixed<Realtime,Unpaused>},
Finished{timer:TimerFixed<Realtime,Paused>},
Started{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Unpaused>},
Finished{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Paused>},
}
#[derive(Clone,Copy,Debug)]
@ -62,14 +72,14 @@ impl Run{
flagged:None,
}
}
pub fn time(&self,time:Time)->Time{
pub fn time(&self,time:PhysicsTime)->Time{
match &self.state{
RunState::Created=>Time::ZERO,
RunState::Started{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(),
}
}
pub fn start(&mut self,time:Time)->Result<(),Error>{
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
match &self.state{
RunState::Created=>{
self.state=RunState::Started{
@ -81,7 +91,7 @@ impl Run{
RunState::Finished{..}=>Err(Error::AlreadyFinished),
}
}
pub fn finish(&mut self,time:Time)->Result<(),Error>{
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
//this uses Copy
match &self.state{
RunState::Created=>Err(Error::NotStarted),
@ -100,4 +110,10 @@ impl Run{
self.flagged=Some(flag_reason);
}
}
pub fn get_finish_time(&self)->Option<Time>{
match &self.state{
RunState::Finished{timer}=>Some(timer.time()),
_=>None,
}
}
}

View File

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

View File

@ -22,79 +22,106 @@ impl PauseState for Unpaused{
}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum Inner{}
type InnerTime=Time<Inner>;
#[derive(Clone,Copy,Debug)]
pub struct Realtime{
offset:Time,
pub struct Realtime<In,Out>{
offset:InnerTime,
_in:core::marker::PhantomData<In>,
_out:core::marker::PhantomData<Out>,
}
impl Realtime{
pub const fn new(offset:Time)->Self{
Self{offset}
impl<In,Out> Realtime<In,Out>{
pub const fn new(offset:InnerTime)->Self{
Self{
offset,
_in:core::marker::PhantomData,
_out:core::marker::PhantomData,
}
}
}
#[derive(Clone,Copy,Debug)]
pub struct Scaled{
pub struct Scaled<In,Out>{
scale:Ratio64,
offset:Time,
offset:InnerTime,
_in:core::marker::PhantomData<In>,
_out:core::marker::PhantomData<Out>,
}
impl Scaled{
pub const fn new(scale:Ratio64,offset:Time)->Self{
Self{scale,offset}
impl<In,Out> Scaled<In,Out>
where Time<In>:Copy,
{
pub const fn new(scale:Ratio64,offset:InnerTime)->Self{
Self{
scale,
offset,
_in:core::marker::PhantomData,
_out:core::marker::PhantomData,
}
}
const fn with_scale(scale:Ratio64)->Self{
Self{scale,offset:Time::ZERO}
Self::new(scale,InnerTime::ZERO)
}
const fn scale(&self,time:Time)->Time{
Time::raw(self.scale.mul_int(time.get()))
const fn scale(&self,time:Time<In>)->InnerTime{
InnerTime::raw(self.scale.mul_int(time.get()))
}
const fn get_scale(&self)->Ratio64{
self.scale
}
fn set_scale(&mut self,time:Time,new_scale:Ratio64){
fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
let new_time=self.get_time(time);
self.scale=new_scale;
self.set_time(time,new_time);
}
}
pub trait TimerState:Copy+std::fmt::Debug{
pub trait TimerState{
type In;
type Out;
fn identity()->Self;
fn get_time(&self,time:Time)->Time;
fn set_time(&mut self,time:Time,new_time:Time);
fn get_offset(&self)->Time;
fn set_offset(&mut self,offset:Time);
fn get_time(&self,time:Time<Self::In>)->Time<Self::Out>;
fn set_time(&mut self,time:Time<Self::In>,new_time:Time<Self::Out>);
fn get_offset(&self)->InnerTime;
fn set_offset(&mut self,offset:InnerTime);
}
impl TimerState for Realtime{
impl<In,Out> TimerState for Realtime<In,Out>{
type In=In;
type Out=Out;
fn identity()->Self{
Self{offset:Time::ZERO}
Self::new(InnerTime::ZERO)
}
fn get_time(&self,time:Time)->Time{
time+self.offset
fn get_time(&self,time:Time<In>)->Time<Out>{
time.coerce()+self.offset.coerce()
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-time;
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
self.offset=new_time.coerce()-time.coerce();
}
fn get_offset(&self)->Time{
fn get_offset(&self)->InnerTime{
self.offset
}
fn set_offset(&mut self,offset:Time){
fn set_offset(&mut self,offset:InnerTime){
self.offset=offset;
}
}
impl TimerState for Scaled{
impl<In,Out> TimerState for Scaled<In,Out>
where Time<In>:Copy,
{
type In=In;
type Out=Out;
fn identity()->Self{
Self{scale:Ratio64::ONE,offset:Time::ZERO}
Self::new(Ratio64::ONE,InnerTime::ZERO)
}
fn get_time(&self,time:Time)->Time{
self.scale(time)+self.offset
fn get_time(&self,time:Time<In>)->Time<Out>{
(self.scale(time)+self.offset).coerce()
}
fn set_time(&mut self,time:Time,new_time:Time){
self.offset=new_time-self.scale(time);
fn set_time(&mut self,time:Time<In>,new_time:Time<Out>){
self.offset=new_time.coerce()-self.scale(time);
}
fn get_offset(&self)->Time{
fn get_offset(&self)->InnerTime{
self.offset
}
fn set_offset(&mut self,offset:Time){
fn set_offset(&mut self,offset:InnerTime){
self.offset=offset;
}
}
@ -106,8 +133,10 @@ pub struct TimerFixed<T:TimerState,P:PauseState>{
}
//scaled timer methods are generic across PauseState
impl<P:PauseState> TimerFixed<Scaled,P>{
pub fn scaled(time:Time,new_time:Time,scale:Ratio64)->Self{
impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
where Time<In>:Copy,
{
pub fn scaled(time:Time<In>,new_time:Time<Out>,scale:Ratio64)->Self{
let mut timer=Self{
state:Scaled::with_scale(scale),
_paused:P::new(),
@ -118,15 +147,17 @@ impl<P:PauseState> TimerFixed<Scaled,P>{
pub const fn get_scale(&self)->Ratio64{
self.state.get_scale()
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
self.state.set_scale(time,new_scale)
}
}
//pause and unpause is generic across TimerState
impl<T:TimerState> TimerFixed<T,Paused>{
pub fn into_unpaused(self,time:Time)->TimerFixed<T,Unpaused>{
let new_time=self.time(time);
impl<T:TimerState> TimerFixed<T,Paused>
where Time<T::In>:Copy,
{
pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{
let new_time=self.time();
let mut timer=TimerFixed{
state:self.state,
_paused:Unpaused,
@ -134,9 +165,14 @@ impl<T:TimerState> TimerFixed<T,Paused>{
timer.set_time(time,new_time);
timer
}
pub fn time(&self)->Time<T::Out>{
self.state.get_offset().coerce()
}
}
impl<T:TimerState> TimerFixed<T,Unpaused>{
pub fn into_paused(self,time:Time)->TimerFixed<T,Paused>{
impl<T:TimerState> TimerFixed<T,Unpaused>
where Time<T::In>:Copy,
{
pub fn into_paused(self,time:Time<T::In>)->TimerFixed<T,Paused>{
let new_time=self.time(time);
let mut timer=TimerFixed{
state:self.state,
@ -145,11 +181,14 @@ impl<T:TimerState> TimerFixed<T,Unpaused>{
timer.set_time(time,new_time);
timer
}
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
self.state.get_time(time)
}
}
//the new constructor and time queries are generic across both
impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn new(time:Time,new_time:Time)->Self{
pub fn new(time:Time<T::In>,new_time:Time<T::Out>)->Self{
let mut timer=Self{
state:T::identity(),
_paused:P::new(),
@ -166,15 +205,9 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn into_state(self)->T{
self.state
}
pub fn time(&self,time:Time)->Time{
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
match P::IS_PAUSED{
true=>self.state.get_offset(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time,new_time:Time){
match P::IS_PAUSED{
true=>self.state.set_offset(new_time),
true=>self.state.set_offset(new_time.coerce()),
false=>self.state.set_time(time,new_time),
}
}
@ -198,7 +231,11 @@ pub enum Timer<T:TimerState>{
Paused(TimerFixed<T,Paused>),
Unpaused(TimerFixed<T,Unpaused>),
}
impl<T:TimerState> Timer<T>{
impl<T:TimerState> Timer<T>
where
T:Copy,
Time<T::In>:Copy,
{
pub fn from_state(state:T,paused:bool)->Self{
match paused{
true=>Self::Paused(TimerFixed::from_state(state)),
@ -211,32 +248,32 @@ impl<T:TimerState> Timer<T>{
Self::Unpaused(timer)=>(timer.into_state(),false),
}
}
pub fn paused(time:Time,new_time:Time)->Self{
pub fn paused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
Self::Paused(TimerFixed::new(time,new_time))
}
pub fn unpaused(time:Time,new_time:Time)->Self{
pub fn unpaused(time:Time<T::In>,new_time:Time<T::Out>)->Self{
Self::Unpaused(TimerFixed::new(time,new_time))
}
pub fn time(&self,time:Time)->Time{
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match self{
Self::Paused(timer)=>timer.time(time),
Self::Paused(timer)=>timer.time(),
Self::Unpaused(timer)=>timer.time(time),
}
}
pub fn set_time(&mut self,time:Time,new_time:Time){
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
match self{
Self::Paused(timer)=>timer.set_time(time,new_time),
Self::Unpaused(timer)=>timer.set_time(time,new_time),
}
}
pub fn pause(&mut self,time:Time)->Result<(),Error>{
pub fn pause(&mut self,time:Time<T::In>)->Result<(),Error>{
*self=match *self{
Self::Paused(_)=>return Err(Error::AlreadyPaused),
Self::Unpaused(timer)=>Self::Paused(timer.into_paused(time)),
};
Ok(())
}
pub fn unpause(&mut self,time:Time)->Result<(),Error>{
pub fn unpause(&mut self,time:Time<T::In>)->Result<(),Error>{
*self=match *self{
Self::Paused(timer)=>Self::Unpaused(timer.into_unpaused(time)),
Self::Unpaused(_)=>return Err(Error::AlreadyUnpaused),
@ -249,7 +286,7 @@ impl<T:TimerState> Timer<T>{
Self::Unpaused(_)=>false,
}
}
pub fn set_paused(&mut self,time:Time,paused:bool)->Result<(),Error>{
pub fn set_paused(&mut self,time:Time<T::In>,paused:bool)->Result<(),Error>{
match paused{
true=>self.pause(time),
false=>self.unpause(time),
@ -257,14 +294,16 @@ impl<T:TimerState> Timer<T>{
}
}
//scaled timer methods are generic across PauseState
impl Timer<Scaled>{
impl<In,Out> Timer<Scaled<In,Out>>
where Time<In>:Copy,
{
pub const fn get_scale(&self)->Ratio64{
match self{
Self::Paused(timer)=>timer.get_scale(),
Self::Unpaused(timer)=>timer.get_scale(),
}
}
pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
match self{
Self::Paused(timer)=>timer.set_scale(time,new_scale),
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
@ -280,12 +319,17 @@ mod test{
Time::from_secs($s)
};
}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Parent{}
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
enum Calculated{}
#[test]
fn test_timerfixed_scaled(){
//create a paused timer that reads 0s
let timer=TimerFixed::<Scaled,Paused>::from_state(Scaled{scale:0.5f32.try_into().unwrap(),offset:sec!(0)});
let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0)));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));
assert_eq!(timer.time(),sec!(0));
//unpause it after one second
let timer=timer.into_unpaused(sec!(1));
@ -295,12 +339,12 @@ mod test{
//pause the timer after 11 seconds
let timer=timer.into_paused(sec!(11));
//the paused timer at 20 seconds should read 5s
assert_eq!(timer.time(sec!(20)),sec!(5));
assert_eq!(timer.time(),sec!(5));
}
#[test]
fn test_timer()->Result<(),Error>{
//create a paused timer that reads 0s
let mut timer=Timer::<Realtime>::paused(sec!(0),sec!(0));
let mut timer=Timer::<Realtime<Parent,Calculated>>::paused(sec!(0),sec!(0));
//the paused timer at 1 second should read 0s
assert_eq!(timer.time(sec!(1)),sec!(0));

View File

@ -1,3 +1,5 @@
// This whole thing should be a drive macro
pub trait Updatable<Updater>{
fn update(&mut self,update:Updater);
}
@ -53,4 +55,3 @@ impl Updatable<OuterUpdate> for Outer{
}
}
}
//*/

View File

@ -1,6 +1,6 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.4.1"
version = "0.5.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -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" }

View File

@ -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<L:Loader<Resource=Texture,Index=H>>(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<L:Loader<Resource=Mesh,Index=H>>(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))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "fixed_wide"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -17,4 +17,4 @@ zeroes=["dep:arrayvec"]
bnum = "0.12.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 }

View File

@ -1,6 +1,6 @@
use bnum::{BInt,cast::As};
#[derive(Clone,Copy,Debug,Default,Hash)]
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord)]
/// A Fixed point number for which multiply operations widen the bits in the output. (when the wide-mul feature is enabled)
/// N is the number of u64s to use
/// F is the number of fractional bits (always N*32 lol)
@ -87,12 +87,6 @@ impl_from!(
i8,i16,i32,i64,i128,isize
);
impl<const N:usize,const F:usize> PartialEq for Fixed<N,F>{
#[inline]
fn eq(&self,other:&Self)->bool{
self.bits.eq(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialEq<T> for Fixed<N,F>
where
T:Copy,
@ -103,14 +97,7 @@ where
self.bits.eq(&other.into())
}
}
impl<const N:usize,const F:usize> Eq for Fixed<N,F>{}
impl<const N:usize,const F:usize> PartialOrd for Fixed<N,F>{
#[inline]
fn partial_cmp(&self,other:&Self)->Option<std::cmp::Ordering>{
self.bits.partial_cmp(&other.bits)
}
}
impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
where
T:Copy,
@ -121,12 +108,6 @@ impl<const N:usize,const F:usize,T> PartialOrd<T> for Fixed<N,F>
self.bits.partial_cmp(&other.into())
}
}
impl<const N:usize,const F:usize> Ord for Fixed<N,F>{
#[inline]
fn cmp(&self,other:&Self)->std::cmp::Ordering{
self.bits.cmp(&other.bits)
}
}
impl<const N:usize,const F:usize> std::ops::Neg for Fixed<N,F>{
type Output=Self;

View File

@ -57,7 +57,7 @@ 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

View File

@ -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.1.2", path = "../fixed_wide", registry = "strafesnet", optional = true }
paste = { version = "1.0.15", optional = true }
[dev-dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "strafesnet_rbx_loader"
version = "0.5.2"
version = "0.6.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@ -15,8 +15,10 @@ glam = "0.29.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_mesh = "0.3.1"
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" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }

View File

@ -1,8 +1,11 @@
use std::io::Read;
use rbx_dom_weak::WeakDom;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod rbx;
mod mesh;
mod union;
pub mod loader;
mod primitives;
pub mod data{
@ -30,6 +33,9 @@ impl Model{
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{
fn as_ref(&self)->&WeakDom{
@ -42,7 +48,7 @@ pub struct Place{
services:roblox_emulator::context::Services,
}
impl Place{
fn new(dom:WeakDom)->Option<Self>{
pub fn new(dom:WeakDom)->Option<Self>{
let context=roblox_emulator::context::Context::from_ref(&dom);
Some(Self{
services:context.find_services()?,
@ -61,6 +67,9 @@ impl Place{
}
}
}
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{
@ -92,16 +101,49 @@ pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
}
}
//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)
}

View File

@ -0,0 +1,191 @@
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::RobloxFaceTextureDescription;
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<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=&'a str;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?;
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:[Option<RobloxFaceTextureDescription>;6],
},
}
#[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:crate::rbx::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<'a>(std::marker::PhantomData<&'a ()>);
impl MeshLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for MeshLoader<'a>{
type Error=MeshError;
type Index=MeshIndex<'a>;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?;
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("PhysicsData"){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("MeshData"){
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)
}
}

View File

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

View File

@ -1,4 +1,4 @@
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::integer::{vec3,Planar64Vec3};
#[derive(Debug)]
@ -42,50 +42,6 @@ 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{
@ -126,17 +82,14 @@ const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
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]);
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 fn pairs(self)->impl Iterator<Item=(usize,FaceDescription)>{
self.0.into_iter().enumerate().filter_map(|(i,v)|v.map(|u|(i,u)))
}
}
pub fn unit_cube(render:RenderConfigId)->Mesh{
@ -149,10 +102,6 @@ pub fn unit_cube(render:RenderConfigId)->Mesh{
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]);
impl WedgeFaceDescription{
@ -163,15 +112,15 @@ impl WedgeFaceDescription{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
}
}
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)
}
// 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]);
impl CornerWedgeFaceDescription{
@ -182,15 +131,15 @@ impl CornerWedgeFaceDescription{
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)
}
// pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
// let mut t=CornerWedgeFaceDescription::default();
// t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render));
// generate_partial_unit_cornerwedge(t)
// }
#[derive(Clone)]
pub struct FaceDescription{
@ -208,6 +157,50 @@ impl FaceDescription{
}
}
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
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],
],
];
let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new();
@ -286,35 +279,35 @@ 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=[
const WEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
// right (1, 0, 0)
vec![
&[
[6,2,0],//[vertex,tex,norm]
[2,0,0],
[1,3,0],
],
// FrontTop (0, 1, -1)
vec![
&[
[3,1,1],
[2,0,1],
[6,3,1],
[7,2,1],
],
// back (0, 0, 1)
vec![
&[
[0,3,2],
[1,2,2],
[2,1,2],
[3,0,2],
],
// left (-1, 0, 0)
vec![
&[
[0,2,3],
[3,1,3],
[7,3,3],
],
// bottom (0,-1, 0)
vec![
&[
[1,1,4],
[0,0,4],
[7,3,4],
@ -358,7 +351,7 @@ 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|{
WEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index
@ -399,34 +392,34 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
}
pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
let cornerwedge_default_polys=[
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
// right (1, 0, 0)
vec![
&[
[6,2,0],//[vertex,tex,norm]
[5,1,0],
[1,3,0],
],
// BackTop (0, 1, 1)
vec![
&[
[5,3,1],
[0,1,1],
[1,0,1],
],
// LeftTop (-1, 1, 0)
vec![
&[
[5,3,2],
[7,2,2],
[0,1,2],
],
// bottom (0,-1, 0)
vec![
&[
[1,1,3],
[0,0,3],
[7,3,3],
[6,2,3],
],
// front (0, 0,-1)
vec![
&[
[5,0,4],
[6,3,4],
[7,2,4],
@ -469,7 +462,7 @@ 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|{
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index

View File

@ -1,5 +1,7 @@
use std::collections::HashMap;
use crate::loader::MeshIndex;
use crate::primitives;
use strafesnet_common::aabb::Aabb;
use strafesnet_common::map;
use strafesnet_common::model;
use strafesnet_common::gameplay_modes;
@ -8,6 +10,9 @@ use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId;
use strafesnet_common::updatable::Updatable;
use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
fn class_is_a(class: &str, superclass: &str) -> bool {
if class==superclass {
@ -130,9 +135,9 @@ impl ModesBuilder{
fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){
self.mode_updates.push((mode_id,mode_update));
}
fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
self.stage_updates.push((mode_id,stage_id,stage_update));
}
// fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
// self.stage_updates.push((mode_id,stage_id,stage_update));
// }
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->attr::CollisionAttributes{
let mut general=attr::GeneralAttributes::default();
@ -341,58 +346,103 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}
}
#[derive(Clone,Copy,PartialEq)]
struct RobloxTextureTransform{
offset_u:f32,
offset_v:f32,
scale_u:f32,
scale_v:f32,
#[derive(Clone,Copy)]
pub struct RobloxTextureTransform{
offset_studs_u:f32,
offset_studs_v:f32,
studs_per_tile_u:f32,
studs_per_tile_v:f32,
size_u:f32,
size_v:f32,
}
impl std::cmp::Eq for RobloxTextureTransform{}//????
impl std::default::Default for RobloxTextureTransform{
fn default()->Self{
Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub struct RobloxTextureTransformBits{
offset_studs_u:u32,
offset_studs_v:u32,
studs_per_tile_u:u32,
studs_per_tile_v:u32,
size_u:u32,
size_v:u32,
}
impl std::hash::Hash for RobloxTextureTransform{
fn hash<H:std::hash::Hasher>(&self,state:&mut H) {
self.offset_u.to_ne_bytes().hash(state);
self.offset_v.to_ne_bytes().hash(state);
self.scale_u.to_ne_bytes().hash(state);
self.scale_v.to_ne_bytes().hash(state);
impl RobloxTextureTransform{
fn identity()->Self{
Self{
offset_studs_u:0.0,
offset_studs_v:0.0,
studs_per_tile_u:1.0,
studs_per_tile_v:1.0,
size_u:1.0,
size_v:1.0,
}
}
pub fn to_bits(self)->RobloxTextureTransformBits{
RobloxTextureTransformBits{
offset_studs_u:self.offset_studs_u.to_bits(),
offset_studs_v:self.offset_studs_v.to_bits(),
studs_per_tile_u:self.studs_per_tile_u.to_bits(),
studs_per_tile_v:self.studs_per_tile_v.to_bits(),
size_u:self.size_u.to_bits(),
size_v:self.size_v.to_bits(),
}
}
pub fn affine(&self)->glam::Affine2{
glam::Affine2::from_translation(
glam::vec2(self.offset_studs_u/self.studs_per_tile_u,self.offset_studs_v/self.studs_per_tile_v)
)
*glam::Affine2::from_scale(
glam::vec2(self.size_u/self.studs_per_tile_u,self.size_v/self.studs_per_tile_v)
)
}
pub fn set_size(&mut self,size_u:f32,size_v:f32){
self.size_u=size_u;
self.size_v=size_v;
}
}
#[derive(Clone,PartialEq)]
struct RobloxFaceTextureDescription{
render:RenderConfigId,
color:glam::Vec4,
transform:RobloxTextureTransform,
impl core::hash::Hash for RobloxTextureTransform{
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
self.to_bits().hash(state);
}
}
impl std::cmp::Eq for RobloxFaceTextureDescription{}//????
impl std::hash::Hash for RobloxFaceTextureDescription{
fn hash<H:std::hash::Hasher>(&self,state:&mut H){
self.render.hash(state);
self.transform.hash(state);
for &el in self.color.as_ref().iter(){
el.to_ne_bytes().hash(state);
}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub struct RobloxFaceTextureDescriptionBits{
render:RenderConfigId,
color:[u32;4],
transform:RobloxTextureTransformBits,
}
#[derive(Clone,Copy)]
pub struct RobloxFaceTextureDescription{
pub render:RenderConfigId,
pub color:glam::Vec4,
pub transform:RobloxTextureTransform,
}
impl core::cmp::PartialEq for RobloxFaceTextureDescription{
fn eq(&self,other:&Self)->bool{
self.to_bits().eq(&other.to_bits())
}
}
impl core::cmp::Eq for RobloxFaceTextureDescription{}
impl core::hash::Hash for RobloxFaceTextureDescription{
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
self.to_bits().hash(state);
}
}
impl RobloxFaceTextureDescription{
fn to_face_description(&self)->primitives::FaceDescription{
pub fn to_bits(self)->RobloxFaceTextureDescriptionBits{
RobloxFaceTextureDescriptionBits{
render:self.render,
color:self.color.to_array().map(f32::to_bits),
transform:self.transform.to_bits(),
}
}
pub fn to_face_description(&self)->primitives::FaceDescription{
primitives::FaceDescription{
render:self.render,
transform:glam::Affine2::from_translation(
glam::vec2(self.transform.offset_u,self.transform.offset_v)
)
*glam::Affine2::from_scale(
glam::vec2(self.transform.scale_u,self.transform.scale_v)
),
transform:self.transform.affine(),
color:self.color,
}
}
}
type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
pub type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
#[derive(Clone,Eq,Hash,PartialEq)]
@ -403,52 +453,134 @@ enum RobloxBasePartDescription{
Wedge(RobloxWedgeDescription),
CornerWedge(RobloxCornerWedgeDescription),
}
fn get_texture_description<'a>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
dom:&'a rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance,
size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription{
//use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
for &mut decal_ref in temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_u)),
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_v)),
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
) = (
decal.properties.get("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_studs_u,
offset_studs_v,
studs_per_tile_u,
studs_per_tile_v,
size_u,
size_v,
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::identity())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::identity())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} is invalid",normal_id);
}
}
}
}
part_texture_description
}
enum Shape{
Primitive(primitives::Primitives),
MeshPart,
PhysicsData,
}
enum MeshAvailability{
Immediate,
Deferred(RenderConfigId),
DeferredMesh(RenderConfigId),
DeferredUnion(RobloxPartDescription),
}
struct DeferredModelDeferredAttributes{
struct DeferredModelDeferredAttributes<'a>{
render:RenderConfigId,
model:ModelDeferredAttributes,
model:ModelDeferredAttributes<'a>,
}
struct ModelDeferredAttributes{
struct ModelDeferredAttributes<'a>{
mesh:model::MeshId,
deferred_attributes:GetAttributesArgs,
deferred_attributes:GetAttributesArgs<'a>,
color:model::Color4,//transparency is in here
transform:Planar64Affine3,
}
struct DeferredUnionDeferredAttributes<'a>{
render:RobloxPartDescription,
model:ModelDeferredAttributes<'a>,
}
struct ModelOwnedAttributes{
mesh:model::MeshId,
attributes:attr::CollisionAttributes,
color:model::Color4,//transparency is in here
transform:Planar64Affine3,
}
struct GetAttributesArgs{
name:Box<str>,
struct GetAttributesArgs<'a>{
name:&'a str,
can_collide:bool,
velocity:Planar64Vec3,
}
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:&rbx_dom_weak::WeakDom,
mut acquire_render_config_id:AcquireRenderConfigId,
mut acquire_mesh_id:AcquireMeshId,
)->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
pub fn convert<'a>(
dom:&'a rbx_dom_weak::WeakDom,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
)->PartialMap1<'a>{
let mut deferred_models_deferred_attributes=Vec::new();
let mut deferred_unions_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new();
let mut primitive_meshes=Vec::new();
let mut mesh_id_from_description=HashMap::new();
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=acquire_render_config_id(None);
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let mut object_refs=Vec::new();
let mut temp_objects=Vec::new();
@ -485,9 +617,6 @@ where
continue;
}
//at this point a new model is going to be generated for sure.
let model_id=model::ModelId::new(primitive_models_deferred_attributes.len() as u32);
//TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){
@ -506,6 +635,7 @@ where
"WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
_=>{
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
Shape::Primitive(primitives::Primitives::Cube)
@ -514,74 +644,8 @@ where
let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB
//use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal");
for &decal_ref in &temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(rbx_dom_weak::types::Variant::Float32(ox)),
Some(rbx_dom_weak::types::Variant::Float32(oy)),
Some(rbx_dom_weak::types::Variant::Float32(sx)),
Some(rbx_dom_weak::types::Variant::Float32(sy)),
) = (
decal.properties.get("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape);
}
}
}
}
//TODO: TAB TAB
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
//obscure rust syntax "slice pattern"
let [
f0,//Cube::Right
@ -598,7 +662,7 @@ where
//use front face texture first and use top face texture as a fallback
primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([
f0,//Cube::Right->Wedge::Right
if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront
f5.or(f1),//Cube::Front|Cube::Top->Wedge::TopFront
f2,//Cube::Back->Wedge::Back
f3,//Cube::Left->Wedge::Left
f4,//Cube::Bottom->Wedge::Bottom
@ -606,8 +670,8 @@ where
//TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
f0,//Cube::Right->CornerWedge::Right
if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack
if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft
f2.or(f1.clone()),//Cube::Back|Cube::Top->CornerWedge::TopBack
f3.or(f1),//Cube::Left|Cube::Top->CornerWedge::TopLeft
f4,//Cube::Bottom->CornerWedge::Bottom
f5,//Cube::Front->CornerWedge::Front
]),
@ -694,29 +758,51 @@ where
object.properties.get("TextureID"),
){
(
MeshAvailability::Deferred(acquire_render_config_id(Some(texture_asset_id.as_ref()))),
acquire_mesh_id(mesh_asset_id.as_ref()),
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id.as_ref())),
)
}else{
panic!("Mesh has no Mesh or Texture");
},
Shape::PhysicsData=>{
let mut content="";
let mut mesh_data:&[u8]=&[];
let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::Content(asset_id))=object.properties.get("AssetId"){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("MeshData"){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("PhysicsData"){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
},
};
let model_deferred_attributes=ModelDeferredAttributes{
mesh:mesh_id,
transform:model_transform,
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
deferred_attributes:GetAttributesArgs{
name:object.name.as_str().into(),
name:object.name.as_str(),
can_collide:*can_collide,
velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
},
};
match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render,
model:model_deferred_attributes
}),
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
}),
}
}
}
@ -725,21 +811,71 @@ where
primitive_meshes,
primitive_models_deferred_attributes,
deferred_models_deferred_attributes,
deferred_unions_deferred_attributes,
}
}
struct MeshWithAabb{
mesh:model::Mesh,
aabb:strafesnet_common::aabb::Aabb,
aabb:Aabb,
}
pub struct PartialMap1{
fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
old_mesh_id:model::MeshId,
render:RenderConfigId,
)->Option<(model::MeshId,&'a Aabb)>{
//ignore meshes that fail to load completely for now
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone();
//set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render;
}
primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
}
fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription,
)->Option<(model::MeshId,&'a Aabb)>{
//ignore uniones that fail to load completely for now
loaded_meshes.get(&old_union_id).map(|union_with_aabb|(
*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
.entry(part_texture_description.clone()).or_insert_with(||{
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut union_clone=union_with_aabb.mesh.clone();
//set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description){
if let Some(face_texture_description)=maybe_face_texture_description{
graphics_group.render=face_texture_description.render;
}
}
primitive_meshes.push(union_clone);
union_id
}),
&union_with_aabb.aabb,
))
}
pub struct PartialMap1<'a>{
primitive_meshes:Vec<model::Mesh>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
}
impl PartialMap1{
impl PartialMap1<'_>{
pub fn add_meshpart_meshes_and_calculate_attributes(
mut self,
meshpart_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::RobloxMeshBytes)>,
meshpart_meshes:Meshes,
)->PartialMap2{
//calculate attributes
let mut modes_builder=ModesBuilder::default();
@ -752,51 +888,32 @@ impl PartialMap1{
//decode roblox meshes
//generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>=
meshpart_meshes.into_iter().flat_map(|(old_mesh_id,roblox_mesh_bytes)|
match crate::mesh::convert(roblox_mesh_bytes){
Ok(mesh)=>{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
Some((old_mesh_id,MeshWithAabb{
mesh,
aabb,
}))
},
Err(e)=>{
println!("Error converting mesh: {e:?}");
None
},
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
).collect();
(old_mesh_id,MeshWithAabb{
mesh,
aabb,
})
}).collect();
// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
// I just want to chain iterators together man
let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);
let mut mesh_id_from_render_config_id=HashMap::new();
//ignore meshes that fail to load completely for now
let mut acquire_mesh_id_from_render_config_id=|old_mesh_id,render|{
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(self.primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone();
//add a render group lool
mesh_clone.graphics_groups.push(model::IndexedGraphicsGroup{
render,
//the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)]
});
self.primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
};
let mut union_id_from_render_config_id=HashMap::new();
//now that the meshes are loaded, these models can be generated
let models_owned_attributes:Vec<ModelOwnedAttributes>=
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_mesh_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut mesh_id_from_render_config_id,
&loaded_meshes,
deferred_model_deferred_attributes.model.mesh,
deferred_model_deferred_attributes.render
)?;
@ -814,7 +931,32 @@ impl PartialMap1{
deferred_model_deferred_attributes.model.transform.translation
),
})
}).chain(self.primitive_models_deferred_attributes.into_iter())
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_union_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut union_id_from_render_config_id,
&loaded_meshes,
deferred_union_deferred_attributes.model.mesh,
deferred_union_deferred_attributes.render
)?;
let size=aabb.size();
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
color:deferred_union_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().fix_1(),
(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().fix_1(),
(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().fix_1()
]),
deferred_union_deferred_attributes.model.transform.translation
),
})
}))
.chain(self.primitive_models_deferred_attributes.into_iter())
.enumerate().map(|(model_id,model_deferred_attributes)|{
let model_id=model::ModelId::new(model_id as u32);
ModelOwnedAttributes{
@ -882,15 +1024,21 @@ pub struct PartialMap2{
impl PartialMap2{
pub fn add_render_configs_and_textures(
self,
render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
render_configs:RenderConfigs,
)->map::CompleteMap{
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
=textures.into_iter().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)|{
//this may generate duplicate no-texture render configs but idc
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{
// This may generate duplicate no-texture render configs but idc
//
// This is because some textures may not exist, so the render config
// that it points to is unique but is texture.
//
// I don't think this needs to be fixed because missing textures
// should be a conversion error anyways.
render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied()
);

177
lib/rbx_loader/src/union.rs Normal file
View File

@ -0,0 +1,177 @@
use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
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,
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 [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
];
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{
normal_agreement_checker.check(vertex.normal_id);
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
let tex_coord=glam::Vec2::from_array(vertex.tex);
let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
(0..NORMAL_FACES).map(|polygon_group_id|{
model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
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(|[vertex_id0,vertex_id1,vertex_id2]|{
let face=[
mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
})).collect::<Result<_,_>>()?;
let 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
View File

@ -0,0 +1,11 @@
[package]
name = "rbxassetid"
version = "0.1.0"
edition = "2021"
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"

View File

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

View File

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

View File

@ -33,7 +33,7 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
// 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"))?;
f(&mut *dom)
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{

View File

@ -1,22 +1,22 @@
macro_rules! type_from_lua_userdata{
($asd:ident)=>{
impl mlua::FromLua for $asd{
($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>()?),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
}
}
}
};
}
macro_rules! type_from_lua_userdata_lua_lifetime{
($asd:ident)=>{
impl mlua::FromLua for $asd<'static>{
($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::UserData(ud)=>Ok(*ud.borrow::<Self>()?),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($asd),other))),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
}
}
}

View File

@ -59,8 +59,8 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
match delay.classify(){
std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?,
// cases where the number is too large to schedule
std::num::FpCategory::Infinite=>return Ok(()),
std::num::FpCategory::Normal=>if (u64::MAX as f64)<delay{
std::num::FpCategory::Infinite
|std::num::FpCategory::Normal if (u64::MAX as f64)<delay=>{
return Ok(());
},
_=>(),

View File

@ -1,6 +1,6 @@
[package]
name = "strafesnet_snf"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -8,4 +8,4 @@ edition = "2021"
[dependencies]
binrw = "0.14.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }

View File

@ -1,98 +1,347 @@
use binrw::{BinReaderExt, binrw};
use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt};
use crate::newtypes;
use crate::file::BlockId;
use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
#[derive(Debug)]
pub enum Error{
InvalidHeader,
InvalidHeader(binrw::Error),
InvalidSegment(binrw::Error),
SegmentConvert(newtypes::integer::RatioError),
InstructionConvert(newtypes::physics::InstructionConvert),
InstructionWrite(binrw::Error),
InvalidSegmentId(SegmentId),
InvalidData(binrw::Error),
IO(std::io::Error),
File(crate::file::Error),
}
// Bot files are simply the sequence of instructions that the physics received during the run.
// The instructions are partitioned into timestamped blocks for ease of streaming.
//
// Keyframe information required for efficient seeking
// is part of a different file, and is generated from this file.
/* block types
BLOCK_BOT_HEADER:
u128 map_resource_uuid //which map is this bot running
//don't include style info in bot header because it's in the simulation state
//blocks are laid out in chronological order, but indices may jump around.
u64 num_segments
// Segments are laid out in chronological order,
// but block_id is not necessarily in ascending order.
//
// This is to place the final segment close to the start of the file,
// which allows the duration of the bot to be conveniently calculated
// from the first and last instruction timestamps.
//
// Use exact physics version for replay playback
// Use highest compatible physics version for verification
u32 physics_version
u32 num_segments
for _ in 0..num_segments{
i64 time //simulation_state timestamp
u64 block_id
i64 time
u32 instruction_count
u32 block_id
}
BLOCK_BOT_SEGMENT:
//format version indicates what version of these structures to use
SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about.
//to read, greedily decode instructions until eof
loop{
//delta encode as much as possible (time,mousepos)
//strafe ticks are implied
//physics can be implied in an input-only bot file
TimedInstruction<SimulationInstruction> instruction
// segments can potentially be losslessly compressed!
for _ in 0..instruction_count{
// TODO: delta encode as much as possible (time,mousepos)
i64 time
physics::Instruction instruction
}
*/
//error hiding mock code
mod simulation{
#[super::binrw]
#[brw(little)]
pub struct State{}
#[super::binrw]
#[brw(little)]
pub struct Instruction{}
#[binrw]
#[brw(little)]
struct SegmentHeader{
time:i64,
instruction_count:u32,
block_id:BlockId,
}
#[binrw]
#[brw(little)]
struct Header{
physics_version:u32,
num_segments:u32,
#[br(count=num_segments)]
segments:Vec<SegmentHeader>,
}
// mod instruction{
// #[super::binrw]
// #[brw(little)]
// pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{
// time:u64,
// instruction:Instruction
// }
// }
// mod timeline{
// #[super::binrw]
// #[brw(little)]
// pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{
// #[bw(try_calc(u32::try_from(instructions.len())))]
// instruction_count:u32,
// #[br(count=instruction_count)]
// instructions:Vec<super::instruction::TimedInstruction<Instruction>>
// }
// }
//serious code
#[binrw]
#[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)]
pub struct SegmentId(u32);
#[binrw]
#[brw(little)]
pub struct Segment{
state:simulation::State,
//#[bw(try_calc(u32::try_from(instructions.len())))]
//instruction_count:u32,
//#[br(count=instruction_count)]
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
pub instructions:Vec<TimedPhysicsInstruction>
}
//please remember that strafe ticks are implicit! 33% smaller bot files
#[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{
/// time of the first instruction in this segment.
time:Time,
instruction_count:u32,
/// How many total instructions in segments up to and including this segment
/// Alternatively, the id of the first instruction be in the _next_ segment
instructions_subtotal:u64,
block_id:BlockId,
}
pub struct StreamableBot<R:BinReaderExt>{
file:crate::file::File<R>,
//timeline:timeline::Timeline<SegmentId>,
segment_id_to_block_id:Vec<crate::file::BlockId>,
segment_map:Vec<SegmentInfo>,
}
impl<R:BinReaderExt> StreamableBot<R>{
pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{
Err(Error::InvalidHeader)
pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{
//assume the file seek is in the right place to start reading header
let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
let mut instructions_subtotal=0;
let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
instructions_subtotal+=instruction_count as u64;
SegmentInfo{
time:Time::raw(time),
instruction_count,
instructions_subtotal,
block_id,
}
}).collect();
Ok(Self{
file,
segment_map,
})
}
pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{
let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?;
let mut block=self.file.block_reader(block_id).map_err(Error::File)?;
let segment=block.read_le().map_err(Error::InvalidSegment)?;
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
}
pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{
let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
&self.segment_map[start..=end]
}
// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
// // TODO: This is off by one, both should be one less
// let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
// let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
// &self.segment_map[start..=end]
// }
fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
for _ in 0..segment_info.instruction_count{
let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
}
Ok(())
}
pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::with_capacity(segment_info.instruction_count as usize),
};
self.append_to_segment(segment_info,&mut segment)?;
Ok(segment)
}
pub fn read_all(&mut self)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::new(),
};
for i in 0..self.segment_map.len(){
let segment_info=self.segment_map[i];
self.append_to_segment(segment_info,&mut segment)?;
}
Ok(segment)
}
}
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
// decide which instructions to put in which segment
// write segment 1 to block 1
// write segment N to block 2
// write rest of segments
// 1 2 3 4 5
// becomes
// [1 5] 2 3 4
struct SegmentHeaderInfo{
time:Time,
instruction_count:u32,
range:core::ops::Range<usize>
}
let mut segment_header_infos=Vec::new();
let mut raw_segments=std::io::Cursor::new(Vec::new());
// block info
let mut start_time=Time::ZERO;
let mut start_position=raw_segments.position() as usize;
let mut instruction_count=0;
let mut last_position=start_position;
let mut iter=instructions.into_iter();
macro_rules! collect_instruction{
($instruction:expr)=>{
let time=$instruction.time;
let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
instruction_count+=1;
let position=raw_segments.position() as usize;
// exceeds max block size
if MAX_BLOCK_SIZE<position-last_position{
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..last_position,
});
start_position=last_position;
instruction_count=0;
start_time=time;
}
last_position=position;
}
}
// unroll one loop iteration to grab the starting time
if let Some(instruction)=iter.next(){
start_time=instruction.time;
collect_instruction!(instruction);
}
for instruction in iter{
collect_instruction!(instruction);
}
//last block, whatever size it happens to be
{
let final_position=raw_segments.position() as usize;
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..final_position,
});
}
// drop cursor
let raw_segments=raw_segments.into_inner();
let num_segments=segment_header_infos.len();
// segments list is in chronological order
let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
time:time.get(),
instruction_count,
block_id,
};
let segments=if 2<num_segments{
let mut segments=Vec::with_capacity(num_segments);
// segment 1 is second block
if let Some(seg)=segment_header_infos.first(){
segments.push(make_segment_header(BlockId::new(1),seg));
}
// rest of segments start at fourth block
for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
make_segment_header(BlockId::new(3+i as u32),seg);
}
// last segment is third block
if let Some(seg)=segment_header_infos.last(){
segments.push(make_segment_header(BlockId::new(2),seg));
}
segments
}else{
// all segments in order
segment_header_infos.iter().enumerate().map(|(i,seg)|
make_segment_header(BlockId::new(1+i as u32),seg)
).collect()
};
let header=Header{
physics_version,
num_segments:num_segments as u32,
segments,
};
// map header is +1
let block_count=1+num_segments as u32;
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
// block_location is one longer than block_count
let mut block_location=Vec::with_capacity(1+block_count as usize);
//probe header length
let mut bot_header_data=Vec::new();
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
// the first block location is the map header
block_location.push(offset);
offset+=bot_header_data.len() as u64;
block_location.push(offset);
// priming includes file header + first 3 blocks [bot header, first segment, last segment]
let priming=if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
let priming=offset;
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
priming
}else{
// all segments in order
for seg in &segment_header_infos{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
offset
};
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Bot,
version:VERSION,
priming,
resource:0,
block_count,
block_location,
};
// write file header
writer.write_le(&file_header).map_err(Error::InvalidData)?;
// write bot header
writer.write(&bot_header_data).map_err(Error::IO)?;
// write blocks
if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
}else{
// all segments in order
for seg in segment_header_infos{
writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
}
}
Ok(())
}

View File

@ -7,21 +7,29 @@ pub enum Error{
/*
BLOCK_DEMO_HEADER:
u128 map_resource_id
u64 map_header_block_id
u32 num_maps
for map_id in 0..num_maps{
i64 simulation_time
u128 map_resource_id
u64 map_header_block_id
}
u32 num_bots
for bot_id in 0..num_bots{
i64 simulation_time
u128 bot_resource_id
u64 bot_header_block_id
}
//map loading timeline
//bot loading timeline
how to do worldstate for deathrun!?
- this is done in the client, there is no worldstate in the demo file
*/
pub struct StreamableDemo<R:BinReaderExt>{
map:Box<crate::map::StreamableMap<R>>,
map:Vec<crate::map::StreamableMap<R>>,
bots:Vec<crate::bot::StreamableBot<R>>,
}
impl<R:BinReaderExt> StreamableDemo<R>{

View File

@ -73,6 +73,16 @@ pub struct Header{
#[br(count=block_count+1)]
pub block_location:Vec<u64>,
}
impl Header{
pub const fn calculate_size(block_count:u32)->usize{
4 // fourcc
+4 // version
+8 // priming
+16 // resource
+4 // block_count
+(block_count as usize+1)*8 // block_location
}
}
#[binrw]
#[brw(little)]

View File

@ -86,6 +86,7 @@ for model_id in 0..num_models{
//if you hash the resource itself and set the first 8 bits to this, that's the resource uuid
#[binrw]
#[brw(little,repr=u8)]
#[repr(u8)]
enum ResourceType{
Mesh,
Texture,
@ -94,21 +95,6 @@ enum ResourceType{
//Video,
//Animation,
}
const RESOURCE_TYPE_VARIANT_COUNT:u8=2;
#[binrw]
#[brw(little)]
struct ResourceId(u128);
impl ResourceId{
fn resource_type(&self)->Option<ResourceType>{
let discriminant=self.0 as u8;
//TODO: use this when it is stabilized https://github.com/rust-lang/rust/issues/73662
//if (discriminant as usize)<std::mem::variant_count::<ResourceType>(){
match discriminant<RESOURCE_TYPE_VARIANT_COUNT{
true=>Some(unsafe{std::mem::transmute::<u8,ResourceType>(discriminant)}),
false=>None,
}
}
}
struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>,
@ -135,11 +121,6 @@ struct ResourceBlockHeader{
resource:ResourceType,
id:BlockId,
}
#[binrw]
#[brw(little)]
struct ResourceExternalHeader{
resource_uuid:ResourceId,
}
#[binrw]
#[brw(little)]
@ -411,28 +392,26 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
attributes:map.attributes.into_iter().map(Into::into).collect(),
render_configs:map.render_configs.into_iter().map(Into::into).collect(),
};
let mut file_header=crate::file::Header{
//probe header length
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
// calculate final file header
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
offset+=map_header_data.len() as u64;
// priming includes map header
let priming=offset;
for position in &mut block_location{
*position+=offset;
}
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Map,
version:0,
priming:0,
priming,
resource:0,
block_count,
block_location,
};
//probe header length
let mut file_header_data=Vec::new();
binrw::BinWrite::write_le(&file_header,&mut std::io::Cursor::new(&mut file_header_data)).map_err(Error::InvalidData)?;
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
//update file header according to probe data
let mut offset=file_header_data.len() as u64;
file_header.priming=offset;
file_header.block_location[0]=offset;
offset+=map_header_data.len() as u64;
for position in &mut file_header.block_location[1..]{
*position+=offset;
}
//write (updated) file header
writer.write_le(&file_header).map_err(Error::InvalidData)?;

View File

@ -1,3 +1,9 @@
pub const fn flag(b:bool,mask:u8)->u8{
(-(b as i8) as u8)&mask
}
pub fn bool_from_u8(value:u8)->bool{
value!=0
}
pub fn bool_into_u8(value:&bool)->u8{
*value as u8
}

View File

@ -38,6 +38,23 @@ pub struct Ratio64Vec2{
pub x:Ratio64,
pub y:Ratio64,
}
impl TryInto<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
type Error=RatioError;
fn try_into(self)->Result<strafesnet_common::integer::Ratio64Vec2,Self::Error>{
Ok(strafesnet_common::integer::Ratio64Vec2{
x:self.x.try_into()?,
y:self.y.try_into()?,
})
}
}
impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
fn from(value:strafesnet_common::integer::Ratio64Vec2)->Self{
Self{
x:value.x.into(),
y:value.y.into(),
}
}
}
pub type Angle32=i32;
pub type Planar64=i64;

View File

@ -1,7 +1,9 @@
mod common;
pub mod aabb;
pub mod model;
pub mod mouse;
pub mod integer;
pub mod physics;
pub mod gameplay_modes;
pub mod gameplay_style;
pub mod gameplay_attributes;

View File

@ -0,0 +1,25 @@
use super::integer::Time;
#[binrw::binrw]
#[brw(little)]
pub struct MouseState{
pub pos:[i32;2],
pub time:Time,
}
impl<T> Into<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn into(self)->strafesnet_common::mouse::MouseState<T>{
strafesnet_common::mouse::MouseState{
pos:self.pos.into(),
time:strafesnet_common::integer::Time::raw(self.time),
}
}
}
impl<T> From<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn from(value:strafesnet_common::mouse::MouseState<T>)->Self{
Self{
pos:value.pos.to_array(),
time:value.time.get(),
}
}
}

View File

@ -0,0 +1,156 @@
use super::integer::Time;
use super::common::{bool_from_u8,bool_into_u8};
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::TimeInner>;
#[binrw::binrw]
#[brw(little)]
pub struct TimedInstruction{
pub time:Time,
pub instruction:Instruction,
}
impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<TimedPhysicsInstruction,Self::Error>{
Ok(strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::raw(self.time),
instruction:self.instruction.try_into()?,
})
}
}
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::physics::InstructionConvert;
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
Ok(Self{
time:value.time.get(),
instruction:value.instruction.try_into()?,
})
}
}
#[binrw::binrw]
#[brw(little)]
pub enum Instruction{
#[brw(magic=0u8)]
ReplaceMouse{
m0:super::mouse::MouseState,
m1:super::mouse::MouseState
},
#[brw(magic=1u8)]
SetNextMouse(super::mouse::MouseState),
#[brw(magic=2u8)]
SetMoveRight(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=3u8)]
SetMoveUp(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=4u8)]
SetMoveBack(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=5u8)]
SetMoveLeft(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=6u8)]
SetMoveDown(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=7u8)]
SetMoveForward(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=8u8)]
SetJump(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=9u8)]
SetZoom(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=10u8)]
Reset,
#[brw(magic=11u8)]
Restart(super::gameplay_modes::ModeId),
#[brw(magic=12u8)]
Spawn(super::gameplay_modes::ModeId,super::gameplay_modes::StageId),
#[brw(magic=13u8)]
PracticeFly,
#[brw(magic=14u8)]
SetSensitivity(super::integer::Ratio64Vec2),
#[brw(magic=255u8)]
Idle,
}
#[derive(Debug)]
pub enum InstructionConvert{
/// This is an instruction that can be dropped when serializing
DropInstruction,
}
impl std::fmt::Display for InstructionConvert{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for InstructionConvert{}
impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<strafesnet_common::physics::Instruction,Self::Error>{
Ok(match self{
Instruction::ReplaceMouse{m0,m1}=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
Instruction::SetNextMouse(m)=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m.into())),
Instruction::SetMoveRight(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state.into())),
Instruction::SetMoveUp(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state.into())),
Instruction::SetMoveBack(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state.into())),
Instruction::SetMoveLeft(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state.into())),
Instruction::SetMoveDown(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state.into())),
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
strafesnet_common::gameplay_modes::ModeId::new(mode_id),
strafesnet_common::gameplay_modes::StageId::new(stage_id),
)),
Instruction::PracticeFly=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly),
Instruction::SetSensitivity(sensitivity)=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity.try_into()?)),
Instruction::Idle=>strafesnet_common::physics::Instruction::Idle,
})
}
}
impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
type Error=InstructionConvert;
fn try_from(value:strafesnet_common::physics::Instruction)->Result<Self,Self::Error>{
match value{
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0,m1})=>Ok(Instruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m))=>Ok(Instruction::SetNextMouse(m.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state))=>Ok(Instruction::SetMoveRight(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state))=>Ok(Instruction::SetMoveUp(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state))=>Ok(Instruction::SetMoveBack(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state))=>Ok(Instruction::SetMoveLeft(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state))=>Ok(Instruction::SetMoveDown(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
mode_id.get(),
stage_id.get(),
)),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly)=>Ok(Instruction::PracticeFly),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity))=>Ok(Instruction::SetSensitivity(sensitivity.into())),
strafesnet_common::physics::Instruction::Idle=>Ok(Instruction::Idle),
}
}
}

2
lib/snf/src/session.rs Normal file
View File

@ -0,0 +1,2 @@
// a session is a recording of the client's inputs
// which should deterministically recreate a bot or whatever the client did

37
map-tool/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "map-tool"
version = "1.7.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
clap = { version = "4.4.2", features = ["derive"] }
flate2 = "1.0.27"
futures = "0.3.31"
image = "0.25.2"
image_dds = "0.7.1"
lazy-regex = "3.1.0"
rbx_asset = { version = "0.2.5", registry = "strafesnet" }
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.0", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../lib/deferred_loader", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.6.0"
vmdl = "0.2.0"
vmt-parser = "0.2.0"
vpk = "0.2.0"
vtf = "0.3.0"
#[profile.release]
#lto = true
#strip = true
#codegen-units = 1

23
map-tool/LICENSE Normal file
View File

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

2
map-tool/README.md Normal file
View File

@ -0,0 +1,2 @@
# map-tool

30
map-tool/src/main.rs Normal file
View File

@ -0,0 +1,30 @@
mod roblox;
mod source;
use clap::{Parser,Subcommand};
use anyhow::Result as AResult;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands{
#[command(flatten)]
Roblox(roblox::Commands),
#[command(flatten)]
Source(source::Commands),
}
#[tokio::main]
async fn main()->AResult<()>{
let cli=Cli::parse();
match cli.command{
Commands::Roblox(commands)=>commands.run().await,
Commands::Source(commands)=>commands.run().await,
}
}

431
map-tool/src/roblox.rs Normal file
View File

@ -0,0 +1,431 @@
use std::path::{Path,PathBuf};
use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt;
const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)]
pub enum Commands{
RobloxToSNF(RobloxToSNFSubcommand),
DownloadAssets(DownloadAssetsSubcommand),
}
#[derive(Args)]
pub struct RobloxToSNFSubcommand {
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
}
#[derive(Args)]
pub struct DownloadAssetsSubcommand{
#[arg(required=true)]
roblox_files:Vec<PathBuf>,
// #[arg(long)]
// cookie_file:Option<String>,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
Commands::DownloadAssets(subcommand)=>download_assets(
subcommand.roblox_files,
rbx_asset::cookie::Cookie::new("".to_string()),
).await,
}
}
}
#[allow(unused)]
#[derive(Debug)]
enum LoadDomError{
IO(std::io::Error),
Binary(rbx_binary::DecodeError),
Xml(rbx_xml::DecodeError),
UnknownFormat,
}
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
let mut first_8=[0u8;8];
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
input.rewind().map_err(LoadDomError::IO)?;
match &first_8{
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
_=>Err(LoadDomError::UnknownFormat),
}
}
/* The ones I'm interested in:
Beam.Texture
Decal.Texture
FileMesh.MeshId
FileMesh.TextureId
MaterialVariant.ColorMap
MaterialVariant.MetalnessMap
MaterialVariant.NormalMap
MaterialVariant.RoughnessMap
MeshPart.MeshId
MeshPart.TextureID
ParticleEmitter.Texture
Sky.MoonTextureId
Sky.SkyboxBk
Sky.SkyboxDn
Sky.SkyboxFt
Sky.SkyboxLf
Sky.SkyboxRt
Sky.SkyboxUp
Sky.SunTextureId
SurfaceAppearance.ColorMap
SurfaceAppearance.MetalnessMap
SurfaceAppearance.NormalMap
SurfaceAppearance.RoughnessMap
SurfaceAppearance.TexturePack
*/
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(property){
let url:&str=content.as_ref();
if let Ok(asset_id)=url.parse(){
content_list.insert(asset_id);
}else{
println!("Content failed to parse into AssetID: {:?}",content);
}
}else{
println!("property={} does not exist for class={}",property,object.class.as_str());
}
}
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=tokio::fs::File::open(path).await?;
let mut data=Vec::new();
file.read_to_end(&mut data).await?;
Ok(Cursor::new(data))
}
#[derive(Default)]
struct UniqueAssets{
meshes:HashSet<RobloxAssetId>,
unions:HashSet<RobloxAssetId>,
textures:HashSet<RobloxAssetId>,
}
impl UniqueAssets{
fn collect(&mut self,object:&Instance){
match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content_id(&mut self.textures,object,"TextureID");
accumulate_content_id(&mut self.meshes,object,"MeshId");
},
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Sky"=>{
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
accumulate_content_id(&mut self.textures,object,"SunTextureId");
},
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
_=>(),
}
}
}
#[allow(unused)]
#[derive(Debug)]
enum UniqueAssetError{
IO(std::io::Error),
LoadDom(LoadDomError),
}
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
// read entire file
let mut assets=UniqueAssets::default();
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
for object in dom.into_raw().1.into_values(){
assets.collect(&object);
}
Ok(assets)
}
enum DownloadType{
Texture(RobloxAssetId),
Mesh(RobloxAssetId),
Union(RobloxAssetId),
}
impl DownloadType{
fn path(&self)->PathBuf{
match self{
DownloadType::Texture(asset_id)=>format!("downloaded_textures/{}",asset_id.0.to_string()).into(),
DownloadType::Mesh(asset_id)=>format!("meshes/{}",asset_id.0.to_string()).into(),
DownloadType::Union(asset_id)=>format!("unions/{}",asset_id.0.to_string()).into(),
}
}
fn asset_id(&self)->u64{
match self{
DownloadType::Texture(asset_id)=>asset_id.0,
DownloadType::Mesh(asset_id)=>asset_id.0,
DownloadType::Union(asset_id)=>asset_id.0,
}
}
}
enum DownloadResult{
Cached(PathBuf),
Data(Vec<u8>),
Failed,
}
#[derive(Default,Debug)]
struct Stats{
total_assets:u32,
cached_assets:u32,
downloaded_assets:u32,
failed_downloads:u32,
timed_out_downloads:u32,
}
async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{
stats.total_assets+=1;
let download_instruction=download_instruction;
// check if file exists on disk
let path=download_instruction.path();
if tokio::fs::try_exists(path.as_path()).await?{
stats.cached_assets+=1;
return Ok(DownloadResult::Cached(path));
}
let asset_id=download_instruction.asset_id();
// if not, download file
let mut retry=0;
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
let mut backoff=1000f32;
loop{
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id,
version:None,
}).await;
match asset_result{
Ok(asset_result)=>{
stats.downloaded_assets+=1;
tokio::fs::write(path,&asset_result).await?;
break Ok(DownloadResult::Data(asset_result));
},
Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
if scwuab.status_code.as_u16()==429{
if retry==12{
println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1;
break Ok(DownloadResult::Failed);
}
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
backoff*=BACKOFF_MUL;
retry+=1;
}else{
stats.failed_downloads+=1;
println!("weird scuwab error: {scwuab:?}");
break Ok(DownloadResult::Failed);
}
},
Err(e)=>{
stats.failed_downloads+=1;
println!("sadly error: {e}");
break Ok(DownloadResult::Failed);
},
}
}
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Image error {0:?}")]
Image(#[from]image::ImageError),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
}
async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
let data=match download_result{
DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
DownloadResult::Data(data)=>data,
DownloadResult::Failed=>return Ok(()),
};
// image::ImageFormat::Png
// image::ImageFormat::Jpeg
let image=image::load_from_memory(&data)?.to_rgba8();
// pick format
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds=image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
let file_name=format!("textures/{}.dds",asset_id.0);
let mut file=std::fs::File::create(file_name)?;
dds.write(&mut file)?;
Ok(())
}
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("downloaded_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
tokio::fs::create_dir_all("unions"),
)?;
// use mpsc
let thread_limit=std::thread::available_parallelism()?.get();
let (send_assets,mut recv_assets)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT);
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
// map decode dispatcher
// read files multithreaded
// produce UniqueAssetsResult per file
tokio::spawn(async move{
// move send so it gets dropped when all maps have been decoded
// closing the channel
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_assets.clone();
tokio::spawn(async move{
let result=unique_assets(path.as_path()).await;
_=send.send(result).await;
drop(permit);
});
}
});
// download manager
// insert into global unique assets guy
// add to download queue if the asset is globally unique and does not already exist on disk
let mut stats=Stats::default();
let context=rbx_asset::cookie::CookieContext::new(cookie);
let mut globally_unique_assets=UniqueAssets::default();
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
// SLOW MODE:
// acquire all permits
// drop all permits
// pop one job
// if it succeeds go into fast mode
// FAST MODE:
// acquire one permit
// pop a job
let download_thread=tokio::spawn(async move{
while let Some(result)=recv_assets.recv().await{
let unique_assets=match result{
Ok(unique_assets)=>unique_assets,
Err(e)=>{
println!("error: {e:?}");
continue;
},
};
for texture_id in unique_assets.textures{
if globally_unique_assets.textures.insert(texture_id){
let data=download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?;
send_texture.send((texture_id,data)).await?;
}
}
for mesh_id in unique_assets.meshes{
if globally_unique_assets.meshes.insert(mesh_id){
download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?;
}
}
for union_id in unique_assets.unions{
if globally_unique_assets.unions.insert(union_id){
download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?;
}
}
}
dbg!(stats);
Ok::<(),anyhow::Error>(())
});
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
tokio::spawn(async move{
let result=convert_texture(asset_id,download_result).await;
drop(permit);
result.unwrap();
});
}
download_thread.await??;
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
Ok(())
}
#[derive(Debug)]
#[allow(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
RobloxRead(strafesnet_rbx_loader::ReadError),
RobloxLoad(strafesnet_rbx_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?;
let model=strafesnet_rbx_loader::read(
std::io::Cursor::new(entire_file)
).map_err(ConvertError::RobloxRead)?;
let mut place=model.into_place();
place.run_scripts();
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
}
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::spawn(async move{
let result=convert_to_snf(path.as_path(),output_folder).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}

464
map-tool/src/source.rs Normal file
View File

@ -0,0 +1,464 @@
use std::path::{Path,PathBuf};
use std::borrow::Cow;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use futures::StreamExt;
use strafesnet_bsp_loader::loader::BspFinder;
use strafesnet_deferred_loader::loader::Loader;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
#[derive(Subcommand)]
pub enum Commands{
SourceToSNF(SourceToSNFSubcommand),
ExtractTextures(ExtractTexturesSubcommand),
VPKContents(VPKContentsSubcommand),
BSPContents(BSPContentsSubcommand),
}
#[derive(Args)]
pub struct SourceToSNFSubcommand {
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct ExtractTexturesSubcommand{
#[arg(required=true)]
bsp_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct VPKContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
#[derive(Args)]
pub struct BSPContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
}
}
}
enum VMTContent{
VMT(String),
VTF(String),
Patch(vmt_parser::material::PatchMaterial),
Unsupported,//don't want to deal with whatever vmt variant
Unresolved,//could not locate a texture because of vmt content
}
impl VMTContent{
fn vtf(opt:Option<String>)->Self{
match opt{
Some(s)=>Self::VTF(s),
None=>Self::Unresolved,
}
}
}
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
//just grab some texture from somewhere for now
match material{
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
_=>unreachable!(),
}
}
#[derive(Debug,thiserror::Error)]
enum GetVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Utf8 error {0:?}")]
Utf8(#[from]std::str::Utf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt not found")]
NotFound,
}
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
//decode vmt and then write
let vmt_str=core::str::from_utf8(&vmt_data)?;
let material=vmt_parser::from_str(vmt_str)?;
//println!("vmt material={:?}",material);
Ok(material)
}
#[derive(Debug,thiserror::Error)]
enum LoadVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("GetVMT error {0:?}")]
GetVMT(#[from]GetVMTError),
#[error("FromUtf8 error {0:?}")]
FromUtf8(#[from]std::string::FromUtf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt unsupported")]
Unsupported,
#[error("Vmt unresolved")]
Unresolved,
#[error("Vmt not found")]
NotFound,
}
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
match get_some_texture(material){
VMTContent::VMT(mut s)=>{
s.make_ascii_lowercase();
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
},
VMTContent::VTF(s)=>{
let mut texture_file_name=PathBuf::from("materials");
texture_file_name.push(s);
texture_file_name.set_extension("vtf");
texture_file_name.as_mut_os_str().make_ascii_lowercase();
Ok(finder.find(texture_file_name.to_str().unwrap())?)
},
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
mat.resolve(|search_name|{
let name_lowercase=search_name.to_lowercase();
match finder.find(&name_lowercase)?{
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
None=>Err(LoadVMTError::NotFound),
}
})?
),
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
}
}
fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
let mut texture_file_name=PathBuf::from("materials");
//lower case
texture_file_name.push(texture_name);
texture_file_name.as_mut_os_str().make_ascii_lowercase();
//remove stem and search for both vtf and vmt files
let stem=texture_file_name.file_stem().unwrap().to_owned();
texture_file_name.pop();
texture_file_name.push(stem);
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
return Ok(Some(stuff));
}
// search for both vmt,vtf
let mut texture_file_name_vmt=texture_file_name.clone();
texture_file_name_vmt.set_extension("vmt");
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
match get_vmt_result{
Ok(material)=>{
let vmt_result=recursive_vmt_loader(finder,material);
match vmt_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
// try looking for vtf
let mut texture_file_name_vtf=texture_file_name.clone();
texture_file_name_vtf.set_extension("vtf");
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
match get_vtf_result{
Ok(material)=>{
let vtf_result=recursive_vmt_loader(finder,material);
match vtf_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
Ok(None)
}
#[derive(Debug,thiserror::Error)]
enum ExtractTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("MeshLoad error {0:?}")]
MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
#[error("Load VMT error {0:?}")]
LoadVMT(#[from]LoadVMTError),
}
async fn gimme_them_textures(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
let bsp=loader_bsp.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
for texture in bsp.textures(){
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
}
let mut mesh_deferred_loader=MeshDeferredLoader::new();
for prop in bsp.static_props(){
mesh_deferred_loader.acquire_mesh_id(prop.model());
}
let finder=BspFinder{
bsp:&loader_bsp,
vpks:vpk_list
};
let mut mesh_loader=strafesnet_bsp_loader::loader::ModelLoader::new(finder);
// load models and collect requested textures
for model_path in mesh_deferred_loader.into_indices(){
let model:vmdl::Model=match mesh_loader.load(model_path){
Ok(model)=>model,
Err(e)=>{
println!("Model={model_path} Load model error: {e}");
continue;
},
};
for texture in model.textures(){
for search_path in &texture.search_paths{
let mut path=PathBuf::from(search_path.as_str());
path.push(texture.name.as_str());
let path=path.to_str().unwrap().to_owned();
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
}
}
}
for texture_path in texture_deferred_loader.into_indices(){
match load_texture(finder,&texture_path){
Ok(Some(texture))=>send_texture.send(
(texture.into_owned(),texture_path.into_owned())
).await.unwrap(),
Ok(None)=>(),
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
}
}
Ok(())
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Vtf error {0:?}")]
Vtf(#[from]vtf::Error),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
}
async fn convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<Path>)->Result<(),ConvertTextureError>{
let image=vtf::from_bytes(&texture)?.highres_image.decode(0)?.to_rgba8();
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds = image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
//write dds
let mut dest=PathBuf::from("textures");
dest.push(write_file_name);
dest.set_extension("dds");
std::fs::create_dir_all(dest.parent().unwrap())?;
let mut writer=std::io::BufWriter::new(std::fs::File::create(dest)?);
dds.write(&mut writer)?;
Ok(())
}
async fn read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
futures::stream::iter(vpk_paths).map(|vpk_path|async{
// idk why it doesn't want to pass out the errors but this is fatal anyways
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
})
.buffer_unordered(thread_limit)
.collect().await
}
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("extracted_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
)?;
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
let mut it=paths.into_iter();
let extract_thread=tokio::spawn(async move{
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_texture.clone();
tokio::spawn(async move{
let result=gimme_them_textures(&path,vpk_list,send).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
}
});
}
});
// convert images
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
// TODO: dedup dest?
tokio::spawn(async move{
let result=convert_texture(data,dest).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
extract_thread.await?;
_=SEM.acquire_many(thread_limit as u32).await?;
Ok(())
}
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
let vpk_index=vpk::VPK::read(&vpk_path)?;
for (label,entry) in vpk_index.tree.into_iter(){
println!("vpk label={} entry={:?}",label,entry);
}
Ok(())
}
fn bsp_contents(path:PathBuf)->AResult<()>{
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
println!("file_name={:?}",file_name);
}
Ok(())
}
#[derive(Debug)]
#[allow(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
BspRead(strafesnet_bsp_loader::ReadError),
BspLoad(strafesnet_bsp_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?;
let bsp=strafesnet_bsp_loader::read(
std::io::Cursor::new(entire_file)
).map_err(ConvertError::BspRead)?;
let map=bsp.to_snf(LoadFailureMode::DefaultToNone,vpk_list).map_err(ConvertError::BspLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
}
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::spawn(async move{
let result=convert_to_snf(path.as_path(),vpk_list,output_folder).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}

View File

@ -1,6 +1,6 @@
[package]
name = "strafe-client"
version = "0.10.5"
version = "0.11.0"
edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "Custom"
@ -9,24 +9,24 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
user-install=[] # as opposed to portable install
default = ["snf"]
snf = ["dep:strafesnet_snf"]
source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
configparser = "3.0.2"
ddsfile = "0.5.1"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1"
pollster = "0.4.0"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true }
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
strafesnet_roblox_bot_file = { version = "0.2.0", registry = "strafesnet" }
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "23.0.1"
wgpu = "24.0.0"
winit = "0.30.7"

68
strafe-client/src/app.rs Normal file
View File

@ -0,0 +1,68 @@
use crate::window::Instruction;
use strafesnet_common::integer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::TimeInner as SessionTimeInner;
pub struct App<'a>{
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>,
}
impl<'a> App<'a>{
pub fn new(
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTimeInner>>,
)->App<'a>{
Self{
root_time,
window_thread,
}
}
fn send_timed_instruction(&mut self,instruction:Instruction){
let time=integer::Time::from_nanos(self.root_time.elapsed().as_nanos() as i64);
self.window_thread.send(TimedInstruction{time,instruction}).unwrap();
}
}
impl winit::application::ApplicationHandler for App<'_>{
fn resumed(&mut self,_event_loop:&winit::event_loop::ActiveEventLoop){
//
}
fn window_event(
&mut self,
event_loop:&winit::event_loop::ActiveEventLoop,
_window_id:winit::window::WindowId,
event:winit::event::WindowEvent,
){
match event{
winit::event::WindowEvent::KeyboardInput{
event:winit::event::KeyEvent{
logical_key:winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),
state:winit::event::ElementState::Pressed,
..
},
..
}
|winit::event::WindowEvent::CloseRequested=>{
event_loop.exit();
},
_=>(),
}
self.send_timed_instruction(Instruction::WindowEvent(event));
}
fn device_event(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop,
_device_id:winit::event::DeviceId,
event:winit::event::DeviceEvent,
){
self.send_timed_instruction(Instruction::DeviceEvent(event));
}
fn about_to_wait(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop
){
self.send_timed_instruction(Instruction::WindowEvent(winit::event::WindowEvent::RedrawRequested));
}
}

View File

@ -1,5 +1,9 @@
use std::io::Read;
#[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[allow(dead_code)]
#[derive(Debug)]
pub enum ReadError{
#[cfg(feature="roblox")]
@ -10,6 +14,8 @@ pub enum ReadError{
StrafesNET(strafesnet_snf::Error),
#[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")]
StrafesNETBot(strafesnet_snf::bot::Error),
Io(std::io::Error),
UnknownFileFormat,
}
@ -20,37 +26,52 @@ impl std::fmt::Display for ReadError{
}
impl std::error::Error for ReadError{}
pub enum DataStructure{
pub enum ReadFormat{
#[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::Model),
#[cfg(feature="source")]
Source(strafesnet_bsp_loader::Bsp),
#[cfg(feature="snf")]
StrafesNET(strafesnet_common::map::CompleteMap),
SNFM(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
SNFB(strafesnet_snf::bot::Segment),
}
pub fn read<R:Read+std::io::Seek>(input:R)->Result<DataStructure,ReadError>{
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,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..4]{
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned();
// reading the entire file is way faster than round tripping the disk constantly
let mut entire_file=Vec::new();
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
let cursor=std::io::Cursor::new(entire_file);
match peek.as_slice(){
#[cfg(feature="roblox")]
b"<rob"=>Ok(DataStructure::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)),
#[cfg(feature="source")]
b"VBSP"=>Ok(DataStructure::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)),
#[cfg(feature="snf")]
b"SNFM"=>Ok(DataStructure::StrafesNET(
strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
b"SNFM"=>Ok(ReadFormat::SNFM(
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)?
)),
#[cfg(feature="snf")]
b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)?
)),
_=>Err(ReadError::UnknownFileFormat),
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum LoadError{
ReadError(ReadError),
File(std::io::Error),
Io(std::io::Error),
#[cfg(feature="roblox")]
LoadRoblox(strafesnet_rbx_loader::LoadError),
#[cfg(feature="source")]
LoadSource(strafesnet_bsp_loader::LoadError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -59,86 +80,32 @@ impl std::fmt::Display for LoadError{
}
impl std::error::Error for LoadError{}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::CompleteMap,LoadError>{
pub enum LoadFormat{
#[cfg(any(feature="snf",feature="roblox",feature="source"))]
Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
Bot(strafesnet_snf::bot::Segment),
}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
//blocking because it's simpler...
let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{
#[cfg(feature="snf")]
DataStructure::StrafesNET(map)=>Ok(map),
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)),
#[cfg(feature="snf")]
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")]
DataStructure::Roblox(model)=>{
ReadFormat::Roblox(model)=>{
let mut place=model.into_place();
place.run_scripts();
let mut loader=strafesnet_deferred_loader::roblox_legacy();
let (texture_loader,mesh_loader)=loader.get_inner_mut();
let map_step1=strafesnet_rbx_loader::convert(
&place,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
);
let meshpart_meshes=mesh_loader.load_meshes().map_err(LoadError::Io)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(
meshpart_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_rbx_loader::data::RobloxMeshBytes::new(loader_model.get()))
)
);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
)
);
Ok(map)
Ok(LoadFormat::Map(
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?
))
},
#[cfg(feature="source")]
DataStructure::Source(bsp)=>{
let mut loader=strafesnet_deferred_loader::source_legacy();
let (texture_loader,mesh_loader)=loader.get_inner_mut();
let map_step1=strafesnet_bsp_loader::convert(
&bsp,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
);
let prop_meshes=mesh_loader.load_meshes(bsp.as_ref());
let map_step2=map_step1.add_prop_meshes(
//the type conflagulator 9000
prop_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_bsp_loader::data::ModelData{
mdl:strafesnet_bsp_loader::data::MdlData::new(loader_model.mdl.get()),
vtx:strafesnet_bsp_loader::data::VtxData::new(loader_model.vtx.get()),
vvd:strafesnet_bsp_loader::data::VvdData::new(loader_model.vvd.get()),
})
),
|name|texture_loader.acquire_render_config_id(name),
);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
),
);
Ok(map)
},
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(
bsp.to_snf(LoadFailureMode::DefaultToNone,&[]).map_err(LoadError::LoadSource)?
)),
}
}

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