Compare commits

...

316 commits

Author SHA1 Message Date
IQuant
f3b3e81c01 Restore world decoder, handle air pixels properly. 2025-10-12 17:12:39 +03:00
IQuant
8d18843147 Fix world encode 2025-10-12 16:36:59 +03:00
IQuant
7f0d7fbb53 Use correct name for "operator new" 2025-10-12 16:07:39 +03:00
IQuant
43dcabe601 Noita heap stuff 2025-10-12 15:41:45 +03:00
IQuant
ea6a17323a Update deps 2025-10-06 16:07:07 +03:00
IQuant
710579d963 Fix ewext build maybe 2025-10-06 16:01:08 +03:00
IQuant
629609ac13 Move step to ci_make_archives scripts 2025-10-06 15:42:52 +03:00
IQuant
48774b3308
Merge pull request #434 from merll002/patch-1
Create Linux (Lutris) install guide and automatic startup script
2025-10-06 15:34:26 +03:00
Leo
6eea42586d
Add step to copy Lutris start script to target 2025-10-03 22:42:39 +01:00
Leo
2f41505433
Change name 2025-10-03 22:39:21 +01:00
Leo
1415d40304
Upload easy launch script 2025-10-03 22:38:54 +01:00
Leo
a9b78c86ec
Fix typos and create Lutris install guide
Added Linux install guide and fixed some things
2025-10-03 22:05:06 +01:00
Jacob Birkett
20c74926e1 nix: packages: noita-proxy: meta: inherit manifest.package.description 2025-10-01 17:12:56 +03:00
Jacob Birkett
bd1d7d3f24 noita-proxy: cargo: package: more accurate description 2025-10-01 17:12:56 +03:00
Jacob Birkett
f549015047 nix: packages: noita-proxy: add desktopItems 2025-10-01 17:12:56 +03:00
Jacob Birkett
46a3673749 nix: shell: inherit inputs from noita-proxy 2025-10-01 17:12:56 +03:00
Jacob Birkett
be993008c3 nix+flake: packages+overlays: init noita-proxy 2025-10-01 17:12:56 +03:00
Jacob Birkett
876d3da91e flake: init with Rust devShell 2025-10-01 17:12:56 +03:00
Mikael Walhelm
b336f34fd0
Merge pull request #426 from RoenaltAstrophore/421
Add instructions to use proxy on MacOs
2025-09-01 19:06:37 -04:00
Audric Henquin
cf597064e4 421 Add instructions to use proxy on MacOs 2025-08-31 17:57:01 -04:00
bgkillas
f4f2020f87 fix ewext test and make some code nicer 2025-08-30 16:55:47 -04:00
bgkillas
b39be102a7 actually send chunks to proxy again 2025-08-30 13:48:04 -04:00
bgkillas
785e945790 fix windows build 2025-08-29 20:20:44 -04:00
bgkillas
19315ef268 use only 1 pixel type, add testing for world and fix some logic errors 2025-08-29 18:42:43 -04:00
bgkillas
2d617b900d remove some unused depends and update egui, still cant figure out how to update steamworks i think this was setup in a way they do not like or smth brain too tired to think of threading 2025-08-26 20:14:19 -04:00
bgkillas
720b9e2396 some more world sync work 2025-08-23 21:51:42 -04:00
bgkillas
38f8fe4c59 format 2025-08-22 22:01:08 -04:00
bgkillas
f55245f4f0 use rayon for world sync 2025-08-22 21:32:02 -04:00
bgkillas
05bd0736ff fix vanilla error and borrow error 2025-08-21 15:44:40 -04:00
bgkillas
f78070e1b5 try to implement world sync, prob made some mistakes but no crash is good enough for me 2025-08-20 23:01:53 -04:00
bgkillas
ec3857c6b9 some stdmap functions 2025-08-19 11:24:57 -04:00
bgkillas
fb56e2f1d9 make map length getter use length field 2025-08-18 14:54:18 -04:00
bgkillas
525b5dfba2 more probably useless things pog 2025-08-18 13:45:53 -04:00
bgkillas
c93f6dfd82 add cstring eq 2025-08-17 22:05:58 -04:00
bgkillas
52cd62ab15 add entity get with name 2025-08-17 18:16:15 -04:00
bgkillas
d8c64c4682 fix incorrectly named things and add vtable to component trait 2025-08-17 12:29:20 -04:00
bgkillas
4b0f5d15cc add some cstring constructors and give cstring data to Component trait 2025-08-17 11:57:51 -04:00
bgkillas
448ddcaeab create component is only borked not crash 2025-08-16 22:27:12 -04:00
bgkillas
49f6999bc4 use double ended iterators for component stuffs for testing purposes 2025-08-16 18:37:45 -04:00
bgkillas
fe386ac2ff more create component work, still crashes(i dont like component buffers type) 2025-08-15 18:03:01 -04:00
bgkillas
1919f38b09 use cast instead of as 2025-08-14 18:30:42 -04:00
bgkillas
a399aba66a implement (crashing) new component 2025-08-10 22:39:02 -04:00
bgkillas
200e156e42 add entity create new 2025-08-10 17:21:00 -04:00
bgkillas
6ea9d7d850 fix string being wrong 2025-07-25 00:13:19 -04:00
bgkillas
69f4eb30a2 add names field to mods thingy 2025-07-24 22:02:52 -04:00
bgkillas
5663b1ec04 change kill flag to be a boolean 2025-07-24 18:36:27 -04:00
bgkillas
3d4b65e329 fix some bad regex changing some field names 2025-07-24 15:15:41 -04:00
bgkillas
8bd7cd8475 make add tag take mutable tag mgr 2025-07-24 11:59:34 -04:00
bgkillas
9d822debff rename component manager to component buffer as thats what noiter calls it 2025-07-24 00:10:23 -04:00
bgkillas
80d15432d9 note the reverse vec in componentmanager 2025-07-23 23:53:13 -04:00
bgkillas
2c6e71a189 add inventory component to inventory struct 2025-07-23 20:11:13 -04:00
bgkillas
64b44826de fix new game 2025-07-23 16:57:13 -04:00
bgkillas
ab862a71a0 add more fields to inventory 2025-07-23 16:05:45 -04:00
bgkillas
3fdd72c363 make types a bit nicer 2025-07-23 15:18:41 -04:00
bgkillas
6b15e58202 get some more fields 2025-07-23 12:48:16 -04:00
bgkillas
397c5773b2 add tiny bit of info on mods 2025-07-23 00:43:58 -04:00
bgkillas
a1484eb171 deduce a bit of fields 2025-07-22 23:13:07 -04:00
bgkillas
a238941595 make types bit nicer 2025-07-22 21:42:09 -04:00
bgkillas
f2103c26ec make component name conversion to stdstring const time 2025-07-22 19:36:29 -04:00
bgkillas
8442ff32c0 add some more functions 2025-07-22 16:57:27 -04:00
bgkillas
af731bc94e add a bunch of component methods 2025-07-22 13:41:26 -04:00
bgkillas
9819ffb9a0 complete a bunch of entity functions 2025-07-22 12:45:49 -04:00
bgkillas
b7ccd4d9ec fix some crashes 2025-07-21 21:35:54 -04:00
bgkillas
0519d6ed64 add a bit more data to inventory struct, unwrap globals since they always exist 2025-07-21 19:31:01 -04:00
bgkillas
84ac39abf6 add inventory struct 2025-07-21 15:45:46 -04:00
bgkillas
474422fe28 fix size of comp data and log perf not working 2025-07-21 15:14:02 -04:00
bgkillas
78c2ed2772 fix platform, global stats and translation manager 2025-07-21 10:26:12 -04:00
bgkillas
3715df415a add filenames 2025-07-21 09:46:07 -04:00
bgkillas
95d9e3dd0d fix entity tag methods 2025-07-21 09:35:20 -04:00
bgkillas
8d7adf8df6 better component type data 2025-07-21 01:00:32 -04:00
bgkillas
a6cb4d67e8 add some tag methods 2025-07-21 00:17:43 -04:00
bgkillas
9b15f14306 fix some types crashing 2025-07-20 23:08:33 -04:00
bgkillas
2ce9ea0f08 add some more types 2025-07-20 18:20:35 -04:00
bgkillas
53a7808a47 organize some stuff better 2025-07-20 14:12:28 -04:00
bgkillas
0863b27021 add some functions to grabbed for ease of use 2025-07-20 13:21:00 -04:00
bgkillas
2476958246 steal some more types from noita utility box 2025-07-20 12:57:05 -04:00
bgkillas
263c79a2f0 add all components(with incomplete types) 2025-07-20 02:40:45 -04:00
bgkillas
3c68691de5 change some field names 2025-07-20 01:06:41 -04:00
bgkillas
76ab66167b remove tags.rs 2025-07-19 03:33:45 -04:00
bgkillas
fb58eaff2b try to get statuseffectdatacomponent 2025-07-19 03:31:05 -04:00
bgkillas
bd27ce28a3 more type info 2025-07-19 02:46:42 -04:00
bgkillas
d9769e4b24 more type info 2025-07-18 23:42:15 -04:00
bgkillas
d15ccb2f5d make get function for std map work 2025-07-18 19:08:52 -04:00
bgkillas
bd237869e7 simplify map and add more to stdstring 2025-07-18 16:40:04 -04:00
bgkillas
14e39dd687 fix tag iter, organize types 2025-07-18 13:00:59 -04:00
bgkillas
d31608e483 add name field for component, fix up tag type 2025-07-18 10:06:12 -04:00
bgkillas
33eff1dbaf correct size for entity tags 2025-07-17 17:42:20 -04:00
bgkillas
24005e3438 add tag structs 2025-07-17 15:44:04 -04:00
bgkillas
ef88d56a5b add some entity functions 2025-07-17 12:23:35 -04:00
bgkillas
1aa25d4782 add parent and enabled field 2025-07-17 10:26:56 -04:00
bgkillas
868e7f2ff6 more fields 2025-07-16 23:54:11 -04:00
bgkillas
4e80e84b8c add some mut methods 2025-07-16 21:05:18 -04:00
bgkillas
07c78b2944 add x,y,angle,scalex,scaley to entity 2025-07-16 20:14:33 -04:00
bgkillas
b5047ec7b3 make component iter instead of collecting to vec 2025-07-16 19:25:39 -04:00
bgkillas
660e933d15 component type work 2025-07-16 12:38:53 -04:00
bgkillas
a4eccf3980 fix size of entity being incorrect, add list of components in entitymanager 2025-07-16 00:04:08 -04:00
bgkillas
5d6c328029 add name to entity struct 2025-07-15 19:36:46 -04:00
bgkillas
d973f44399 add entity kill, fix get_entity 2025-07-15 17:00:47 -04:00
bgkillas
e9faa77b28 implement get_entity and get_entity_mut 2025-07-15 14:47:06 -04:00
bgkillas
2a7d3a5c46 optimize encode world more 2025-07-15 09:37:00 -04:00
bgkillas
3d530ca648 get all vtables, more ewext world data setup 2025-07-15 08:19:09 -04:00
bgkillas
29abf3f26f refresh sprites 2025-07-14 17:51:48 -04:00
bgkillas
48c765d3be update rodio again 2025-07-14 12:55:26 -04:00
bgkillas
067570672d refactor a bunch of things to get ready for rust world sync 2025-07-13 21:06:05 -04:00
bgkillas
4ff165b815 some more noita type definitions 2025-07-13 11:50:02 -04:00
bgkillas
58cac3ba81 update cpal/rodio 2025-07-12 15:41:59 -04:00
bgkillas
387ba449d8 make decode rayon-ized 2025-07-12 14:37:33 -04:00
bgkillas
4566f3848e fix liquid cell type being wrong, find cell vtable in memory, fix decode area to not use construct/remove cell ptrs 2025-07-12 14:31:29 -04:00
bgkillas
1d8ec90a9b make blob movement more natural 2025-07-12 10:38:28 -04:00
bgkillas
b4289221f2 add temp pixels to fill in gaps 2025-07-12 01:38:46 -04:00
bgkillas
65b526c8e4 make blob more natural 2025-07-12 01:25:23 -04:00
bgkillas
415ec5b74d attempt to make my own pixels, make blob movement a bit nicer 2025-07-11 16:00:12 -04:00
bgkillas
2ad7b5c92d remove another &mut for *mut, remove the 0ns sleep 2025-07-10 17:31:33 -04:00
bgkillas
56c16fbc12 fix blob guy and make blob guy movement more natural 2025-07-10 16:11:41 -04:00
bgkillas
685e4d6c9c update non user breaking dependencies 2025-07-10 15:27:36 -04:00
bgkillas
e445ffb516 make some nicer types, dont use &'static mut do to UB, use *mut instead of some &'static do to UB when object does mutate via noiter 2025-07-10 15:00:26 -04:00
bgkillas
3500537709 move stuff noita and world stuff to noita_api 2025-07-10 11:40:11 -04:00
bgkillas
ab306c6d4a prob bad idea but turn liquid into blob, differentiate liquid from sand 2025-07-10 00:18:56 -04:00
bgkillas
0fc362695f get displaced by world pixels 2025-07-09 22:54:49 -04:00
bgkillas
0ba90ef44c make debug prints not too big that they crash 2025-07-09 20:05:01 -04:00
bgkillas
1ab79d6cb7 read() less, dont use as_mut when as_ref is fine, simplify types by using & instead of *const where applicable 2025-07-09 19:27:27 -04:00
bgkillas
2d1ccfcb43 remove noita patcher 2025-07-09 18:56:10 -04:00
bgkillas
44b62f8d20 remove grid world ptr from init 2025-07-09 18:54:42 -04:00
bgkillas
f8c00de976 remove material list ptr from init 2025-07-09 18:38:52 -04:00
bgkillas
8abc51ddd9 find remove and construct ptrs better 2025-07-09 14:12:30 -04:00
bgkillas
9dd156bc41 lazily find construct and remove ptrs without noita patcher 2025-07-08 17:18:54 -04:00
bgkillas
d7d3e4ac31 dont have blob freeze when player dies/polies 2025-07-08 15:20:37 -04:00
bgkillas
a677bc1814 fix cloned chunks coord being off 2025-07-08 14:28:05 -04:00
bgkillas
a94f375ebe make color public, and make colour color because makes me sad 2025-07-08 14:14:36 -04:00
bgkillas
34c1e1f6c1 implement default for celldata 2025-07-08 14:10:06 -04:00
bgkillas
0f9e85ebf3 dont put null already null pointers 2025-07-08 13:42:31 -04:00
bgkillas
c14da4b23a make clone chunks give proper chunk coords 2025-07-08 13:14:48 -04:00
bgkillas
efdb38fcbe simplify debug mouse pos output slightly 2025-07-08 13:07:26 -04:00
bgkillas
a2f7a55dd2 make clone chunks give more data, move debug mouse pos to pws 2025-07-08 13:06:25 -04:00
bgkillas
8c1cdbac99 dont increase binary size by a megabyte since a bit silly 2025-07-08 11:49:35 -04:00
bgkillas
ad550622ad make types nicer, fix clone chunks 2025-07-08 11:43:28 -04:00
bgkillas
97e816888c make more stuff pub, add clone_chunks functions, add material list to pws 2025-07-08 10:24:37 -04:00
bgkillas
7b54962824 give cellvtable debug 2025-07-08 09:39:34 -04:00
bgkillas
85773154fd make known fields public 2025-07-08 09:35:19 -04:00
bgkillas
1ee7a2853f remove some allocations 2025-07-07 22:05:35 -04:00
bgkillas
0ff7b16db3 remove unused dependency 2025-07-07 21:14:25 -04:00
bgkillas
cb5a9b02cb remove a mut cast 2025-07-07 20:37:01 -04:00
bgkillas
389933fa1a "simplify" types 2025-07-07 16:16:32 -04:00
bgkillas
8c336250ef dont use lazycell 2025-07-07 15:11:31 -04:00
bgkillas
31a0197937 remove the keep self loaded thing since i dont know what it does and doesn't crash without so idk 2025-07-07 15:10:02 -04:00
bgkillas
5dfbf10c34 simplify checks 2025-07-07 15:00:17 -04:00
bgkillas
ad05004308 fix last and make chunk map an array 2025-07-07 14:55:49 -04:00
bgkillas
41d2d4f747 dont get chunk map from lua 2025-07-07 14:50:10 -04:00
bgkillas
c7dca15649 properly type chunk map ptr 2025-07-07 14:47:40 -04:00
bgkillas
62cd73bd96 make world ptr grid world 2025-07-07 14:33:55 -04:00
bgkillas
07bdcd64b3 use ntypes::CellData type for mat list ptr and blob ptr 2025-07-07 13:09:08 -04:00
bgkillas
d7bad41632 use an array instead of a pointer for pixel array 2025-07-07 12:07:46 -04:00
bgkillas
c524329cb4 remove a *mut as we dont mutate there 2025-07-07 11:50:28 -04:00
bgkillas
a126acdbb2 make decode look more like encode 2025-07-07 00:13:47 -04:00
bgkillas
ec00d2984c multi threaded chunk encode 2025-07-07 00:08:11 -04:00
bgkillas
bb02a1a5de magical fixes 2025-07-06 20:34:09 -04:00
bgkillas
44d72d78f4 Automated commit: v1.6.2 2025-07-06 16:46:41 -04:00
bgkillas
9f30a1c1b4 remove debug prints 2025-07-06 12:40:00 -04:00
bgkillas
8917668475 thing worky now :), no more thinky for me 2025-07-05 21:19:25 -04:00
bgkillas
2fed3cc807 revert some things 2025-07-04 17:03:49 -04:00
bgkillas
dcd7c049ea Automated commit: v1.6.1 2025-07-03 21:49:58 -04:00
bgkillas
434acff594 prob closer to what im meant to do 2025-07-03 18:05:41 -04:00
bgkillas
c905ef4de1 whoops accidentally modified ewext 2025-07-03 14:51:05 -04:00
bgkillas
a42921a0cc more type work 2025-07-03 14:46:21 -04:00
bgkillas
4fd89c2f76 some unfinished type work 2025-07-03 03:22:39 -04:00
bgkillas
c8fe3a6fd2 dont clone cell, make celldata proper size 2025-07-02 22:09:59 -04:00
bgkillas
358e7b3a3b use colour instead of usize, add modified bit to the chunk struct, have left click give debug info on current material hovering over 2025-07-02 17:39:23 -04:00
bgkillas
6a1e36ff40 add more data to ntypes 2025-07-02 14:06:04 -04:00
bgkillas
1a14d35864 make blob more natural 2025-07-02 11:20:38 -04:00
bgkillas
f1e8a001d4 clear cache on set cache 2025-07-02 08:53:38 -04:00
bgkillas
e84a90058d fix blob guy :D 2025-07-02 00:36:57 -04:00
bgkillas
9c063ab2aa fix more potential blob crashes 2025-07-01 19:11:31 -04:00
bgkillas
4efa0c0143 fix larger then 1 sized holes 2025-07-01 18:38:54 -04:00
bgkillas
e03bac7544 improve stability of blob, make blob not have holes, make blob faster 2025-07-01 17:44:36 -04:00
bgkillas
2da883423d improve stability of blob 2025-07-01 16:04:39 -04:00
bgkillas
794f8a47b7 have blob sorround the target 2025-07-01 15:39:51 -04:00
bgkillas
4ca82a686e fix build 2025-07-01 15:04:11 -04:00
bgkillas
ba9ae29b36 allow disabling caching, add logging option, move log perf option to noita 2025-07-01 14:42:58 -04:00
bgkillas
e3dbae674e dont allow for crashes 2025-07-01 13:10:44 -04:00
bgkillas
6d5fc3bced add gravity to blob particals and add example to see the gravity in action 2025-07-01 12:26:35 -04:00
bgkillas
7764d6cb3d remove an uneeded check which breaks things 2025-06-30 11:34:23 -04:00
bgkillas
4884a16643 fix blob guy teleporting 2025-06-30 11:20:26 -04:00
bgkillas
b67563df31 simplify shift 2025-06-30 01:48:34 -04:00
bgkillas
a024afceae fix shifting 2025-06-29 22:50:21 -04:00
bgkillas
04c13e8641 use isize instead of i32 2025-06-29 19:56:57 -04:00
bgkillas
12eb53b8fe spawn pixels correctly 2025-06-29 19:46:10 -04:00
bgkillas
5b42208ebd simplify stuff 2025-06-29 18:31:49 -04:00
bgkillas
1fd4e775ed we got pixels 2025-06-29 13:56:42 -04:00
bgkillas
4f958371fc progress 2025-06-29 13:09:01 -04:00
IQuant
4c2d5ab41c Handle malformed biome files better 2025-06-29 11:52:37 +03:00
IQuant
b0664b8370 Build extension automatically on run-rel 2025-06-29 11:38:10 +03:00
bgkillas
e044691efc make lua functions for blob 2025-06-29 03:19:32 -04:00
bgkillas
aaf6e33bfc call functions correctly 2025-06-28 15:20:06 -04:00
bgkillas
81693c698c why asm fail :c 2025-06-28 04:37:26 -04:00
bgkillas
ce2d8b5f13 make compile 2025-06-28 03:27:56 -04:00
bgkillas
933a0cb08a yay remove cell works :D 2025-06-28 03:05:43 -04:00
bgkillas
ed68940f06 more blob work 2025-06-27 23:23:49 -04:00
bgkillas
fc9b39d660 more blob work 2025-06-27 14:11:41 -04:00
bgkillas
5f076e8e49 fix blob guy world reading 2025-06-27 12:39:53 -04:00
bgkillas
eee8b0be3d re add blob guy 2025-06-27 10:26:09 -04:00
bgkillas
5cfa5191a7 dont make blob guy a submodule? 2025-06-27 10:25:04 -04:00
bgkillas
0154d41b13 move blob guy here and move noita api outside of ewext and noitaapimacro inside of noitaapi 2025-06-27 10:22:14 -04:00
bgkillas
12e1a7cfb5 Automated commit: v1.6.0 2025-06-26 16:53:12 -04:00
bgkillas
bf9bb578d5 fix updater a bit 2025-06-26 16:29:01 -04:00
bgkillas
5c1dafb239 make clippy happy(nightly) 2025-06-26 14:37:19 -04:00
bgkillas
79c4a9a15e make clippy happy(stable) 2025-06-26 14:28:50 -04:00
bgkillas
feff08d3e3 phys init better and cache stain components 2025-06-26 13:10:44 -04:00
bgkillas
281d7a77d0 fix some issues with cache 2025-06-26 12:49:18 -04:00
bgkillas
06e2f7e2ea dont have Results where not needed 2025-06-26 12:31:54 -04:00
bgkillas
618cf08439 cache frame num and world pos 2025-06-26 11:46:34 -04:00
bgkillas
651b69396b dont as f32 in the position function itself to give more freedom of values, add more to noita_api lib 2025-06-26 11:22:19 -04:00
bgkillas
920b2b7829 maybe update dlls and stuff correctly(untested, we will see if i bother to test) 2025-06-26 10:46:33 -04:00
bgkillas
71c934d32f use bool instead of u128 do to performance reasons 2025-06-26 09:51:52 -04:00
bgkillas
ba2305d331 fix get/set being wrong for chunk changed 2025-06-25 22:29:35 -04:00
bgkillas
74c7300de2 more efficient bit packing for noita proxys working chunks 2025-06-25 21:56:34 -04:00
bgkillas
a5384b05ae more efficient bit packing for cache 2025-06-25 21:46:11 -04:00
bgkillas
b0ac6b6f9e less string allocations 2025-06-25 21:14:02 -04:00
bgkillas
aa1f244a2f move print to noita api crate and add some more functions 2025-06-25 16:01:57 -04:00
bgkillas
bda32fe65a move performance tests around a bit and improve code quality 2025-06-25 10:09:40 -04:00
bgkillas
f9566e0368 minor change 2025-06-25 09:38:48 -04:00
bgkillas
2181d6de46 fix a stale cache issue 2025-06-25 02:01:51 -04:00
bgkillas
74b063453a cache files for animations better 2025-06-25 02:01:15 -04:00
bgkillas
19de23e914 cache file names for animations 2025-06-25 01:05:19 -04:00
bgkillas
c7cbbb9c97 fix stuff not syncing sometimes oops 2025-06-24 23:25:27 -04:00
bgkillas
148d7c8131 ensure orb duplication may not happen 2025-06-24 21:35:10 -04:00
bgkillas
9526496db2 fix chest mimic duping when you poly 2025-06-24 18:32:37 -04:00
bgkillas
52f057097e fix on new ent logic being changed a bit by accident add more performance logging to ewext init 2025-06-24 15:37:56 -04:00
bgkillas
95a284734a call var.name() less and improve code quality 2025-06-24 14:17:37 -04:00
bgkillas
6d5464ba85 optimize on new entity a bit 2025-06-24 13:43:47 -04:00
bgkillas
5ee7872953 fix some potential bugs and optimize on_new_entity more 2025-06-24 11:38:13 -04:00
bgkillas
6e0e3fe178 optimize getting entities components 2025-06-24 02:26:05 -04:00
bgkillas
07dec1a025 use less noita_api::raw hooks in ewext 2025-06-23 22:23:33 -04:00
bgkillas
865ea5271e try to fix workflow 2025-06-23 21:30:01 -04:00
bgkillas
1bf13ac5d2 remove nightly install from justfile 2025-06-23 21:28:07 -04:00
bgkillas
32f5414485 use nightly ewext only for workflow 2025-06-23 21:27:37 -04:00
bgkillas
748146f7fb make damagetype enum for inflict damage 2025-06-23 21:19:45 -04:00
bgkillas
fd9d83341c more caching 2025-06-23 20:19:49 -04:00
bgkillas
24b94b2cf9 remove error on picking up an item that you do not own as it is exepcted 2025-06-23 17:18:29 -04:00
bgkillas
dd012dc8d3 reduce ewext size by 160kb 2025-06-23 16:43:02 -04:00
bgkillas
42b8ddb05d remove entities from cache when they are not apart of des 2025-06-23 15:45:18 -04:00
bgkillas
b23f1e0386 fix crash on start 2025-06-23 10:43:08 -04:00
bgkillas
ff0dbf8b5b dont use set length for stain effects as it seems to get angry 2025-06-23 10:37:40 -04:00
bgkillas
a359268fc9 add len to length error 2025-06-23 10:32:54 -04:00
bgkillas
6cd1c092e5 use smallvec for entity cache, use set array size for stains 2025-06-23 01:02:11 -04:00
bgkillas
fe672e6a5d get rid of allocations during poly check 2025-06-22 18:35:20 -04:00
bgkillas
0db6f279bc fix serialization error and wait a few frames for ewext to start 2025-06-22 14:33:03 -04:00
bgkillas
eeaf90878c pass times around nicer 2025-06-22 13:38:05 -04:00
bgkillas
427c9a406f actually use the cache i make 2025-06-22 13:18:58 -04:00
bgkillas
a337dfbb1e support entity caching more fully, need to fix performance and cache degragation issues 2025-06-22 11:39:05 -04:00
bgkillas
b288cebf02 try to fix the weird errors that happen / make them more debuggable 2025-06-22 08:10:49 -04:00
bgkillas
e93db76c77 initial entity cache setup(buggy) 2025-06-21 21:11:33 -04:00
bgkillas
54b60d5a1c dont show cumulative times for ewext update perf 2025-06-21 13:40:41 -04:00
bgkillas
9e80b862b4 fix spell ban 2025-06-20 21:38:53 -04:00
bgkillas
aebe26407e i dont know what python comments are it seems 2025-06-20 17:02:57 -04:00
bgkillas
3e625d2841 revert changes to macos bin 2025-06-20 16:49:26 -04:00
bgkillas
95d26d56c6 hopefully fix macos updating 2025-06-20 15:13:38 -04:00
bgkillas
6e8f86e26c build ewext smaller 2025-06-20 15:07:45 -04:00
bgkillas
b171cd10b5 fix macos 2025-06-20 15:02:52 -04:00
bgkillas
b7abad91fd fix some formatting and actions dependency issue 2025-06-20 14:40:58 -04:00
bgkillas
256192400f have dylib next to exe 2025-06-20 14:23:40 -04:00
bgkillas
338c26549a make macos release more userfriendly 2025-06-20 14:18:05 -04:00
bgkillas
cd4d87d073 make spells less likely to be in walls i hope 2025-06-20 14:12:01 -04:00
bgkillas
1952ec1ac5 fix some more potential crashes 2025-06-20 13:38:12 -04:00
bgkillas
2b90f2d78b remove an unwrap i think is safe but just to be sure 2025-06-19 21:23:15 -04:00
bgkillas
92bb29d672 fix a crash 2025-06-19 20:55:00 -04:00
bgkillas
812f3cc7de attach to console on windows if availbalbe 2025-06-19 16:50:29 -04:00
bgkillas
b5bafc6bfd add percentiles to perf script 2025-06-19 07:36:40 -04:00
bgkillas
8d4735d755 fix perf script 2025-06-19 07:30:36 -04:00
bgkillas
1dc2106316 simplify apply diff do to unreachability of some patterns 2025-06-19 04:11:03 -04:00
bgkillas
66041bd10b fix crash on boot 2025-06-18 18:11:09 -04:00
bgkillas
01cc4ad767 use a enum instead of a tuple of options 2025-06-18 17:36:20 -04:00
bgkillas
20d0f65ae9 slightly more cursed less allocations 2025-06-18 15:25:57 -04:00
bgkillas
7c3191f3a7 less allocations 2025-06-18 15:02:06 -04:00
IQuant
f5a1dadfc6 Parse _biomes_all.xml instead of having a hardcoded list of biome script files to patch, hopefully improving mod compat. 2025-06-18 20:46:47 +03:00
bgkillas
66851f50b2 disable laser traps from syncing since they like to break 2025-06-18 12:56:20 -04:00
bgkillas
bcd48f31e0 use a more modern clipboard crate for no specific reason 2025-06-18 12:53:48 -04:00
bgkillas
a5d6de8bc8 add my perf script specify hook name for perf data 2025-06-18 12:17:45 -04:00
bgkillas
5d3e99298a remove some more allocations 2025-06-18 11:35:29 -04:00
bgkillas
8184eabf47 forgot to clear vector in last 2025-06-18 11:04:51 -04:00
bgkillas
47014cac9b reduce allocations significantly 2025-06-18 10:49:29 -04:00
bgkillas
5383ba5d87 less clones 2025-06-18 10:29:27 -04:00
bgkillas
452e6037b9 fix dead enemies not staying dead after restart 2025-06-18 09:58:49 -04:00
bgkillas
3beed97f2d add log performance button in proxy ui 2025-06-17 18:35:27 -04:00
bgkillas
0bac664c0f make github actions not yell at me 2025-06-17 16:24:56 -04:00
bgkillas
58eb097ec5 note why chunk map is disabled by default 2025-06-17 16:10:15 -04:00
bgkillas
4ff4fd3437 update dependencies(i wanted to update cpal but waiting on rodio :c) 2025-06-17 16:03:58 -04:00
bgkillas
67619fc7a2 use less clones/collects in ewext 2025-06-16 13:57:27 -04:00
IQuant
7fa759af8f Automated commit: v1.5.5 2025-06-16 18:46:30 +03:00
IQuant
fb6a50edd3 Fix formatting 2025-06-16 18:35:23 +03:00
IQuant
fae780f754
Merge pull request #390 from benaryorg/cli_socket_addr_bind
noita-proxy: socketaddr binding via --host
2025-06-16 18:33:50 +03:00
benaryorg
1bc016a84f
noita-proxy: socketaddr binding via --host
This allows binding to arbitrary IP addresses (`[2001:db8::1]:12345`, `203.0.113.1:12345`) while still providing compatibility to the previous behaviour of binding to a port on IPv4 only.
The only non-compatible change is that when providing neither a valid `SocketAddr` nor a valid port the code will now err out instead of defaulting to a default port to avoid silently discarding arguments or invalid configurations.
As a result `--host --exe-path` will now fail as `--exe-path` is not a valid address or port.

Minor changes:

- some practically infallible parsing (`Ipv4Addr::UNSPECIFIED`) has also been replaced by infallible typing avoiding unnecessary `unwrap()`s
- typos

Fixes #389

Signed-off-by: benaryorg <binary@benary.org>
2025-06-16 12:25:01 +00:00
IQuant
fa9be0d8b5 Make clippy happy (again) 2025-06-02 13:07:58 +03:00
IQuant
f8a23221e8 Automated commit: v1.5.4 2025-06-02 11:51:31 +03:00
IQuant
aa6210e3f0 Make clippy happy 2025-06-02 11:37:16 +03:00
IQuant
0ef697c71c Remove info about stress tests, as we haven't done any of those in a while 2025-06-01 22:58:55 +03:00
IQuant
7dc4e6a9aa Do the same thing for save_state stuff 2025-06-01 22:57:02 +03:00
IQuant
4f1096d8d6 Use more proper path for storing proxy config 2025-06-01 22:11:33 +03:00
Mikael Walhelm
b136fdc26c
Merge pull request #378 from goluboch/master
translated more strings for russian
2025-05-19 10:27:16 -04:00
golub
5f6f69b5fe translated more strings for russian 2025-05-19 15:38:03 +03:00
bgkillas
3128350798 Automated commit: v1.5.3 2025-05-09 11:46:28 -04:00
bgkillas
444a94e91e fix pvp :( 2025-05-09 10:48:26 -04:00
IQuant
dc33b9e49a Update readme for #368 2025-05-08 17:21:12 +03:00
bgkillas
ae77a8f01b Automated commit: v1.5.2 2025-04-28 14:26:34 -04:00
bgkillas
f34b61f24a make suspended containers only work on host cuz im lazy 2025-04-28 14:16:30 -04:00
bgkillas
aace122141 dont sync suspended containers 2025-04-28 14:11:04 -04:00
bgkillas
f553241d9a proton detect better 2025-04-28 14:00:37 -04:00
bgkillas
1b77546bef dont tp physics bodys large distances cuz it prob makes game sad 2025-04-28 11:53:46 -04:00
bgkillas
70594e0be1 dont use cell eater logic from certain bodys 2025-04-28 11:52:41 -04:00
bgkillas
97f719d08c disable chunkmap by default 2025-04-28 11:50:16 -04:00
IQuant
a408b237d9 Fix CI description 2025-04-25 22:10:53 +03:00
IQuant
984877dce8 Try CI for macos 2025-04-25 20:39:11 +03:00
IQuant
462a76b5f6
Add OSX steamsdk 2025-04-04 23:27:53 +03:00
bgkillas
5bcd770441 Automated commit: v1.5.1 2025-03-22 13:54:57 -04:00
IQuant
eabeae9fdd Make the proxy restart automatically on auto update while we're at it. 2025-03-19 20:43:40 +03:00
IQuant
8ccb11dbc2 Work around reading comprehension issues. 2025-03-19 20:41:19 +03:00
bgkillas
7a63cf2928 fix hamis vase 2025-03-15 17:11:23 -04:00
131 changed files with 30944 additions and 7729 deletions

View file

@ -11,6 +11,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: i686-pc-windows-gnu
toolchain: 'nightly'
- name: Install extra deps
run: sudo apt-get update && sudo apt-get install -y gcc-mingw-w64-i686
@ -19,10 +20,12 @@ jobs:
workspaces: ewext -> target
- name: Build ewext
run: cargo build --release --target i686-pc-windows-gnu
run: |
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
cargo +nightly build --release --target=i686-pc-windows-gnu -Zbuild-std="panic_abort,std"
working-directory: ./ewext
- name: Copy ewext
run: cp ewext/target/i686-pc-windows-gnu/release/ewext.dll quant.ew/ewext1.dll
run: cp ewext/target/i686-pc-windows-gnu/release/ewext.dll quant.ew/ewext.dll
- name: Create archive
run: python scripts/ci_make_archives.py mod
@ -59,9 +62,31 @@ jobs:
name: noita-proxy-linux.zip
path: target/noita-proxy-linux.zip
build-proxy-macos:
name: Build proxy for macos
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: noita-proxy -> target
- name: Build Macos proxy release
run: cargo build --release
working-directory: ./noita-proxy
- name: Create archives
run: python scripts/ci_make_archives.py macos
- uses: actions/upload-artifact@v4
with:
name: noita-proxy-macos-arm64.zip
path: target/noita-proxy-macos.zip
build-proxy-windows:
name: Build proxy for windows
runs-on: windows-2019
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
@ -87,7 +112,7 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
needs: [build-mod, build-proxy-linux, build-proxy-windows]
needs: [build-mod, build-proxy-linux, build-proxy-windows, build-proxy-macos]
steps:
- uses: actions/checkout@v4

6
.gitignore vendored
View file

@ -6,3 +6,9 @@ save_state
/quant.ew/ewext.dll
/quant.ew/ewext0.dll
/quant.ew/ewext1.dll
/blob_guy/blob_guy/blob_guy.dll
/blob_guy/target
# Nix
/result
/result-man

View file

@ -10,5 +10,10 @@
"wait",
"async"
],
"C_Cpp.default.compilerPath": "/usr/bin/clang++"
"C_Cpp.default.compilerPath": "/usr/bin/clang++",
"spellright.language": [],
"spellright.documentTypes": [
"markdown",
"latex"
]
}

View file

@ -24,15 +24,21 @@ build_luajit:
# `rustup target add i686-pc-windows-gnu` first
build_ext:
cd ewext && cargo build --release --target=i686-pc-windows-gnu
cp ewext/target/i686-pc-windows-gnu/release/ewext.dll quant.ew/ewext1.dll
cp ewext/target/i686-pc-windows-gnu/release/ewext.dll quant.ew/ewext.dll
build_ext_debug:
cd ewext && cargo build --target=i686-pc-windows-gnu
cp ewext/target/i686-pc-windows-gnu/debug/ewext.dll quant.ew/ewext1.dll
cp ewext/target/i686-pc-windows-gnu/debug/ewext.dll quant.ew/ewext.dll
##
build_blob:
cd blob_guy && cargo +nightly build --release --target=i686-pc-windows-gnu -Zbuild-std="panic_abort,std" -Zbuild-std-features=panic_immediate_abort
cp blob_guy/target/i686-pc-windows-gnu/release/blob_guy.dll blob_guy/blob_guy/blob_guy.dll
build_blob_debug:
cd blob_guy && cargo build --target=i686-pc-windows-gnu
cp blob_guy/target/i686-pc-windows-gnu/debug/blob_guy.dll blob_guy/blob_guy/blob_guy.dll
run-rel: add_dylib_release
run-rel: add_dylib_release build_ext
cd noita-proxy && NP_SKIP_MOD_CHECK=1 cargo run --release
flamegraph: add_dylib_debug

View file

@ -34,6 +34,28 @@ Then, start Noita, and enable the mod.
Now you're ready to start a server and have fun!
### Installation on MacOS (provided by @Ownezx and @Roenalt)
1. Install a GOG copy of Noita using [portingkit](https://www.portingkit.com/) by following the guide given directly on the Noita entry page on portingkit with a few specific options in the "Advance Settings" step:
1. Set the Engine to "WS12WineKegworks10.0-battle.net"
2. Set the Operating System to "Windows 11".
2. After confirming that the game launch, open the folder where the game is installed and navigate to where the `noita.exe` is located (usually in "/Users/{User}/Applications/Noita.app/Contents/SharedSupport/prefix/drive_c/GOG Games/Noita") and add a shortcut to it in the sidebar of the Finder.
3. Go to [releases](https://github.com/IntQuant/noita_entangled_worlds/releases), download the latest `noita-proxy-macos.zip`.
4. Unpack it and launch the proxy, it will ask to give the path to the `noita.exe` (that we save a shortcut to!). Once the path is given, the proxy will be able to download and install the mod automatically.
5. Close the proxy, then launch it again via a terminal with the following command: `~/Applications/noita-proxy-macos/noita_proxy --launch-cmd '"/Users/{User}/Applications/Noita.app/Contents/MacOS/wineskinlauncher" --run "C:\GOG Games\Noita\noita.exe"'`
6. Then you can enjoy the mod as usual, by enabling it in the "Mods" menu of Noita.
Note: The proxy must be launched via terminal with the command above every time you want to play multiplayer.
## Installation on Linux with Lutris (provided by @merll002)
1. Install the GOG version of Noita through the lutris game installer:
<img width="596" height="64" alt="image" src="https://github.com/user-attachments/assets/dfc2f415-1557-4716-b3e2-c62aae941344" />
2. Navigate to the directory where the proxy was downloaded
3. Run the proxy by typing `./start.sh`
4. Enable the mod (refer to main installation instructions)
5. Done!
## Connect using Steam
In the Proxy window, click on "Create Lobby". Then, "Save lobby ID to clipboard". Send that ID to your friends, who can then *copy* it and press "Connect to lobby in clipboard".
@ -42,19 +64,43 @@ In the Proxy window, click on "Create Lobby". Then, "Save lobby ID to clipboard"
After that, just start a new Noita game on everyone's PCs, and you should be in multiplayer mode :)
## When to press "New Game" and when to press "Continue"
- "New Game" - you're joining a multiplayer run you haven't joined before.
- "Continue" - you're reconnecting to a multiplayer run that you've joined before and hasn't ended yet.
Using the same save file for multiplayer and singleplayer isn't something that should be done.
## Global perks
Some perks are perks and affect the entire world, and thus are shown for every player.
There are 11 global perks:
- No More Shuffle
- Unlimited Spells
- Trick Blood Money
- Gold is Forever
- Greed
- Trick Greed
- Peace with Gods
- Extra Item in Holy Mountain
- More Love
- More Hatred
- More Blood
## Mods support
[The mods listed here](https://docs.google.com/spreadsheets/d/1nMdqzrLCav_diXbNPB9RgxPcCQzDPgXdEv-klKWJyS0) have been tested by the community, it is publically editable so please add any untested mod with your findings
## Cli connect
## CLI connect
can also connect via cli, just run `noita_proxy --lobby [steam_code/ip and port]`
You can also connect via cli, just run `noita_proxy --lobby [steam_code/ip and port]`
## Cli host
## CLI host
can also host via cli, just run `noita_proxy --host [steam/port]`, "--host steam" will host a steam game and "--host 5123" or any port will host via ip at that port
You can also host via cli, just run `noita_proxy --host [steam/port]`, "--host steam" will host a steam game and "--host 5123" or any port will host via ip at that port
## Connecting via steam without steam version of game

4206
blob_guy/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

28
blob_guy/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "blob_guy"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
#crate-type = ["rlib"]
[profile.release]
lto = true
strip = true # Not having that causes wine debugger to crash.
panic = "abort"
split-debuginfo = "packed"
incremental=true
codegen-units=1
opt-level = 3
[dependencies]
noita_api = {path = "../noita_api"}
eyre = "0.6.12"
smallvec = "1.15.1"
rustc-hash = "2.1.1"
rayon = "1.11.0"
[dev-dependencies]
rupl = {git = "https://github.com/bgkillas/rupl.git", default-features = false, features = ["egui"] }
eframe = "0.32.1"

View file

@ -0,0 +1,32 @@
ModMaterialsFileAdd("mods/blob_guy/materials.xml")
package.cpath = package.cpath .. ";./mods/blob_guy/?.dll"
package.path = package.path .. ";./mods/blob_guy/?.lua"
local blob_guy = require("blob_guy")
local started = -1
local times = 0
local start = 10
local times_len = start
function OnWorldPreUpdate()
if started == -1 then
return
end
if started > 0 then
started = started - 1
return
end
local start_time = GameGetRealWorldTimeSinceStarted()
blob_guy.update()
local end_time = GameGetRealWorldTimeSinceStarted()
local delta = (end_time - start_time) * 1000000
times = times + delta
times_len = times_len - 1
if times_len == 0 then
times_len = start
GamePrint(math.floor(times / start + 0.5))
times = 0
end
end
function OnWorldInitialized()
started = 60
blob_guy.init_particle_world_state()
end

View file

@ -0,0 +1,11 @@
<Materials>
<CellData
name="blob_guy"
ui_name="blob guy"
wang_color="AAFFAAAA"
cell_type="liquid"
liquid_sand="0"
tags="[static]"
>
</CellData>
</Materials>

View file

@ -0,0 +1,9 @@
<Mod
name="blob guy"
description="a mod which adds a material enemy"
request_no_api_restrictions="1"
is_game_mode="0"
translation_xml_path=""
translation_csv_path=""
>
</Mod>

View file

@ -0,0 +1,136 @@
use blob_guy::blob_guy::Blob;
use blob_guy::chunk::{Chunks, Pos};
use eframe::egui;
use rupl::types::{Color, Complex, Graph, GraphType, Name, Show, Vec2};
use std::f64::consts::PI;
fn main() {
eframe::run_native(
"blob",
eframe::NativeOptions {
..Default::default()
},
Box::new(|_| Ok(Box::new(App::new()))),
)
.unwrap();
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.main(ctx);
}
}
struct App {
plot: Graph,
data: Data,
frame: u8,
}
struct Data {
blob: Blob,
world: Chunks,
}
impl Data {
fn new() -> Self {
Self {
blob: Blob::new(0.0, 0.0),
world: Default::default(),
}
}
fn update(&mut self, plot: &mut Graph) {
let s = &plot.names[0].vars[0];
let s = &s[3..s.len() - 1];
let (a, b) = s.split_once(',').unwrap();
let GraphType::Point(p) = &mut plot.data[0] else {
unreachable!()
};
p.x = a.parse().unwrap();
p.y = b.parse().unwrap();
self.blob.pos = Pos::new(p.x as f32, p.y as f32);
self.blob
.update(
self.blob.pos.to_chunk(),
&mut self.world,
self.blob.mean(),
true,
)
.unwrap();
let sx = p.x;
let sy = p.y;
plot.data.drain(1..);
plot.main_colors.drain(1..);
for (x, y) in self.blob.pixels.keys().copied() {
let p1 = Vec2::new(x as f64 + 0.5, y as f64 + 0.5);
plot.data.push(GraphType::Point(p1));
plot.main_colors.push(Color {
r: 170,
g: 170,
b: 255,
});
}
let (x, y) = self.blob.mean();
let (x, y) = (x as f64, y as f64);
plot.data.push(GraphType::Point(Vec2 { x, y }));
plot.main_colors.push(Color {
r: 255,
g: 170,
b: 255,
});
let r = (self.blob.pixels.len() as f64 / PI).sqrt().ceil();
let r2 = r * r;
let c = 512;
let mut values = Vec::with_capacity(2 * c + 1);
let mut values2 = Vec::with_capacity(2 * c + 1);
for i in -(c as isize)..=c as isize {
let x = i as f64 / c as f64 * r;
let y = (r2 - x * x).sqrt();
values.push((x + sx, Complex::Real(y + sy)));
values2.push((x + sx, Complex::Real(sy - y)));
}
plot.data.push(GraphType::List(vec![
GraphType::Coord(values),
GraphType::Coord(values2),
]));
plot.main_colors.push(Color { r: 0, g: 0, b: 0 });
self.blob.cull();
}
}
impl App {
fn new() -> Self {
let mut plot = Graph::new(
vec![GraphType::Point(Vec2::splat(0.0))],
vec![Name {
vars: vec!["a={0,0}".to_string()],
name: "a".to_string(),
show: Show::Real,
}],
false,
-32.0,
32.0,
);
plot.point_size = 8.0;
App {
plot,
data: Data::new(),
frame: 8,
}
}
fn main(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default()
.frame(egui::Frame::default().fill(egui::Color32::from_rgb(255, 255, 255)))
.show(ctx, |ui| {
self.plot.keybinds(ui);
let rect = ctx.available_rect();
self.plot
.set_screen(rect.width() as f64, rect.height() as f64, true, true);
if self.frame == 0 {
self.data.update(&mut self.plot);
self.frame = 8;
} else {
self.frame -= 1;
}
self.plot.update(ctx, ui);
ctx.request_repaint();
});
}
}

345
blob_guy/src/blob_guy.rs Normal file
View file

@ -0,0 +1,345 @@
use crate::chunk::Chunks;
use crate::chunk::{CellType, ChunkPos, Pos};
use crate::{CHUNK_AMOUNT, State};
#[cfg(target_arch = "x86")]
use noita_api::EntityID;
use rustc_hash::{FxBuildHasher, FxHashMap};
use std::f32::consts::{PI, TAU};
pub const OFFSET: isize = CHUNK_AMOUNT as isize / 2;
impl State {
pub fn update(&mut self) -> eyre::Result<()> {
if noita_api::raw::input_is_mouse_button_just_down(1)? {
unsafe {
self.particle_world_state
.assume_init_mut()
.debug_mouse_pos()?
};
}
if self.blobs.is_empty() {
self.push_new();
return Ok(());
}
'upper: for blob in self.blobs.iter_mut() {
let mean = blob.mean();
blob.update_pos()?;
let start = Pos::new(mean.0, mean.1).to_chunk();
if self
.world
.read(
unsafe { self.particle_world_state.assume_init_ref() },
self.blob_guy,
start,
)
.is_err()
{
blob.update(start, &mut self.world, mean, false)?;
continue 'upper;
}
blob.update(start, &mut self.world, mean, true)?;
self.world.paint(
unsafe { self.particle_world_state.assume_init_mut() },
self.blob_guy,
start,
);
blob.cull();
}
Ok(())
}
pub fn push_new(&mut self) {
self.blobs.push(Blob::new(256.0, -(64.0 + 32.0)));
let start = self.blobs[0].pos.to_chunk();
if self
.world
.read(
unsafe { self.particle_world_state.assume_init_ref() },
self.blob_guy,
start,
)
.is_err()
{
return;
}
self.blobs[0].register_pixels(start, &mut self.world);
self.world.paint(
unsafe { self.particle_world_state.assume_init_mut() },
self.blob_guy,
start,
);
}
}
pub const SIZE: usize = 24;
pub struct Blob {
pub pos: Pos,
pub pixels: FxHashMap<(isize, isize), Pixel>,
pub count: usize,
}
#[derive(Default, Copy, Clone)]
pub struct Pixel {
pub pos: Pos,
velocity: Pos,
stop: Option<usize>,
mutated: bool,
temp: bool,
}
const DIRECTIONS: [f32; 7] = [
0.0,
PI / 4.0,
-PI / 4.0,
PI / 3.0,
-PI / 3.0,
PI / 2.3,
-PI / 2.3,
];
impl Pixel {
fn new(pos: Pos) -> Self {
Pixel {
pos,
..Default::default()
}
}
}
impl Blob {
pub fn cull(&mut self) {
self.pixels.retain(|_, p| !p.temp);
}
pub fn update_pos(&mut self) -> eyre::Result<()> {
#[cfg(target_arch = "x86")]
{
if let Ok(player) =
EntityID::get_closest_with_tag(self.pos.x as f64, self.pos.y as f64, "player_unit")
{
let (x, y) = player.position()?;
self.pos.x = x as f32;
self.pos.y = y as f32 - 7.0;
}
}
Ok(())
}
pub fn mean(&self) -> (f32, f32) {
let n = self
.pixels
.values()
.fold((0.0, 0.0), |acc, p| (acc.0 + p.pos.x, acc.1 + p.pos.y));
(
n.0 / self.pixels.len() as f32,
n.1 / self.pixels.len() as f32,
)
}
const THETA_COUNT: usize = 16;
pub fn get_thetas(&self, r: f32) -> [bool; Self::THETA_COUNT] {
let mut arr = [0; Self::THETA_COUNT];
for p in self.pixels.values() {
let dx = self.pos.x - p.pos.x;
let dy = self.pos.y - p.pos.y;
if dy.hypot(dx) < r {
let n = Self::THETA_COUNT as f32 * (dy.atan2(dx) / TAU + 0.5);
arr[n as usize] += 1;
}
}
let l = self.pixels.len().div_ceil(Self::THETA_COUNT);
arr.map(|n| n < l / 2)
}
pub fn update(
&mut self,
start: ChunkPos,
map: &mut Chunks,
mean: (f32, f32),
loaded: bool,
) -> eyre::Result<()> {
let r = (self.pixels.len() as f32 / PI).sqrt().ceil();
let array = &self.get_thetas(r);
let theta = (mean.1 - self.pos.y).atan2(mean.0 - self.pos.x);
for p in self.pixels.values_mut() {
p.mutated = false;
}
let mut keys = self.pixels.keys().cloned().collect::<Vec<(isize, isize)>>();
keys.sort_unstable_by(|(a, b), (x, y)| {
let da = self.pos.x.floor() as isize - a;
let db = self.pos.y.floor() as isize - b;
let dx = self.pos.x.floor() as isize - x;
let dy = self.pos.y.floor() as isize - y;
let r1 = da * da + db * db;
let r2 = dx * dx + dy * dy;
r2.cmp(&r1)
});
while !keys.is_empty()
&& let Some((c, p)) = self.pixels.remove_entry(&keys.remove(0))
{
self.run(c, p, theta, map, start, array);
}
if !loaded {
return Ok(());
}
self.register_pixels(start, map);
Ok(())
}
pub fn register_pixels(&mut self, start: ChunkPos, map: &mut Chunks) {
let mut last = ChunkPos::new(isize::MAX, isize::MAX);
let mut k = 0;
for (x, y) in self.pixels.keys().copied() {
let c = ChunkPos::new(x, y);
if c != last {
let new = c.get_world(start);
if new < 0 || new as usize >= CHUNK_AMOUNT * CHUNK_AMOUNT {
continue;
}
k = new as usize;
last = c;
}
map.0[k][(x, y)] = match map.0[k][(x, y)] {
CellType::Unknown | CellType::Liquid => {
map.0[k].modified = true;
CellType::Blob
}
_ => CellType::Ignore,
}
}
}
pub fn new(x: f32, y: f32) -> Self {
let mut pixels = FxHashMap::with_capacity_and_hasher(SIZE * SIZE, FxBuildHasher);
for i in 0..SIZE * SIZE {
let a = (i / SIZE) as f32 - SIZE as f32 / 2.0 + 0.5;
let b = (i % SIZE) as f32 - SIZE as f32 / 2.0 + 0.5;
let p = Pixel::new(Pos::new(x + a, y + b));
pixels.insert((p.pos.x.floor() as isize, p.pos.y.floor() as isize), p);
}
Blob {
pos: Pos::new(x, y),
pixels,
count: SIZE * SIZE,
}
}
#[allow(clippy::too_many_arguments)]
pub fn run(
&mut self,
c: (isize, isize),
mut p: Pixel,
theta: f32,
world: &Chunks,
start: ChunkPos,
array: &[bool; Self::THETA_COUNT],
) -> bool {
if p.mutated {
self.pixels.insert(c, p);
return false;
}
p.mutated = true;
let dx = self.pos.x - p.pos.x;
let dy = self.pos.y - p.pos.y;
let dist = dy.hypot(dx).max(0.1);
let psi = dy.atan2(dx);
let angle = psi - theta;
let mag = 512.0;
let (ax, ay) = if !array[(Self::THETA_COUNT as f32 * (dy.atan2(dx) / TAU + 0.5)) as usize] {
let n = if psi < theta + PI {
psi + PI / 4.0
} else {
psi - PI / 4.0
};
let (s, c) = n.sin_cos();
let (dx, dy) = (dist * c, dist * s);
(mag * dx / dist, mag * dy / dist)
} else {
let phi = (3.0 * angle.abs() / PI + 1.0).min(3.0);
(phi * mag * dx / dist, phi * mag * dy / dist)
};
p.velocity.x += ax / 60.0;
p.velocity.y += ay / 60.0;
let damping = 0.9;
p.velocity.x *= damping;
p.velocity.y *= damping;
let mut n;
if let Some(k) = p.stop.as_mut() {
let phi = p.velocity.y.atan2(p.velocity.x);
let t = phi + DIRECTIONS[*k / 2];
let (sin, cos) = t.sin_cos();
p.pos.x += cos * if k.is_multiple_of(2) { 1.0 } else { 1.5 };
p.pos.y += sin * if k.is_multiple_of(2) { 1.0 } else { 1.5 };
n = (p.pos.x.floor() as isize, p.pos.y.floor() as isize);
let mut l = *k;
if self.pixels.contains_key(&n) {
loop {
if l.div_ceil(2) == DIRECTIONS.len() {
l = 0;
} else {
l += 1;
}
if l == *k {
break;
}
let t = phi + DIRECTIONS[*k / 2] + PI;
let (sin, cos) = t.sin_cos();
let x = p.pos.x + cos * if k.is_multiple_of(2) { 1.0 } else { 1.5 };
let y = p.pos.y + sin * if k.is_multiple_of(2) { 1.0 } else { 1.5 };
n = (x.floor() as isize, y.floor() as isize);
if !self.pixels.contains_key(&n) {
p.pos.x = x;
p.pos.y = y;
break;
}
}
}
if k.div_ceil(2) == DIRECTIONS.len() {
*k = 0;
} else {
*k += 1;
}
} else {
p.pos.x += p.velocity.x / 60.0;
p.pos.y += p.velocity.y / 60.0;
n = (p.pos.x.floor() as isize, p.pos.y.floor() as isize);
}
if c != n {
let pos = ChunkPos::new(n.0, n.1);
let index = pos.get_world(start);
if index >= 0
&& let Some(chunk) = world.0.get(index as usize)
&& match chunk[n] {
CellType::Unknown => false,
CellType::Blob => false,
CellType::Remove => false,
CellType::Ignore => false,
CellType::Other => false,
CellType::Solid => true,
CellType::Liquid => true,
CellType::Sand => true,
CellType::Physics => true,
}
{
if matches!(chunk[n], CellType::Liquid) {
self.pixels.insert(n, p);
self.count += 1;
}
p.pos.x = c.0 as f32 + 0.5;
p.pos.y = c.1 as f32 + 0.5;
if p.stop.is_none() {
p.stop = Some(0)
}
self.pixels.insert(c, p);
false
} else if let Some(b) = self.pixels.remove(&n) {
if self.run(n, b, theta, world, start, array) && !self.pixels.contains_key(&n) {
p.stop = None;
self.pixels.insert(n, p);
true
} else {
p.pos.x = c.0 as f32 + 0.5;
p.pos.y = c.1 as f32 + 0.5;
if p.stop.is_none() {
p.stop = Some(0)
}
self.pixels.insert(c, p);
false
}
} else {
p.stop = None;
self.pixels.insert(n, p);
true
}
} else {
self.pixels.insert(c, p);
false
}
}
}

246
blob_guy/src/chunk.rs Normal file
View file

@ -0,0 +1,246 @@
use crate::blob_guy::OFFSET;
use crate::{CHUNK_AMOUNT, CHUNK_SIZE};
use eyre::{ContextCompat, eyre};
use noita_api::heap;
use noita_api::noita::types;
use noita_api::noita::world::ParticleWorldState;
use rayon::iter::{
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
};
use std::ops::{Index, IndexMut};
use std::slice::{Iter, IterMut};
#[derive(Debug)]
pub struct Chunk {
pub data: [CellType; CHUNK_SIZE * CHUNK_SIZE],
pub modified: bool,
}
#[derive(Default, Copy, Clone, Debug)]
pub enum CellType {
#[default]
Unknown,
Solid,
Liquid,
Sand,
Blob,
Remove,
Ignore,
Physics,
Other,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct Pos {
pub x: f32,
pub y: f32,
}
#[derive(Default)]
pub struct Chunks(pub [Chunk; CHUNK_AMOUNT * CHUNK_AMOUNT]);
impl Chunks {
pub fn read(
&mut self,
particle_world_state: &ParticleWorldState,
blob_guy: u16,
start: ChunkPos,
) -> eyre::Result<()> {
self.0
.par_iter_mut()
.enumerate()
.try_for_each(|(i, chunk)| unsafe {
let x = i as isize / CHUNK_AMOUNT as isize + start.x;
let y = i as isize % CHUNK_AMOUNT as isize + start.y;
particle_world_state.encode_area(x - OFFSET, y - OFFSET, chunk, blob_guy)
})
}
pub fn paint(
&mut self,
particle_world_state: &mut ParticleWorldState,
blob_guy: u16,
start: ChunkPos,
) {
self.0.par_iter().enumerate().for_each(|(i, chunk)| unsafe {
let x = i as isize / CHUNK_AMOUNT as isize + start.x;
let y = i as isize % CHUNK_AMOUNT as isize + start.y;
let _ = particle_world_state.decode_area(x - OFFSET, y - OFFSET, chunk, blob_guy);
});
}
}
impl Pos {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn to_chunk(self) -> ChunkPos {
ChunkPos::new(self.x.floor() as isize, self.y.floor() as isize)
}
}
impl Default for Chunk {
fn default() -> Self {
Self {
data: [CellType::Unknown; CHUNK_SIZE * CHUNK_SIZE],
modified: false,
}
}
}
impl Index<usize> for Chunk {
type Output = CellType;
fn index(&self, index: usize) -> &Self::Output {
&self.data[index]
}
}
impl IndexMut<usize> for Chunk {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.data[index]
}
}
impl Index<(isize, isize)> for Chunk {
type Output = CellType;
fn index(&self, (x, y): (isize, isize)) -> &Self::Output {
let n = x.rem_euclid(CHUNK_SIZE as isize) as usize * CHUNK_SIZE
+ y.rem_euclid(CHUNK_SIZE as isize) as usize;
&self.data[n]
}
}
impl IndexMut<(isize, isize)> for Chunk {
fn index_mut(&mut self, (x, y): (isize, isize)) -> &mut Self::Output {
let n = x.rem_euclid(CHUNK_SIZE as isize) as usize * CHUNK_SIZE
+ y.rem_euclid(CHUNK_SIZE as isize) as usize;
&mut self.data[n]
}
}
impl Chunk {
pub fn iter_mut(&mut self) -> IterMut<'_, CellType> {
self.data.iter_mut()
}
pub fn iter(&self) -> Iter<'_, CellType> {
self.data.iter()
}
}
#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy)]
pub struct ChunkPos {
pub x: isize,
pub y: isize,
}
impl ChunkPos {
pub fn new(x: isize, y: isize) -> Self {
Self {
x: x.div_euclid(CHUNK_SIZE as isize),
y: y.div_euclid(CHUNK_SIZE as isize),
}
}
pub fn get_world(self, start: Self) -> isize {
(self.x - start.x + OFFSET) * CHUNK_AMOUNT as isize + (self.y - start.y + OFFSET)
}
}
pub trait ChunkOps {
///# Safety
unsafe fn encode_area(
&self,
x: isize,
y: isize,
chunk: &mut Chunk,
blob: u16,
) -> eyre::Result<()>;
///# Safety
unsafe fn decode_area(&self, x: isize, y: isize, chunk: &Chunk, blob: u16) -> eyre::Result<()>;
}
pub const SCALE: isize = (512 / CHUNK_SIZE as isize).ilog2() as isize;
impl ChunkOps for ParticleWorldState {
///# Safety
unsafe fn encode_area(
&self,
x: isize,
y: isize,
chunk: &mut Chunk,
blob: u16,
) -> eyre::Result<()> {
let (shift_x, shift_y) = self.get_shift::<CHUNK_SIZE>(x, y);
let Some(pixel_array) = unsafe { self.world_ptr.as_mut() }
.wrap_err("no world")?
.chunk_map
.get(x >> SCALE, y >> SCALE)
else {
return Err(eyre!("chunk not loaded"));
};
let mut modified = false;
for ((i, j), pixel) in (0..CHUNK_SIZE as isize)
.flat_map(|i| (0..CHUNK_SIZE as isize).map(move |j| (i, j)))
.zip(chunk.iter_mut())
{
*pixel = if let Some(cell) = pixel_array.get(shift_x + i, shift_y + j) {
match cell.material.cell_type {
types::CellType::Liquid => {
if cell.material.material_type as u16 == blob {
modified = true;
CellType::Remove
} else {
let lcell = cell.get_liquid();
if lcell.is_static {
CellType::Solid
} else if cell.material.liquid_sand {
CellType::Sand
} else {
CellType::Liquid
}
}
}
types::CellType::Solid => CellType::Physics,
types::CellType::Fire | types::CellType::Gas => CellType::Other,
_ => CellType::Unknown,
}
} else {
CellType::Unknown
}
}
chunk.modified = modified;
Ok(())
}
///# Safety
unsafe fn decode_area(
&self,
cx: isize,
cy: isize,
chunk: &Chunk,
blob: u16,
) -> eyre::Result<()> {
if !chunk.modified {
return Ok(());
}
let Some(pixel_array) = unsafe { self.world_ptr.as_mut() }
.wrap_err("no world")?
.chunk_map
.get_mut(cx >> SCALE, cy >> SCALE)
else {
return Err(eyre!("chunk not loaded"));
};
let (shift_x, shift_y) = self.get_shift::<CHUNK_SIZE>(cx, cy);
let x = cx * CHUNK_SIZE as isize;
let y = cy * CHUNK_SIZE as isize;
let blob_cell = unsafe {
Box::new(types::LiquidCell::create(
self.material_list.get_static(blob as usize).unwrap(),
self.cell_vtables.liquid(),
self.world_ptr,
))
};
for ((i, j), pixel) in (0..CHUNK_SIZE as isize)
.flat_map(|i| (0..CHUNK_SIZE as isize).map(move |j| (i, j)))
.zip(chunk.iter())
{
match pixel {
CellType::Blob => {
let world_x = x + i;
let world_y = y + j;
let cell = pixel_array.get_mut_raw(shift_x + i, shift_y + j);
let new = heap::place_new_ref(*blob_cell.clone());
new.x = world_x;
new.y = world_y;
*cell = (new as *mut types::LiquidCell).cast();
}
CellType::Remove => {
let cell = pixel_array.get_mut_raw(shift_x + i, shift_y + j);
*cell = std::ptr::null_mut();
}
_ => {}
}
}
Ok(())
}
}

62
blob_guy/src/lib.rs Normal file
View file

@ -0,0 +1,62 @@
pub mod blob_guy;
pub mod chunk;
use crate::blob_guy::Blob;
use crate::chunk::Chunks;
use noita_api::add_lua_fn;
use noita_api::lua::LUA;
use noita_api::lua::LuaState;
use noita_api::lua::lua_bindings::{LUA_REGISTRYINDEX, lua_State};
use noita_api::noita::world::ParticleWorldState;
use smallvec::SmallVec;
use std::cell::RefCell;
use std::ffi::c_int;
use std::mem::MaybeUninit;
pub const CHUNK_SIZE: usize = 128;
pub const CHUNK_AMOUNT: usize = 3;
struct State {
particle_world_state: MaybeUninit<ParticleWorldState>,
blobs: SmallVec<[Blob; 8]>,
world: Chunks,
blob_guy: u16,
}
thread_local! {
static STATE: RefCell<State> = State {
particle_world_state: MaybeUninit::uninit(),
blobs: Default::default(),
world: Default::default(),
blob_guy: 0,
}.into();
}
/// # Safety
///
/// Only gets called by lua when loading a module.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaopen_blob_guy(lua: *mut lua_State) -> c_int {
unsafe {
LUA.lua_createtable(lua, 0, 0);
LUA.lua_createtable(lua, 0, 0);
LUA.lua_setmetatable(lua, -2);
LUA.lua_newuserdata(lua, 0);
LUA.lua_createtable(lua, 0, 0);
LUA.lua_setmetatable(lua, -2);
LUA.lua_setfield(lua, LUA_REGISTRYINDEX, c"luaclose_blob_guy".as_ptr());
add_lua_fn!(init_particle_world_state);
add_lua_fn!(update);
}
1
}
fn init_particle_world_state(_: LuaState) -> eyre::Result<()> {
STATE.with(|state| {
let mut state = state.try_borrow_mut()?;
let blob_guy = noita_api::raw::cell_factory_get_type("blob_guy".into())? as u16;
state.blob_guy = blob_guy;
state.particle_world_state = MaybeUninit::new(ParticleWorldState::new()?);
Ok(())
})
}
fn update(_: LuaState) -> eyre::Result<()> {
STATE.with(|state| {
let mut state = state.try_borrow_mut()?;
state.update()
})
}

314
ewext/Cargo.lock generated
View file

@ -2,20 +2,11 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "arrayvec"
@ -23,21 +14,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "base64"
version = "0.22.1"
@ -52,9 +28,9 @@ checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
[[package]]
name = "bitcode"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18c1406a27371b2f76232a2259df6ab607b91b5a0a7476a7729ff590df5a969a"
checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74"
dependencies = [
"arrayvec",
"bitcode_derive",
@ -65,45 +41,77 @@ dependencies = [
[[package]]
name = "bitcode_derive"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23"
checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bytemuck"
version = "1.22.0"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "ewext"
version = "1.5.0"
version = "1.6.0"
dependencies = [
"backtrace",
"bimap",
"eyre",
"iced-x86",
"libloading",
"noita_api",
"noita_api_macro",
"rand",
"rayon",
"rustc-hash",
"shared",
]
@ -119,28 +127,32 @@ dependencies = [
]
[[package]]
name = "getrandom"
version = "0.3.1"
name = "flate2"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"cfg-if",
"libc",
"wasi",
"windows-targets",
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "gimli"
version = "0.31.1"
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "glam"
version = "0.30.0"
version = "0.30.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fcdf9683c406c2fc4d124afd29c0d595e22210d633cbdb8695ba9935ab1dc6"
checksum = "f2d1aab06663bdce00d6ca5e5ed586ec8d18033a771906c993a1e3755b368d85"
[[package]]
name = "heck"
@ -159,9 +171,9 @@ dependencies = [
[[package]]
name = "indenter"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]]
name = "itoa"
@ -177,15 +189,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libloading"
version = "0.8.6"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets",
@ -193,28 +205,33 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "noita_api"
version = "0.1.0"
version = "1.6.0"
dependencies = [
"base64",
"eyre",
"iced-x86",
"libloading",
"noita_api_macro",
"object",
"rayon",
"rustc-hash",
"shared",
"smallvec",
]
[[package]]
@ -230,18 +247,20 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.7"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"flate2",
"memchr",
"ruzstd",
]
[[package]]
name = "once_cell"
version = "1.21.0"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pin-project-lite"
@ -260,9 +279,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@ -277,14 +296,19 @@ dependencies = [
]
[[package]]
name = "rand"
version = "0.9.0"
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
"zerocopy",
]
[[package]]
@ -307,10 +331,24 @@ dependencies = [
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rustc-hash"
@ -319,10 +357,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustversion"
version = "1.0.20"
name = "ruzstd"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c"
dependencies = [
"twox-hash",
]
[[package]]
name = "ryu"
@ -352,9 +393,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
@ -373,32 +414,37 @@ dependencies = [
]
[[package]]
name = "strum"
version = "0.27.1"
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@ -418,9 +464,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.28"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
@ -429,13 +475,19 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
]
[[package]]
name = "twox-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56"
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -444,19 +496,26 @@ checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-targets"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
@ -469,75 +528,72 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
name = "wit-bindgen"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
[[package]]
name = "zerocopy"
version = "0.8.23"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,6 +1,6 @@
[package]
name = "ewext"
version = "1.5.0"
version = "1.6.0"
edition = "2024"
[lib]
@ -9,20 +9,22 @@ crate-type = ["cdylib"]
[profile.release]
lto = true
#strip = true # Not having that causes wine debugger to crash.
strip = true # Not having that causes wine debugger to crash.
panic = "abort"
split-debuginfo = "packed"
incremental=true
codegen-units=1
opt-level = 3
[dependencies]
backtrace = "0.3.74"
iced-x86 = "1.21.0"
noita_api_macro = {path = "noita_api_macro"}
eyre = "0.6.12"
noita_api = {path = "noita_api"}
noita_api = {path = "../noita_api"}
shared = {path = "../shared"}
libloading = "0.8.6"
rand = "0.9.0"
rustc-hash = "2.0.0"
libloading = "0.8.8"
rand = "0.9.2"
rustc-hash = "2.1.1"
bimap = "0.6.3"
rayon = "1.11.0"
[features]
#enables cross-compilation on older systems (for example, when compiling on ubuntu 20.04)

View file

@ -1,11 +0,0 @@
[package]
name = "noita_api"
version = "0.1.0"
edition = "2024"
[dependencies]
eyre = "0.6.12"
libloading = "0.8.5"
noita_api_macro = {path = "../noita_api_macro"}
shared = {path = "../../shared"}
base64 = "0.22.1"

View file

@ -1,773 +0,0 @@
use crate::lua::{LuaGetValue, LuaPutValue};
use crate::serialize::deserialize_entity;
use base64::Engine;
use eyre::{Context, OptionExt, eyre};
use shared::des::Gid;
use shared::{GameEffectData, GameEffectEnum};
use std::collections::HashMap;
use std::{
borrow::Cow,
num::{NonZero, TryFromIntError},
ops::Deref,
};
pub mod lua;
pub mod serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EntityID(pub NonZero<isize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ComponentID(pub NonZero<isize>);
pub struct Obj(pub usize);
pub struct Color(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PhysicsBodyID(pub i32);
pub trait Component: From<ComponentID> + Into<ComponentID> + Deref<Target = ComponentID> {
const NAME_STR: &'static str;
}
noita_api_macro::generate_components!();
impl EntityID {
/// Returns true if entity is alive.
///
/// Corresponds to EntityGetIsAlive from lua api.
pub fn is_alive(self) -> bool {
raw::entity_get_is_alive(self).unwrap_or(false)
}
pub fn name(self) -> eyre::Result<String> {
raw::entity_get_name(self).map(|s| s.to_string())
}
pub fn handle_poly(&self) -> eyre::Result<Option<Gid>> {
for ent in self.children(None) {
if let Ok(Some(effect)) =
ent.try_get_first_component_including_disabled::<GameEffectComponent>(None)
{
let name = effect.effect()?;
match name {
GameEffectEnum::Polymorph
| GameEffectEnum::PolymorphRandom
| GameEffectEnum::PolymorphUnstable
| GameEffectEnum::PolymorphCessation => {
if let Ok(data) =
raw::component_get_value::<Cow<str>>(effect.0, "mSerializedData")
{
if data.is_empty() {
return Ok(None);
}
if let Ok(data) =
base64::engine::general_purpose::STANDARD.decode(data.to_string())
{
let data = String::from_utf8_lossy(&data)
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>();
let mut data = data.split("VariableStorageComponentewgidlid");
let _ = data.next();
if let Some(data) = data.next() {
let mut gid = String::new();
for c in data.chars() {
if c.is_numeric() {
gid.push(c)
} else {
break;
}
}
return Ok(Some(Gid(gid.parse::<u64>()?)));
}
}
}
return Ok(None);
}
_ => {}
}
}
}
Ok(None)
}
pub fn add_tag(self, tag: impl AsRef<str>) -> eyre::Result<()> {
raw::entity_add_tag(self, tag.as_ref().into())
}
/// Returns true if entity has a tag.
///
/// Corresponds to EntityGetTag from lua api.
pub fn has_tag(self, tag: impl AsRef<str>) -> bool {
raw::entity_has_tag(self, tag.as_ref().into()).unwrap_or(false)
}
pub fn remove_tag(self, tag: impl AsRef<str>) -> eyre::Result<()> {
raw::entity_remove_tag(self, tag.as_ref().into())
}
pub fn root(self) -> eyre::Result<Option<EntityID>> {
raw::entity_get_root_entity(self)
}
pub fn check_all_phys_init(self) -> eyre::Result<bool> {
for phys_c in self.iter_all_components_of_type::<PhysicsBody2Component>(None)? {
if !phys_c.m_initialized()? {
return Ok(false);
}
}
Ok(true)
}
pub fn kill(self) {
// Shouldn't ever error.
if self.is_alive()
&& self
.try_get_first_component_including_disabled::<CellEaterComponent>(None)
.ok()
.map(|a| a.is_none())
.unwrap_or(true)
&& self.check_all_phys_init().unwrap_or(false)
{
let body_id = raw::physics_body_id_get_from_entity(self, None).unwrap_or_default();
if !body_id.is_empty() {
for com in raw::entity_get_with_tag("ew_peer".into())
.unwrap_or_default()
.iter()
.filter_map(|e| {
e.map(|e| {
e.try_get_first_component_including_disabled::<TelekinesisComponent>(
None,
)
})
})
.flatten()
.flatten()
{
if body_id.contains(&com.get_body_id()) {
let _ = raw::component_set_value(*com, "mState", 0);
}
}
if body_id.len()
!= self
.iter_all_components_of_type_including_disabled::<PhysicsBodyComponent>(None)
.iter()
.len()
+ self
.iter_all_components_of_type_including_disabled::<PhysicsBody2Component>(
None,
)
.iter()
.len()
{
for (i, id) in body_id.iter().enumerate() {
let n = -10000.0;
let _ = raw::physics_body_id_set_transform(
*id,
n + 64.0 * self.0.get() as f64,
n + 64.0 * i as f64,
0.0,
0.0,
0.0,
0.0,
);
}
}
}
}
let _ = raw::entity_kill(self);
}
pub fn set_position(self, x: f32, y: f32, r: Option<f32>) -> eyre::Result<()> {
raw::entity_set_transform(
self,
x as f64,
Some(y as f64),
r.map(|a| a as f64),
None,
None,
)
}
pub fn set_rotation(self, r: f32) -> eyre::Result<()> {
let (x, y) = self.position()?;
raw::entity_set_transform(self, x as f64, Some(y as f64), Some(r as f64), None, None)
}
pub fn transform(self) -> eyre::Result<(f32, f32, f32, f32, f32)> {
let (a, b, c, d, e) = raw::entity_get_transform(self)?;
Ok((a as f32, b as f32, c as f32, d as f32, e as f32))
}
pub fn position(self) -> eyre::Result<(f32, f32)> {
let (x, y, _, _, _) = raw::entity_get_transform(self)?;
Ok((x as f32, y as f32))
}
pub fn rotation(self) -> eyre::Result<f32> {
let (_, _, r, _, _) = raw::entity_get_transform(self)?;
Ok(r as f32)
}
pub fn filename(self) -> eyre::Result<String> {
raw::entity_get_filename(self).map(|x| x.to_string())
}
pub fn parent(self) -> eyre::Result<EntityID> {
Ok(raw::entity_get_parent(self)?.unwrap_or(self))
}
/// Returns the first component of this type if an entity has it.
pub fn try_get_first_component<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<Option<C>> {
raw::entity_get_first_component(self, C::NAME_STR.into(), tag)
.map(|x| x.flatten().map(Into::into))
.wrap_err_with(|| eyre!("Failed to get first component {} for {self:?}", C::NAME_STR))
}
pub fn try_get_first_component_including_disabled<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<Option<C>> {
raw::entity_get_first_component_including_disabled(self, C::NAME_STR.into(), tag)
.map(|x| x.flatten().map(Into::into))
.wrap_err_with(|| eyre!("Failed to get first component {} for {self:?}", C::NAME_STR))
}
/// Returns the first component of this type if an entity has it.
pub fn get_first_component<C: Component>(self, tag: Option<Cow<'_, str>>) -> eyre::Result<C> {
self.try_get_first_component(tag)?
.ok_or_else(|| eyre!("Entity {self:?} has no component {}", C::NAME_STR))
}
pub fn get_first_component_including_disabled<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<C> {
self.try_get_first_component_including_disabled(tag)?
.ok_or_else(|| eyre!("Entity {self:?} has no component {}", C::NAME_STR))
}
pub fn remove_all_components_of_type<C: Component>(
self,
tags: Option<Cow<str>>,
) -> eyre::Result<bool> {
let mut is_some = false;
while let Some(c) = self.try_get_first_component_including_disabled::<C>(tags.clone())? {
is_some = true;
raw::entity_remove_component(self, c.into())?;
}
Ok(is_some)
}
pub fn iter_all_components_of_type<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<impl Iterator<Item = C>> {
Ok(raw::entity_get_component(self, C::NAME_STR.into(), tag)?
.unwrap_or_default()
.into_iter()
.filter_map(|x| x.map(C::from)))
}
pub fn iter_all_components_of_type_including_disabled<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<impl Iterator<Item = C>> {
Ok(
raw::entity_get_component_including_disabled(self, C::NAME_STR.into(), tag)?
.unwrap_or_default()
.into_iter()
.filter_map(|x| x.map(C::from)),
)
}
pub fn add_component<C: Component>(self) -> eyre::Result<C> {
raw::entity_add_component::<C>(self)?.ok_or_eyre("Couldn't create a component")
}
pub fn get_var(self, name: &str) -> Option<VariableStorageComponent> {
self.iter_all_components_of_type_including_disabled::<VariableStorageComponent>(None)
.map(|mut i| i.find(|var| var.name().unwrap_or("".into()) == name))
.unwrap_or(None)
}
pub fn get_var_or_default(self, name: &str) -> eyre::Result<VariableStorageComponent> {
if let Some(var) = self.get_var(name) {
Ok(var)
} else {
let var = self.add_component::<VariableStorageComponent>()?;
var.set_name(name.into())?;
Ok(var)
}
}
pub fn add_lua_init_component<C: Component>(self, file: &str) -> eyre::Result<C> {
raw::entity_add_lua_init_component::<C>(self, file)?
.ok_or_eyre("Couldn't create a component")
}
pub fn load(
filename: impl AsRef<str>,
pos_x: Option<f64>,
pos_y: Option<f64>,
) -> eyre::Result<Self> {
raw::entity_load(filename.as_ref().into(), pos_x, pos_y)?
.ok_or_else(|| eyre!("Failed to spawn entity from filename {}", filename.as_ref()))
}
pub fn max_in_use() -> eyre::Result<Self> {
Ok(Self::try_from(raw::entities_get_max_id()? as isize)?)
}
/// Returns id+1
pub fn next(self) -> eyre::Result<Self> {
Ok(Self(NonZero::try_from(isize::from(self.0) + 1)?))
}
pub fn raw(self) -> isize {
isize::from(self.0)
}
pub fn children(self, tag: Option<Cow<'_, str>>) -> Vec<EntityID> {
raw::entity_get_all_children(self, tag)
.unwrap_or(None)
.unwrap_or_default()
.iter()
.filter_map(|a| *a)
.collect()
}
pub fn get_game_effects(self) -> eyre::Result<Vec<(GameEffectData, EntityID)>> {
let mut effects = Vec::new();
let mut name_to_n: HashMap<String, i32> = HashMap::default();
for ent in self.children(None) {
if ent.has_tag("projectile") {
if let Ok(data) = serialize::serialize_entity(ent) {
let n = ent.filename().unwrap_or(String::new());
let num = name_to_n.entry(n.clone()).or_insert(0);
*num += 1;
effects.push((
GameEffectData::Projectile((format!("{}{}", n, num), data)),
ent,
));
}
} else if let Ok(Some(effect)) =
ent.try_get_first_component_including_disabled::<GameEffectComponent>(None)
{
let name = effect.effect()?;
match name {
GameEffectEnum::Custom => {
if let Ok(file) = ent.filename() {
if !file.is_empty() {
effects.push((GameEffectData::Custom(file), ent))
}
} /* else if let Ok(data) = serialize::serialize_entity(ent) {
let n = ent.filename().unwrap_or(String::new());
effects.push((GameEffectData::Projectile((n, data)), ent))
}
} else if let Ok(data) = serialize::serialize_entity(ent) {
let n = ent.filename().unwrap_or(String::new());
let num = name_to_n.entry(n.clone()).or_insert(0);
*num += 1;
effects.push((
GameEffectData::Projectile((format!("{}{}", n, num), data)),
ent,
))
}*/
}
GameEffectEnum::Polymorph
| GameEffectEnum::PolymorphRandom
| GameEffectEnum::PolymorphUnstable
| GameEffectEnum::PolymorphCessation => {}
_ => effects.push((GameEffectData::Normal(name), ent)),
}
}
}
Ok(effects)
}
pub fn set_game_effects(self, game_effect: &[GameEffectData]) -> eyre::Result<()> {
fn set_frames(ent: EntityID) -> eyre::Result<()> {
if let Some(effect) =
ent.try_get_first_component_including_disabled::<GameEffectComponent>(None)?
{
if effect.frames()? >= 0 {
effect.set_frames(i32::MAX)?;
}
}
if let Some(life) =
ent.try_get_first_component_including_disabled::<LifetimeComponent>(None)?
{
if life.lifetime()? >= 0 {
life.set_lifetime(i32::MAX)?;
}
}
Ok(())
}
let local_effects = self.get_game_effects()?;
for (i, (e1, ent)) in local_effects.iter().enumerate() {
if let GameEffectData::Normal(e1) = e1 {
if *e1 == GameEffectEnum::Polymorph
|| *e1 == GameEffectEnum::PolymorphRandom
|| *e1 == GameEffectEnum::PolymorphUnstable
|| *e1 == GameEffectEnum::PolymorphCessation
{
ent.kill();
continue;
}
}
for (j, (e2, _)) in local_effects.iter().enumerate() {
if i < j && e1 == e2 {
ent.kill()
}
}
}
let local_effects = self.get_game_effects()?;
for effect in game_effect {
if let Some(ent) = local_effects
.iter()
.find_map(|(e, ent)| if e == effect { Some(ent) } else { None })
{
let _ = set_frames(*ent);
} else {
let ent = match effect {
GameEffectData::Normal(e) => {
let e: &str = e.into();
if let Ok(ent) = NonZero::try_from(
raw::get_game_effect_load_to(self, e.into(), true)
.unwrap_or_default()
.1 as isize,
) {
EntityID(ent)
} else {
continue;
}
}
GameEffectData::Custom(file) => {
let (x, y) = self.position().unwrap_or_default();
if let Ok(Some(ent)) =
raw::entity_load(file.into(), Some(x as f64), Some(y as f64))
{
self.add_child(ent);
ent
} else {
continue;
}
}
GameEffectData::Projectile((_, data)) => {
let (x, y) = self.position().unwrap_or_default();
if let Ok(ent) = deserialize_entity(data, x, y) {
self.add_child(ent);
ent
} else {
continue;
}
}
};
let _ = set_frames(ent);
}
}
let local_effects = self.get_game_effects()?;
for (effect, ent) in local_effects {
if game_effect.iter().all(|e| *e != effect) {
ent.kill()
}
}
if let Ok(damage) = self.get_first_component::<DamageModelComponent>(None) {
if game_effect
.iter()
.any(|e| e == &GameEffectData::Normal(GameEffectEnum::OnFire))
{
let _ = damage.set_m_fire_probability(100);
let _ = damage.set_m_fire_probability(1600);
let _ = damage.set_m_fire_probability(1600);
} else {
let _ = damage.set_m_fire_probability(0);
let _ = damage.set_m_fire_probability(0);
let _ = damage.set_m_fire_probability(0);
}
}
Ok(())
}
pub fn add_child(self, child: EntityID) {
let _ = raw::entity_add_child(self.0.get() as i32, child.0.get() as i32);
}
pub fn get_current_stains(self) -> eyre::Result<u64> {
let mut current = 0;
if let Ok(Some(status)) = self.try_get_first_component::<StatusEffectDataComponent>(None) {
for (i, v) in status.stain_effects()?.iter().enumerate() {
if *v >= 0.15 {
current += 1 << i
}
}
}
Ok(current)
}
pub fn set_current_stains(self, current_stains: u64) -> eyre::Result<()> {
if let Ok(Some(status)) = self.try_get_first_component::<StatusEffectDataComponent>(None) {
let file = raw::mod_text_file_get_content(
"data/scripts/status_effects/status_list.lua".into(),
)?;
let to_id = file
.lines()
.filter(|l| {
!l.starts_with("-") && l.contains("id=\"") && !l.contains("BRAIN_DAMAGE")
})
.map(|l| {
l.split("\"")
.map(|a| a.to_string())
.collect::<Vec<String>>()[1]
.clone()
})
.collect::<Vec<String>>();
for ((i, v), id) in status.stain_effects()?.iter().enumerate().zip(to_id) {
if *v >= 0.15 && current_stains & (1 << i) == 0 {
raw::entity_remove_stain_status_effect(self.0.get() as i32, id.into(), None)?
}
}
}
Ok(())
}
pub fn set_components_with_tag_enabled(
self,
tag: Cow<'_, str>,
enabled: bool,
) -> eyre::Result<()> {
raw::entity_set_components_with_tag_enabled(self, tag, enabled)
}
pub fn set_component_enabled(self, com: ComponentID, enabled: bool) -> eyre::Result<()> {
raw::entity_set_component_is_enabled(self, com, enabled)
}
pub fn remove_component(self, component_id: ComponentID) -> eyre::Result<()> {
raw::entity_remove_component(self, component_id)
}
}
impl TryFrom<isize> for EntityID {
type Error = TryFromIntError;
fn try_from(value: isize) -> Result<Self, Self::Error> {
NonZero::<isize>::try_from(value).map(Self)
}
}
impl ComponentID {
pub fn add_tag(self, tag: impl AsRef<str>) -> eyre::Result<()> {
raw::component_add_tag(self, tag.as_ref().into())
}
pub fn has_tag(self, tag: impl AsRef<str>) -> bool {
raw::component_has_tag(self, tag.as_ref().into()).unwrap_or(false)
}
pub fn remove_tag(self, tag: impl AsRef<str>) -> eyre::Result<()> {
raw::component_remove_tag(self, tag.as_ref().into())
}
pub fn object_set_value<T>(self, object: &str, key: &str, value: T) -> eyre::Result<()>
where
T: LuaPutValue,
{
raw::component_object_set_value::<T>(self, object, key, value)?;
Ok(())
}
pub fn object_get_value<T>(self, object: &str, key: &str) -> eyre::Result<T>
where
T: LuaGetValue,
{
raw::component_object_get_value::<T>(self, object, key)
}
}
impl StatusEffectDataComponent {
pub fn stain_effects(self) -> eyre::Result<Vec<f32>> {
let v: Vec<f32> = raw::component_get_value::<Vec<f32>>(self.0, "stain_effects")?;
Ok(v[1..].to_vec())
}
}
impl TelekinesisComponent {
pub fn get_body_id(self) -> PhysicsBodyID {
raw::component_get_value_old::<PhysicsBodyID>(*self, "mBodyID").unwrap_or(PhysicsBodyID(0))
}
}
pub fn game_print(value: impl AsRef<str>) {
let _ = raw::game_print(value.as_ref().into());
}
pub mod raw {
use eyre::Context;
use eyre::eyre;
use super::{Color, ComponentID, EntityID, Obj, PhysData, PhysicsBodyID};
use crate::Component;
use crate::lua::LuaGetValue;
use crate::lua::LuaPutValue;
use std::borrow::Cow;
use std::num::NonZero;
use crate::lua::LuaState;
noita_api_macro::generate_api!();
pub(crate) fn component_get_value<T>(component: ComponentID, field: &str) -> eyre::Result<T>
where
T: LuaGetValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentGetValue2");
lua.push_integer(component.0.into());
lua.push_string(field);
lua.call(2, T::size_on_stack())
.wrap_err("Failed to call ComponentGetValue2")?;
let ret = T::get(lua, -1);
lua.pop_last_n(T::size_on_stack());
ret.wrap_err_with(|| eyre!("Getting {field} for {component:?}"))
}
pub(crate) fn component_get_value_old<T>(component: ComponentID, field: &str) -> eyre::Result<T>
where
T: LuaGetValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentGetValue");
lua.push_integer(component.0.into());
lua.push_string(field);
lua.call(2, T::size_on_stack())
.wrap_err("Failed to call ComponentGetValue")?;
let ret = T::get(lua, -1);
lua.pop_last_n(T::size_on_stack());
ret.wrap_err_with(|| eyre!("Getting {field} for {component:?}"))
}
pub(crate) fn component_object_get_value<T>(
component: ComponentID,
object: &str,
field: &str,
) -> eyre::Result<T>
where
T: LuaGetValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentObjectGetValue2");
lua.push_integer(component.0.into());
lua.push_string(object);
lua.push_string(field);
lua.call(3, T::size_on_stack())
.wrap_err("Failed to call ComponentObjectGetValue2")?;
let ret = T::get(lua, -1);
lua.pop_last_n(T::size_on_stack());
ret.wrap_err_with(|| eyre!("Getting {field} from {object} for {component:?}"))
}
pub(crate) fn component_set_value<T>(
component: ComponentID,
field: &str,
value: T,
) -> eyre::Result<()>
where
T: LuaPutValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentSetValue2");
lua.push_integer(component.0.into());
lua.push_string(field);
value.put(lua);
lua.call((2 + T::SIZE_ON_STACK).try_into()?, 0)
.wrap_err("Failed to call ComponentSetValue2")?;
Ok(())
}
pub(crate) fn component_object_set_value<T>(
component: ComponentID,
object: &str,
field: &str,
value: T,
) -> eyre::Result<()>
where
T: LuaPutValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentObjectSetValue2");
lua.push_integer(component.0.into());
lua.push_string(object);
lua.push_string(field);
value.put(lua);
lua.call((3 + T::SIZE_ON_STACK).try_into()?, 0)
.wrap_err("Failed to call ComponentObjectSetValue2")?;
Ok(())
}
pub fn physics_body_id_get_transform(body: PhysicsBodyID) -> eyre::Result<Option<PhysData>> {
let lua = LuaState::current()?;
lua.get_global(c"PhysicsBodyIDGetTransform");
lua.push_integer(body.0 as isize);
lua.call(1, 6)
.wrap_err("Failed to call PhysicsBodyIDGetTransform")?;
if lua.is_nil_or_none(-1) {
Ok(None)
} else {
match LuaGetValue::get(lua, -1) {
Ok(ret) => {
let ret: (f32, f32, f32, f32, f32, f32) = ret;
lua.pop_last_n(6);
Ok(Some(PhysData {
x: ret.0,
y: ret.1,
angle: ret.2,
vx: ret.3,
vy: ret.4,
av: ret.5,
}))
}
Err(err) => {
lua.pop_last_n(6);
Err(err)
}
}
}
}
pub fn entity_add_component<C: Component>(entity: EntityID) -> eyre::Result<Option<C>> {
let lua = LuaState::current()?;
lua.get_global(c"EntityAddComponent2");
lua.push_integer(entity.raw());
lua.push_string(C::NAME_STR);
lua.call(2, 1)
.wrap_err("Failed to call EntityAddComponent2")?;
let c = lua.to_integer(-1);
lua.pop_last_n(1);
Ok(NonZero::new(c).map(ComponentID).map(C::from))
}
pub fn entity_add_lua_init_component<C: Component>(
entity: EntityID,
file: &str,
) -> eyre::Result<Option<C>> {
let lua = LuaState::current()?;
lua.get_global(c"EwextAddInitLuaComponent");
lua.push_integer(entity.raw());
lua.push_string(file);
lua.call(2, 1)
.wrap_err("Failed to call EntityAddComponent2")?;
let c = lua.to_integer(-1);
lua.pop_last_n(1);
Ok(NonZero::new(c).map(ComponentID).map(C::from))
}
}
pub struct PhysData {
pub x: f32,
pub y: f32,
pub angle: f32,
pub vx: f32,
pub vy: f32,
pub av: f32,
}

View file

@ -1,109 +0,0 @@
use std::{mem, os::raw::c_void, ptr, sync::OnceLock};
use iced_x86::{Decoder, DecoderOptions, Mnemonic};
use noita_api::lua::LuaState;
use crate::noita::ntypes::{EntityManager, ThiscallFn};
static GRABBED: OnceLock<Grabbed> = OnceLock::new();
pub(crate) unsafe fn grab_addr_from_instruction(
func: *const c_void,
offset: isize,
expected_mnemonic: Mnemonic,
) -> *mut c_void {
let instruction_addr = func.wrapping_offset(offset);
// We don't really have an idea of how many bytes the instruction takes, so just take *enough* bytes for most cases.
let instruction_bytes = unsafe { ptr::read_unaligned(instruction_addr.cast::<[u8; 16]>()) };
let mut decoder = Decoder::with_ip(
32,
&instruction_bytes,
instruction_addr as u64,
DecoderOptions::NONE,
);
let instruction = decoder.decode();
if instruction.mnemonic() != expected_mnemonic {
println!("Encountered unexpected mnemonic: {}", instruction);
}
assert_eq!(instruction.mnemonic(), expected_mnemonic);
instruction.memory_displacement32() as *mut c_void
}
struct Grabbed {
globals: GrabbedGlobals,
fns: GrabbedFns,
}
// This only stores pointers that are constant, so should be safe to share between threads.
unsafe impl Sync for Grabbed {}
unsafe impl Send for Grabbed {}
pub(crate) struct GrabbedGlobals {
// These 3 actually point to a pointer.
pub(crate) _game_global: *mut usize,
pub(crate) _world_state_entity: *mut usize,
pub(crate) entity_manager: *const *mut EntityManager,
}
pub(crate) struct GrabbedFns {
pub(crate) get_entity: *const ThiscallFn, //unsafe extern "C" fn(*const EntityManager, u32) -> *mut Entity,
}
pub(crate) fn grab_addrs(lua: LuaState) {
lua.get_global(c"GameGetWorldStateEntity");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let world_state_entity =
unsafe { grab_addr_from_instruction(base, 0x007aa7ce - 0x007aa540, Mnemonic::Mov).cast() };
println!(
"World state entity addr: 0x{:x}",
world_state_entity as usize
);
lua.pop_last();
lua.get_global(c"GameGetFrameNum");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let load_game_global =
unsafe { grab_addr_from_instruction(base, 0x007bf3c9 - 0x007bf140, Mnemonic::Call) }; // CALL load_game_global
println!("Load game global addr: 0x{:x}", load_game_global as usize);
let game_global = unsafe {
grab_addr_from_instruction(load_game_global, 0x00439c17 - 0x00439bb0, Mnemonic::Mov).cast()
};
println!("Game global addr: 0x{:x}", game_global as usize);
lua.pop_last();
lua.get_global(c"EntityGetFilename");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let get_entity = unsafe {
mem::transmute_copy(&grab_addr_from_instruction(
base,
0x0079782b - 0x00797570,
Mnemonic::Call,
))
};
println!("get_entity addr: 0x{:x}", get_entity as usize);
let entity_manager =
unsafe { grab_addr_from_instruction(base, 0x00797821 - 0x00797570, Mnemonic::Mov).cast() };
println!("entity_manager addr: 0x{:x}", entity_manager as usize);
lua.pop_last();
GRABBED
.set(Grabbed {
globals: GrabbedGlobals {
_game_global: game_global,
_world_state_entity: world_state_entity,
entity_manager,
},
fns: GrabbedFns { get_entity },
})
.ok();
}
pub(crate) fn grabbed_fns() -> &'static GrabbedFns {
&GRABBED.get().expect("to be initialized early").fns
}
pub(crate) fn grabbed_globals() -> &'static GrabbedGlobals {
&GRABBED.get().expect("to be initialized early").globals
}

View file

@ -2,44 +2,40 @@
#[unsafe(no_mangle)]
pub extern "C" fn _unwind_resume() {}
use addr_grabber::{grab_addrs, grabbed_fns, grabbed_globals};
use bimap::BiHashMap;
use eyre::{Context, OptionExt, bail};
use modules::{Module, ModuleCtx, entity_sync::EntitySync};
use net::NetManager;
use noita::{ParticleWorldState, ntypes::Entity, pixel::NoitaPixelRun};
use noita_api::add_lua_fn;
use noita_api::addr_grabber::Globals;
use noita_api::heap::raw_new;
use noita_api::noita::types::EntityManager;
use noita_api::noita::world::ParticleWorldState;
use noita_api::{
DamageModelComponent, EntityID, VariableStorageComponent,
EntityID, VariableStorageComponent,
lua::{
LUA, LuaFnRet, LuaGetValue, LuaState, RawString, ValuesOnStack,
LUA, LuaGetValue, LuaState, RawString,
lua_bindings::{LUA_REGISTRYINDEX, lua_State},
},
};
use noita_api_macro::add_lua_fn;
use rustc_hash::{FxHashMap, FxHashSet};
use shared::des::{Gid, RemoteDes};
use shared::{Destination, NoitaInbound, NoitaOutbound, PeerId, ProxyKV, SpawnOnce, WorldPos};
use shared::{Destination, NoitaInbound, NoitaOutbound, PeerId, SpawnOnce, WorldPos};
use std::array::IntoIter;
use std::backtrace::Backtrace;
use std::mem::MaybeUninit;
use std::{
arch::asm,
borrow::Cow,
cell::{LazyCell, RefCell},
ffi::{c_int, c_void},
cell::RefCell,
ffi::c_int,
sync::{LazyLock, Mutex, OnceLock, TryLockError},
thread,
time::Instant,
};
use std::{num::NonZero, sync::MutexGuard};
mod addr_grabber;
mod modules;
mod net;
pub mod noita;
thread_local! {
static STATE: LazyCell<RefCell<ExtState>> = LazyCell::new(|| {
println!("Initializing ExtState");
ExtState::default().into()
});
static STATE: RefCell<ExtState> = Default::default()
}
/// This has a mutex because noita could call us from different threads.
@ -47,7 +43,7 @@ thread_local! {
static NETMANAGER: LazyLock<Mutex<Option<NetManager>>> = LazyLock::new(Default::default);
static KEEP_SELF_LOADED: LazyLock<Result<libloading::Library, libloading::Error>> =
LazyLock::new(|| unsafe { libloading::Library::new("ewext1.dll") });
LazyLock::new(|| unsafe { libloading::Library::new("ewext.dll") });
static MY_PEER_ID: OnceLock<PeerId> = OnceLock::new();
fn try_lock_netmanager() -> eyre::Result<MutexGuard<'static, Option<NetManager>>> {
@ -65,46 +61,42 @@ pub(crate) fn my_peer_id() -> PeerId {
.expect("peer id to be set by this point")
}
/*pub struct TimeTracker {
start: Instant,
message: &'static str,
pub struct WorldSync {
pub particle_world_state: MaybeUninit<ParticleWorldState>,
pub world_num: u8,
}
impl TimeTracker {
pub fn new(message: &'static str) -> Self {
unsafe impl Sync for WorldSync {}
unsafe impl Send for WorldSync {}
impl Default for WorldSync {
fn default() -> Self {
Self {
start: Instant::now(),
message,
particle_world_state: MaybeUninit::uninit(),
world_num: 0,
}
}
}
impl Drop for TimeTracker {
fn drop(&mut self) {
/*let elapsed = self.start.elapsed();
if elapsed.as_millis() > 1 {
game_print(format!(
"ewext {} took longer than expected: {} us",
self.message,
elapsed.as_micros(),
));
}*/
}
}*/
#[derive(Default)]
struct Modules {
entity_sync: Option<EntitySync>,
entity_sync: EntitySync,
world: WorldSync,
}
impl Modules {
fn iter_mut(&mut self) -> IntoIter<&mut dyn Module, 2> {
let modules: [&mut dyn Module; 2] = [&mut self.entity_sync, &mut self.world];
modules.into_iter()
}
}
#[derive(Default)]
struct ExtState {
particle_world_state: Option<ParticleWorldState>,
modules: Modules,
player_entity_map: BiHashMap<PeerId, EntityID>,
fps_by_player: FxHashMap<PeerId, u8>,
dont_spawn: FxHashSet<Gid>,
cam_pos: FxHashMap<PeerId, WorldPos>,
globals: Globals,
}
impl ExtState {
@ -117,84 +109,22 @@ impl ExtState {
})
}
}
fn init_particle_world_state(lua: LuaState) {
println!("\nInitializing particle world state");
let world_pointer = lua.to_integer(1);
let chunk_map_pointer = lua.to_integer(2);
let material_list_pointer = lua.to_integer(3);
println!("pws stuff: {world_pointer:?} {chunk_map_pointer:?}");
STATE.with(|state| {
state.borrow_mut().particle_world_state = Some(ParticleWorldState {
_world_ptr: world_pointer as *mut c_void,
chunk_map_ptr: chunk_map_pointer as *mut c_void,
material_list_ptr: material_list_pointer as _,
runner: Default::default(),
});
});
}
fn encode_area(lua: LuaState) -> ValuesOnStack {
let lua = lua.raw();
let start_x = unsafe { LUA.lua_tointeger(lua, 1) } as i32;
let start_y = unsafe { LUA.lua_tointeger(lua, 2) } as i32;
let end_x = unsafe { LUA.lua_tointeger(lua, 3) } as i32;
let end_y = unsafe { LUA.lua_tointeger(lua, 4) } as i32;
let encoded_buffer = unsafe { LUA.lua_tointeger(lua, 5) } as *mut NoitaPixelRun;
STATE.with(|state| {
let mut state = state.borrow_mut();
let pws = state.particle_world_state.as_mut().unwrap();
let runs = unsafe { pws.encode_area(start_x, start_y, end_x, end_y, encoded_buffer) };
unsafe { LUA.lua_pushinteger(lua, runs as isize) };
});
ValuesOnStack(1)
}
pub fn ephemerial(entity_id: u32) -> eyre::Result<()> {
unsafe {
let entity_manager = grabbed_globals().entity_manager.read();
let mut entity: *mut Entity;
asm!(
"mov ecx, {entity_manager}",
"push {entity_id:e}",
"call {get_entity}",
entity_manager = in(reg) entity_manager,
get_entity = in(reg) grabbed_fns().get_entity,
entity_id = in(reg) entity_id,
clobber_abi("C"),
out("ecx") _,
out("eax") entity,
);
if entity.is_null() {
bail!("Entity {} not found", entity_id);
}
entity.cast::<c_void>().offset(0x8).cast::<u32>().write(0);
pub fn ephemerial(entity_id: usize, em: &mut EntityManager) {
if let Some(entity) = em.get_entity_mut(entity_id) {
entity.filename_index = 0;
}
Ok(())
}
fn make_ephemerial(lua: LuaState) -> eyre::Result<()> {
let entity_id = lua.to_integer(1) as u32;
ephemerial(entity_id)?;
let entity_id = lua.to_integer(1).cast_unsigned();
ephemerial(
entity_id,
ExtState::with_global(|state| state.globals.entity_manager_mut())?,
);
Ok(())
}
struct InitKV {
key: String,
value: String,
}
impl From<ProxyKV> for InitKV {
fn from(value: ProxyKV) -> Self {
InitKV {
key: value.key,
value: value.value,
}
}
}
fn netmanager_connect(_lua: LuaState) -> eyre::Result<Vec<RawString>> {
#[cfg(debug_assertions)]
println!("Connecting to proxy...");
let mut netman = NetManager::new()?;
@ -212,6 +142,7 @@ fn netmanager_connect(_lua: LuaState) -> eyre::Result<Vec<RawString>> {
}
*NETMANAGER.lock().unwrap() = Some(netman);
#[cfg(debug_assertions)]
println!("Ok!");
Ok(kvs)
}
@ -225,8 +156,15 @@ fn netmanager_recv(_lua: LuaState) -> eyre::Result<Option<RawString>> {
NoitaInbound::Ready { .. } => bail!("Unexpected Ready message"),
NoitaInbound::ProxyToDes(proxy_to_des) => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Some(entity_sync) = &mut state.modules.entity_sync {
entity_sync.handle_proxytodes(proxy_to_des);
match state.modules.entity_sync.handle_proxytodes(proxy_to_des) {
Err(e) => {
let _ = print_error(e);
}
Ok(Some(peer)) => {
state.fps_by_player.remove(&peer);
state.player_entity_map.remove_by_left(&peer);
}
Ok(None) => {}
}
})?,
NoitaInbound::RemoteMessage {
@ -234,27 +172,27 @@ fn netmanager_recv(_lua: LuaState) -> eyre::Result<Option<RawString>> {
message: shared::RemoteMessage::RemoteDes(remote_des),
} => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Some(entity_sync) = &mut state.modules.entity_sync {
match entity_sync.handle_remotedes(
source,
remote_des,
netmanager,
&state.player_entity_map,
&state.dont_spawn,
) {
Ok((Some(gid), _)) => {
state.dont_spawn.insert(gid);
}
Ok((_, Some(pos))) => {
state.cam_pos.insert(source, pos);
}
Ok((_, _)) => {}
Err(s) => {
let _ = print_error(s);
}
match state.modules.entity_sync.handle_remotedes(
source,
remote_des,
netmanager,
&state.player_entity_map,
&mut state.dont_spawn,
&mut state.cam_pos,
state.globals.entity_manager_mut(),
) {
Ok(()) => {}
Err(s) => {
let _ = print_error(s);
}
}
})?,
NoitaInbound::ProxyToWorldSync(msg) => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Err(e) = state.modules.world.handle_remote(msg) {
let _ = print_error(e);
}
})?,
}
}
Ok(None)
@ -275,28 +213,9 @@ fn netmanager_flush(_lua: LuaState) -> eyre::Result<()> {
netmanager.flush()
}
impl LuaFnRet for InitKV {
fn do_return(self, lua: LuaState) -> c_int {
lua.create_table(2, 0);
lua.push_string(&self.key);
lua.rawset_table(-2, 1);
lua.push_string(&self.value);
lua.rawset_table(-2, 2);
1
}
}
fn on_world_initialized(lua: LuaState) {
println!(
"ewext on_world_initialized in thread {:?}",
thread::current().id()
);
grab_addrs(lua);
STATE.with(|state| {
let modules = &mut state.borrow_mut().modules;
modules.entity_sync = Some(EntitySync::default());
})
fn set_world_num(lua: LuaState) -> eyre::Result<()> {
let world_num = lua.to_integer(1);
ExtState::with_global(|state| state.modules.world.world_num = world_num as u8)
}
static IN_MODULE_LOCK: Mutex<()> = Mutex::new(());
@ -314,10 +233,11 @@ fn with_every_module(
fps_by_player: &mut state.fps_by_player,
dont_spawn: &state.dont_spawn,
camera_pos: &mut state.cam_pos,
globals: state.globals.as_mut(),
};
let mut errs = Vec::new();
for module in state.modules.entity_sync.iter_mut() {
if let Err(e) = f(&mut ctx, module as &mut dyn Module) {
for module in state.modules.iter_mut() {
if let Err(e) = f(&mut ctx, module) {
errs.push(e);
}
}
@ -331,18 +251,24 @@ fn with_every_module(
})?
}
fn module_on_world_init(_lua: LuaState) -> eyre::Result<()> {
fn module_on_world_init(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| state.globals = Globals::new(lua))?;
with_every_module(|ctx, module| module.on_world_init(ctx))
}
fn module_on_world_update(_lua: LuaState) -> eyre::Result<()> {
//let _tracker = TimeTracker::new("on_world_update");
with_every_module(|ctx, module| module.on_world_update(ctx))
}
fn module_on_new_entity(lua: LuaState) -> eyre::Result<()> {
let entity = EntityID::try_from(lua.to_string(1)?.parse::<isize>()?)?;
with_every_module(|_, module| module.on_new_entity(entity, true))
let len = lua.to_integer(2);
with_every_module(|_, module| {
let arr = lua.to_integer_array(1, len as usize);
for ent in arr {
module.on_new_entity(ent, true)?
}
Ok(())
})
}
fn module_on_projectile_fired(lua: LuaState) -> eyre::Result<()> {
@ -367,68 +293,59 @@ fn module_on_projectile_fired(lua: LuaState) -> eyre::Result<()> {
})
}
fn bench_fn(_lua: LuaState) -> eyre::Result<()> {
/*fn bench_fn(_lua: LuaState) -> eyre::Result<()> {
let start = Instant::now();
let iters = 10000;
for _ in 0..iters {
let player = noita_api::raw::entity_get_closest_with_tag(0.0, 0.0, "player_unit".into())?
.ok_or_eyre("Entity not found")?;
noita_api::raw::entity_set_transform(player, 0.0, Some(0.0), None, None, None)?;
let player = EntityID::get_closest_with_tag(0.0, 0.0, "player_unit")?;
player.set_position(0.0, 0.0, None)?;
}
let elapsed = start.elapsed();
noita_api::raw::game_print(
format!(
"Took {}us to test, {}ns per call",
elapsed.as_micros(),
elapsed.as_nanos() / iters
)
.into(),
)?;
noita_api::game_print(format!(
"Took {}us to test, {}ns per call",
elapsed.as_micros(),
elapsed.as_nanos() / iters
));
Ok(())
}
}*/
fn test_fn(_lua: LuaState) -> eyre::Result<()> {
let player = noita_api::raw::entity_get_closest_with_tag(0.0, 0.0, "player_unit".into())?
.ok_or_eyre("Entity not found")?;
/*fn test_fn(_lua: LuaState) -> eyre::Result<()> {
let player = EntityID::get_closest_with_tag(0.0, 0.0, "player_unit")?;
let damage_model: DamageModelComponent = player.get_first_component(None)?;
let hp = damage_model.hp()?;
damage_model.set_hp(hp - 1.0)?;
let (x, y, _, _, _) = noita_api::raw::entity_get_transform(player)?;
let (x, y) = player.position()?;
noita_api::raw::game_print(
format!("Component: {:?}, Hp: {}", damage_model.0, hp * 25.0,).into(),
)?;
noita_api::game_print(format!(
"Component: {:?}, Hp: {}",
damage_model.0,
hp * 25.0,
));
let entities = noita_api::raw::entity_get_in_radius_with_tag(x, y, 300.0, "enemy".into())?;
noita_api::raw::game_print(format!("{:?}", entities).into())?;
let entities = EntityID::get_in_radius_with_tag(x, y, 300.0, "enemy")?;
noita_api::game_print(format!("{entities:?}"));
// noita::api::raw::entity_set_transform(player, 0.0, 0.0, 0.0, 1.0, 1.0)?;
Ok(())
}
}*/
fn probe(_lua: LuaState) {
/*fn probe(_lua: LuaState) {
backtrace::trace(|frame| {
let ip = frame.ip() as usize;
println!("Probe: 0x{ip:x}");
let _ip = frame.ip() as usize;
#[cfg(debug_assertions)]
println!("Probe: 0x{_ip:x}");
false
});
}
fn __gc(_lua: LuaState) {
println!("ewext collected in thread {:?}", thread::current().id());
NETMANAGER.lock().unwrap().take();
// TODO this doesn't actually work because it's a thread local
STATE.with(|state| state.take());
}
}*/
pub(crate) fn print_error(error: eyre::Report) -> eyre::Result<()> {
let lua = LuaState::current()?;
lua.get_global(c"EwextPrintError");
lua.push_string(&format!("{:?}\n{}", error, Backtrace::force_capture()));
lua.push_string(&format!("{error:?}\n{}", Backtrace::force_capture()));
lua.call(1, 0i32)
.wrap_err("Failed to call EwextPrintError")?;
Ok(())
@ -438,17 +355,21 @@ pub(crate) fn print_error(error: eyre::Report) -> eyre::Result<()> {
///
/// Only gets called by lua when loading a module.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
#[cfg(debug_assertions)]
println!("Initializing ewext");
raw_new(1);
if let Err(e) = KEEP_SELF_LOADED.as_ref() {
println!("Got an error while loading self: {}", e);
if let Err(_e) = KEEP_SELF_LOADED.as_ref() {
#[cfg(debug_assertions)]
println!("Got an error while loading self: {_e}");
}
#[cfg(debug_assertions)]
println!(
"lua_call: 0x{:x}",
(*LUA.lua_call.as_ref().unwrap()) as usize
);
#[cfg(debug_assertions)]
println!(
"lua_pcall: 0x{:x}",
(*LUA.lua_pcall.as_ref().unwrap()) as usize
@ -463,17 +384,24 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
// Detect module unload. Adapted from NoitaPatcher.
LUA.lua_newuserdata(lua, 0);
LUA.lua_createtable(lua, 0, 0);
fn __gc(_lua: LuaState) {
#[cfg(debug_assertions)]
println!(
"ewext collected in thread {:?}",
std::thread::current().id()
);
NETMANAGER.lock().unwrap().take();
STATE.take();
}
add_lua_fn!(__gc);
LUA.lua_setmetatable(lua, -2);
LUA.lua_setfield(lua, LUA_REGISTRYINDEX, c"luaclose_ewext".as_ptr());
add_lua_fn!(init_particle_world_state);
add_lua_fn!(encode_area);
add_lua_fn!(make_ephemerial);
add_lua_fn!(on_world_initialized);
add_lua_fn!(test_fn);
add_lua_fn!(bench_fn);
add_lua_fn!(probe);
add_lua_fn!(set_world_num);
//add_lua_fn!(test_fn);
//add_lua_fn!(bench_fn);
//add_lua_fn!(probe);
add_lua_fn!(netmanager_connect);
add_lua_fn!(netmanager_recv);
@ -487,10 +415,10 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn sync_projectile(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity = lua.to_string(1)?.parse::<isize>()?;
let entity = lua.to_integer(1);
let peer = PeerId::from_hex(&lua.to_string(2)?)?;
let mut rng: u64 =
u32::from_le_bytes(lua.to_string(3)?.parse::<i32>()?.to_le_bytes()) as u64;
u32::from_le_bytes((lua.to_integer(3) as i32).to_le_bytes()) as u64;
if rng == 0 {
rng = 1;
}
@ -499,12 +427,7 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
peer_n = peer_n.overflowing_add(rng).0
}
let gid = peer_n.overflowing_mul(rng).0;
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
entity_sync.sync_projectile(
state.modules.entity_sync.sync_projectile(
EntityID(NonZero::try_from(entity)?),
Gid(gid),
peer,
@ -516,12 +439,10 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn des_item_thrown(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
entity_sync.cross_item_thrown(LuaGetValue::get(lua, -1)?)?;
.cross_item_thrown(LuaGetValue::get(lua, -1)?)?;
Ok(())
})?
}
@ -529,11 +450,6 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn des_death_notify(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed = EntityID::try_from(lua.to_integer(1))
.wrap_err("Expected to have a valid entity_killed")?;
let wait_on_kill = lua.to_bool(2);
@ -544,11 +460,11 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
.wrap_err("Expected to have a valid filepath")?;
let entity_responsible = EntityID::try_from(lua.to_integer(6)).ok();
let pos = WorldPos::from_f64(x, y);
entity_sync.cross_death_notify(
state.modules.entity_sync.cross_death_notify(
entity_killed,
wait_on_kill,
pos,
file.to_string(),
file,
entity_responsible,
)?;
Ok(())
@ -558,15 +474,10 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn notrack(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed: Option<EntityID> = LuaGetValue::get(lua, -1)?;
let entity_killed =
entity_killed.ok_or_eyre("Expected to have a valid entity_killed")?;
entity_sync.notrack_entity(entity_killed);
state.modules.entity_sync.notrack_entity(entity_killed);
Ok(())
})?
}
@ -574,15 +485,10 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn track(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed: Option<EntityID> = LuaGetValue::get(lua, -1)?;
let entity_killed =
entity_killed.ok_or_eyre("Expected to have a valid entity_killed")?;
entity_sync.track_entity(entity_killed);
state.modules.entity_sync.track_entity(entity_killed);
Ok(())
})?
}
@ -592,10 +498,7 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
let (peer_id, entity): (Cow<'_, str>, Option<EntityID>) = LuaGetValue::get(lua, -1)?;
let peer_id = PeerId::from_hex(&peer_id)?;
let entity = entity.ok_or_eyre("Expected a valid entity")?;
if entity
.iter_all_components_of_type_including_disabled::<VariableStorageComponent>(None)?
.all(|var| var.name().unwrap_or("".into()) != "ew_peer_id")
{
if entity.get_var("ew_peer_id").is_none() {
let var = entity.add_component::<VariableStorageComponent>()?;
var.set_name("ew_peer_id".into())?;
var.set_value_string(peer_id.0.to_string().into())?;
@ -609,7 +512,7 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn set_player_fps(lua: LuaState) -> eyre::Result<()> {
let peer = PeerId::from_hex(&lua.to_string(1)?)?;
let fps = lua.to_string(2)?.parse::<u8>()?;
let fps = lua.to_integer(2) as u8;
ExtState::with_global(|state| {
state.fps_by_player.insert(peer, fps);
Ok(())
@ -620,30 +523,20 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn find_by_gid(lua: LuaState) -> eyre::Result<Option<EntityID>> {
ExtState::with_global(|state| {
let gid = lua.to_string(1)?.parse::<u64>()?;
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
Ok(entity_sync.find_by_gid(Gid(gid)))
Ok(state.modules.entity_sync.find_by_gid(Gid(gid)))
})?
}
add_lua_fn!(find_by_gid);
fn des_chest_opened(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let x = lua.to_string(1)?.parse::<f64>()?;
let y = lua.to_string(2)?.parse::<f64>()?;
let rx = lua.to_string(3)?.parse::<f32>()?;
let ry = lua.to_string(4)?.parse::<f32>()?;
let file = lua.to_string(5)?.to_string();
let x = lua.to_number(1);
let y = lua.to_number(2);
let rx = lua.to_number(3) as f32;
let ry = lua.to_number(4) as f32;
let file = lua.to_string(5)?;
let gid = Gid(lua.to_string(6)?.parse::<u64>()?);
let is_mine = lua.to_string(7)?.parse::<u8>()? == 1;
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let is_mine = lua.to_bool(7);
let mut temp = try_lock_netmanager()?;
let net = temp.as_mut().ok_or_eyre("Netmanager not available")?;
if is_mine {
@ -659,7 +552,11 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
ry,
)),
})?;
for (has_interest, peer) in entity_sync.iter_peers(&state.player_entity_map) {
for (has_interest, peer) in state
.modules
.entity_sync
.iter_peers(&state.player_entity_map)
{
if has_interest {
net.send(&NoitaOutbound::RemoteMessage {
reliable: true,
@ -684,7 +581,7 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
})?;
}
}
} else if let Some(peer) = entity_sync.find_peer_by_gid(gid) {
} else if let Some(peer) = state.modules.entity_sync.find_peer_by_gid(gid) {
net.send(&NoitaOutbound::RemoteMessage {
reliable: true,
destination: Destination::Peer(*peer),
@ -700,8 +597,8 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
fn des_broken_wand(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let x = lua.to_string(1)?.parse::<f64>()?;
let y = lua.to_string(2)?.parse::<f64>()?;
let x = lua.to_number(1);
let y = lua.to_number(2);
let mut temp = try_lock_netmanager()?;
let net = temp.as_mut().ok_or_eyre("Netmanager not available")?;
for peer in state.player_entity_map.left_values() {
@ -720,7 +617,23 @@ pub unsafe extern "C" fn luaopen_ewext1(lua: *mut lua_State) -> c_int {
})?
}
add_lua_fn!(des_broken_wand);
fn set_log(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
state.modules.entity_sync.set_perf(lua.to_bool(1));
Ok(())
})?
}
add_lua_fn!(set_log);
fn set_cache(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
state.modules.entity_sync.set_cache(lua.to_bool(1));
Ok(())
})?
}
add_lua_fn!(set_cache);
}
#[cfg(debug_assertions)]
println!("Initializing ewext - Ok");
1
}

View file

@ -1,6 +1,7 @@
use bimap::BiHashMap;
use eyre::Ok;
use noita_api::EntityID;
use noita_api::addr_grabber::GlobalsMut;
use rustc_hash::{FxHashMap, FxHashSet};
use shared::des::Gid;
use shared::{PeerId, WorldPos};
@ -9,12 +10,15 @@ use crate::net::NetManager;
pub(crate) mod entity_sync;
pub(crate) mod world_sync;
pub(crate) struct ModuleCtx<'a> {
pub(crate) net: &'a mut NetManager,
pub(crate) player_map: &'a mut BiHashMap<PeerId, EntityID>,
pub(crate) camera_pos: &'a mut FxHashMap<PeerId, WorldPos>,
pub(crate) fps_by_player: &'a mut FxHashMap<PeerId, u8>,
pub(crate) dont_spawn: &'a FxHashSet<Gid>,
pub(crate) globals: GlobalsMut,
}
impl ModuleCtx<'_> {
pub(crate) fn locate_player_within_except_me(
@ -63,7 +67,7 @@ pub(crate) trait Module {
Ok(())
}
fn on_new_entity(&mut self, _entity: EntityID, _kill: bool) -> eyre::Result<()> {
fn on_new_entity(&mut self, _entity: isize, _kill: bool) -> eyre::Result<()> {
Ok(())
}

View file

@ -3,17 +3,17 @@
//! Also, each entity gets an owner.
//! Each peer broadcasts an "Interest" zone. If it intersects any peer they receive all information about entities this peer owns.
use super::{Module, NetManager};
use super::{Module, ModuleCtx, NetManager};
use crate::my_peer_id;
use bimap::BiHashMap;
use diff_model::{DES_TAG, LocalDiffModel, RemoteDiffModel, entity_is_item};
use eyre::{Context, OptionExt};
use interest::InterestTracker;
use noita_api::raw::game_get_frame_num;
use noita_api::serialize::serialize_entity;
use noita_api::{
DamageModelComponent, EntityID, ItemCostComponent, LuaComponent, PositionSeedComponent,
ProjectileComponent, VariableStorageComponent, VelocityComponent,
CachedTag, ComponentTag, DamageModelComponent, DamageType, EntityID, EntityManager,
ItemCostComponent, LuaComponent, PositionSeedComponent, ProjectileComponent, VarName,
VelocityComponent,
};
use rustc_hash::{FxHashMap, FxHashSet};
use shared::des::DesToProxy::UpdatePositions;
@ -25,16 +25,16 @@ use std::sync::{LazyLock, Mutex};
mod diff_model;
mod interest;
static ENTITY_EXCLUDES: LazyLock<FxHashSet<String>> = LazyLock::new(|| {
static ENTITY_EXCLUDES: LazyLock<FxHashSet<&'static str>> = LazyLock::new(|| {
let mut hs = FxHashSet::default();
hs.insert("data/entities/items/pickup/perk.xml".to_string());
hs.insert("data/entities/items/pickup/spell_refresh.xml".to_string());
hs.insert("data/entities/items/pickup/heart.xml".to_string());
hs.insert("data/entities/items/pickup/heart_better.xml".to_string());
hs.insert("data/entities/items/pickup/heart_evil.xml".to_string());
hs.insert("data/entities/items/pickup/heart_fullhp.xml".to_string());
hs.insert("data/entities/items/pickup/heart_fullhp_temple.xml".to_string());
hs.insert("data/entities/items/pickup/perk_reroll.xml".to_string());
hs.insert("data/entities/items/pickup/perk.xml");
hs.insert("data/entities/items/pickup/spell_refresh.xml");
hs.insert("data/entities/items/pickup/heart.xml");
hs.insert("data/entities/items/pickup/heart_better.xml");
hs.insert("data/entities/items/pickup/heart_evil.xml");
hs.insert("data/entities/items/pickup/heart_fullhp.xml");
hs.insert("data/entities/items/pickup/heart_fullhp_temple.xml");
hs.insert("data/entities/items/pickup/perk_reroll.xml");
hs
});
@ -56,13 +56,23 @@ pub(crate) struct EntitySync {
local_index: usize,
remote_index: FxHashMap<PeerId, usize>,
peer_order: Vec<PeerId>,
log_performance: bool,
entity_manager: EntityManager,
}
impl EntitySync {
pub(crate) fn set_perf(&mut self, perf: bool) {
self.log_performance = perf;
}
pub(crate) fn set_cache(&mut self, cache: bool) {
self.entity_manager.set_cache(cache);
}
/*pub(crate) fn has_gid(&self, gid: Gid) -> bool {
self.local_diff_model.has_gid(gid) || self.remote_models.values().any(|r| r.has_gid(gid))
}*/
pub(crate) fn track_entity(&mut self, ent: EntityID) {
let _ = self.local_diff_model.track_and_upload_entity(ent);
let _ = self
.local_diff_model
.track_and_upload_entity(ent, &mut self.entity_manager);
}
pub(crate) fn notrack_entity(&mut self, ent: EntityID) {
self.dont_track.insert(ent);
@ -101,6 +111,8 @@ impl Default for EntitySync {
local_index: 0,
remote_index: Default::default(),
peer_order: Vec::new(),
log_performance: false,
entity_manager: EntityManager::default(),
}
}
}
@ -108,21 +120,69 @@ impl Default for EntitySync {
fn entity_is_excluded(entity: EntityID) -> eyre::Result<bool> {
let good = "data/entities/items/wands/wand_good/wand_good_";
let filename = entity.filename()?;
Ok(entity.has_tag("ew_no_enemy_sync")
|| entity.has_tag("polymorphed_player")
|| entity.has_tag("gold_nugget")
|| entity.has_tag("nightmare_starting_wand")
|| ENTITY_EXCLUDES.contains(&filename)
let tags = format!(",{},", entity.tags()?);
Ok(tags.contains(",ew_no_enemy_sync,")
|| tags.contains(",polymorphed_player,")
|| tags.contains(",gold_nugget,")
|| tags.contains(",nightmare_starting_wand,")
|| ENTITY_EXCLUDES.contains(filename.as_ref())
|| filename.starts_with(good)
|| entity.has_tag("player_unit")
|| tags.contains(",player_unit,")
|| filename == "data/entities/items/pickup/greed_curse.xml"
|| (entity.root()? != Some(entity) && !entity.has_tag("ew_sync_child")))
|| (!tags.contains(",ew_sync_child,") && entity.root()? != Some(entity)))
}
impl EntitySync {
pub(crate) fn spawn_once(&mut self, ctx: &mut super::ModuleCtx) -> eyre::Result<()> {
let (x, y) = noita_api::raw::game_get_camera_pos()?;
let frame_num = game_get_frame_num()? as usize;
fn clear_buffer(&mut self, ctx: &mut ModuleCtx, new_intersects: &[PeerId]) -> eyre::Result<()> {
let err1 = if !self.local_diff_model.init_buffer.is_empty() {
let res = std::mem::take(&mut self.local_diff_model.init_buffer);
let (RemoteDes::EntityInit(diff), err) = send_remotedes_ret(
ctx,
true,
Destination::Peers(
self.interest_tracker
.iter_interested()
.filter(|p| !new_intersects.contains(p))
.collect(),
),
RemoteDes::EntityInit(res),
) else {
unreachable!()
};
self.local_diff_model.init_buffer = diff;
self.local_diff_model.uninit();
err
} else {
Ok(())
};
if !self.local_diff_model.update_buffer.is_empty() {
let res = std::mem::take(&mut self.local_diff_model.update_buffer);
let (RemoteDes::EntityUpdate(diff), err) = send_remotedes_ret(
ctx,
true,
Destination::Peers(
self.interest_tracker
.iter_interested()
.filter(|p| !new_intersects.contains(p))
.collect(),
),
RemoteDes::EntityUpdate(res),
) else {
unreachable!()
};
self.local_diff_model.update_buffer = diff;
err1?;
err?;
}
Ok(())
}
pub(crate) fn spawn_once(
&mut self,
ctx: &mut ModuleCtx,
frame_num: usize,
x: f64,
y: f64,
) -> eyre::Result<()> {
let len = self.spawn_once.len();
if len > 0 {
let batch_size = (len / 20).max(1);
@ -137,15 +197,15 @@ impl EntitySync {
let (x, y) = (pos.x as f64, pos.y as f64);
match data {
shared::SpawnOnce::Enemy(file, drops_gold, offending_peer) => {
if let Ok(Some(entity)) =
noita_api::raw::entity_load(file.into(), Some(x), Some(y))
{
if let Ok(entity) = EntityID::load(file, Some(x), Some(y)) {
entity.add_tag("ew_no_enemy_sync")?;
diff_model::init_remote_entity(
entity,
None,
None,
*drops_gold,
&mut self.entity_manager,
ctx.globals.entity_manager,
)?;
if let Some(damage) = entity
.try_get_first_component::<DamageModelComponent>(None)?
@ -159,7 +219,6 @@ impl EntitySync {
}
entity
.children(Some("protection".into()))
.iter()
.for_each(|ent| ent.kill());
damage.set_ui_report_damage(false)?;
if entity.has_tag("boss_centipede") {
@ -181,27 +240,18 @@ impl EntitySync {
"curse",
1.0,
)?;
noita_api::raw::entity_inflict_damage(
entity.raw() as i32,
entity.inflict_damage(
damage.max_hp()? * 100.0,
"DAMAGE_CURSE".into(), //TODO should be enum
"kill sync".into(),
"NONE".into(),
0.0,
0.0,
responsible_entity.map(|e| e.raw() as i32),
None,
None,
None,
DamageType::DamageCurse,
"kill sync",
responsible_entity,
)?;
}
}
}
}
shared::SpawnOnce::Chest(file, rx, ry) => {
if let Ok(Some(ent)) =
noita_api::raw::entity_load(file.into(), Some(x), Some(y))
{
if let Ok(ent) = EntityID::load(file, Some(x), Some(y)) {
ent.add_tag("ew_no_enemy_sync")?;
if let Some(file) = ent
.iter_all_components_of_type_including_disabled::<LuaComponent>(
@ -223,13 +273,12 @@ impl EntitySync {
}
}
shared::SpawnOnce::BrokenWand => {
if let Some(ent) = noita_api::raw::entity_create_new(None)? {
ent.set_position(x as f32, y as f32, None)?;
ent.add_tag("broken_wand")?;
ent.add_lua_init_component::<LuaComponent>(
"data/scripts/buildings/forge_item_convert.lua",
)?;
}
let ent = EntityID::create(None)?;
ent.set_position(x, y, None)?;
ent.add_tag("broken_wand")?;
ent.add_lua_init_component::<LuaComponent>(
"data/scripts/buildings/forge_item_convert.lua",
)?;
}
}
self.spawn_once.remove(i);
@ -240,39 +289,43 @@ impl EntitySync {
}
Ok(())
}
pub fn iter_peers(&self, player_map: &BiHashMap<PeerId, EntityID>) -> Vec<(bool, PeerId)> {
player_map
.left_values()
.filter_map(|p| {
if *p != my_peer_id() {
Some((self.interest_tracker.contains(*p), *p))
} else {
None
}
})
.collect::<Vec<(bool, PeerId)>>()
pub fn iter_peers<'a>(
&'a self,
player_map: &'a BiHashMap<PeerId, EntityID>,
) -> impl Iterator<Item = (bool, PeerId)> + 'a {
player_map.left_values().filter_map(move |p| {
if *p != my_peer_id() {
Some((self.interest_tracker.contains(*p), *p))
} else {
None
}
})
}
fn should_be_tracked(&mut self, entity: EntityID) -> eyre::Result<bool> {
let tags = format!(",{},", entity.tags()?);
let should_be_tracked = [
"enemy",
"ew_synced",
"plague_rat",
"seed_f",
"seed_e",
"seed_d",
"seed_c",
"perk_fungus_tiny",
"helpless_animal",
"nest",
",enemy,",
",ew_synced,",
",plague_rat,",
",seed_f,",
",seed_e,",
",seed_d,",
",seed_c,",
",perk_fungus_tiny,",
",helpless_animal,",
",nest,",
]
.iter()
.any(|tag| entity.has_tag(tag))
.any(|tag| tags.contains(tag))
|| entity_is_item(entity)?;
Ok(should_be_tracked && !entity_is_excluded(entity)?)
}
pub(crate) fn handle_proxytodes(&mut self, proxy_to_des: shared::des::ProxyToDes) {
pub(crate) fn handle_proxytodes(
&mut self,
proxy_to_des: shared::des::ProxyToDes,
) -> eyre::Result<Option<PeerId>> {
match proxy_to_des {
shared::des::ProxyToDes::GotAuthority(full_entity_data) => {
self.local_diff_model.got_authority(full_entity_data);
@ -282,37 +335,36 @@ impl EntitySync {
}
shared::des::ProxyToDes::RemoveEntities(peer) => {
if let Some(remote) = self.remote_models.remove(&peer) {
remote.remove_entities()
remote.remove_entities(&mut self.entity_manager)?
}
self.interest_tracker.remove_peer(peer);
let _ = crate::ExtState::with_global(|state| {
state.fps_by_player.remove(&peer);
state.player_entity_map.remove_by_left(&peer);
});
return Ok(Some(peer));
}
shared::des::ProxyToDes::DeleteEntity(entity) => {
EntityID(entity).kill();
}
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_remotedes(
&mut self,
source: PeerId,
remote_des: RemoteDes,
net: &mut NetManager,
player_entity_map: &BiHashMap<PeerId, EntityID>,
dont_spawn: &FxHashSet<Gid>,
) -> eyre::Result<(Option<Gid>, Option<WorldPos>)> {
dont_spawn: &mut FxHashSet<Gid>,
cam_pos: &mut FxHashMap<PeerId, WorldPos>,
em: &mut noita_api::noita::types::EntityManager,
) -> eyre::Result<()> {
match remote_des {
RemoteDes::ChestOpen(gid, x, y, file, rx, ry) => {
if !dont_spawn.contains(&gid) {
if let Some(ent) = self.find_by_gid(gid) {
ent.kill()
}
if let Ok(Some(ent)) =
noita_api::raw::entity_load(file.into(), Some(x as f64), Some(y as f64))
{
if let Ok(ent) = EntityID::load(file, Some(x as f64), Some(y as f64)) {
ent.add_tag("ew_no_enemy_sync")?;
if let Some(file) = ent
.iter_all_components_of_type_including_disabled::<LuaComponent>(None)?
@ -331,7 +383,7 @@ impl EntitySync {
}
}
}
return Ok((Some(gid), None));
dont_spawn.insert(gid);
}
RemoteDes::ChestOpenRequest(gid, x, y, file, rx, ry) => {
net.send(&NoitaOutbound::RemoteMessage {
@ -379,22 +431,30 @@ impl EntitySync {
.or_insert(RemoteDiffModel::new(source))
.check_entities(lids),*/
RemoteDes::CameraPos(pos) => {
return Ok((None, Some(pos)));
cam_pos.insert(source, pos);
}
RemoteDes::DeadEntities(vec) => self.spawn_once.extend(vec),
RemoteDes::InterestRequest(interest_request) => self
.interest_tracker
.handle_interest_request(source, interest_request),
RemoteDes::EntityUpdate(vec) => {
self.remote_models
.entry(source)
.or_insert(RemoteDiffModel::new(source))
.apply_diff(vec, &mut self.entity_manager)?;
}
RemoteDes::EntityInit(vec) => {
self.dont_kill.extend(
self.remote_models
.entry(source)
.or_insert(RemoteDiffModel::new(source))
.apply_diff(&vec),
.apply_init(vec, &mut self.entity_manager, em)?,
);
}
RemoteDes::ExitedInterest => {
self.remote_models.remove(&source);
if let Some(remote) = self.remote_models.remove(&source) {
remote.remove_entities(&mut self.entity_manager)?
}
}
RemoteDes::Reset => self.interest_tracker.reset_interest_for(source),
RemoteDes::Projectiles(vec) => {
@ -404,17 +464,19 @@ impl EntitySync {
.spawn_projectiles(&vec);
}
RemoteDes::RequestGrab(lid) => {
self.local_diff_model.entity_grabbed(source, lid, net);
self.local_diff_model
.entity_grabbed(source, lid, net, &mut self.entity_manager)?;
}
}
Ok((None, None))
Ok(())
}
pub(crate) fn cross_item_thrown(&mut self, entity: Option<EntityID>) -> eyre::Result<()> {
let entity = entity.ok_or_eyre("Passed entity 0 into cross call")?;
// It might be already tracked in case of tablet telekinesis, no need to track it again.
if !self.local_diff_model.is_entity_tracked(entity) {
self.local_diff_model.track_and_upload_entity(entity)?;
self.local_diff_model
.track_and_upload_entity(entity, &mut self.entity_manager)?;
}
Ok(())
}
@ -445,7 +507,9 @@ impl EntitySync {
) -> eyre::Result<()> {
if peer == my_peer_id() {
self.dont_kill.insert(entity);
let lid = self.local_diff_model.track_entity(entity, gid)?;
let lid = self
.local_diff_model
.track_entity(entity, gid, &mut self.entity_manager)?;
self.local_diff_model.dont_save(lid);
} else if let Some(remote) = self.remote_models.get_mut(&peer) {
remote.wait_for_gid(entity, gid);
@ -455,13 +519,14 @@ impl EntitySync {
}
impl Module for EntitySync {
fn on_world_init(&mut self, ctx: &mut super::ModuleCtx) -> eyre::Result<()> {
send_remotedes(ctx, true, Destination::Broadcast, RemoteDes::Reset)?;
fn on_world_init(&mut self, ctx: &mut ModuleCtx) -> eyre::Result<()> {
send_remotedes(ctx.net, true, Destination::Broadcast, RemoteDes::Reset)?;
Ok(())
}
/// Looks for newly spawned entities that might need to be tracked.
fn on_new_entity(&mut self, entity: EntityID, kill: bool) -> eyre::Result<()> {
fn on_new_entity(&mut self, ent: isize, kill: bool) -> eyre::Result<()> {
let entity = EntityID::try_from(ent)?;
if !kill && !entity.is_alive() {
return Ok(());
}
@ -475,11 +540,56 @@ impl Module for EntitySync {
self.dont_kill_by_gid.insert(gid);
self.local_diff_model.got_polied(gid);
}
if entity.has_tag(DES_TAG)
if self.should_be_tracked(entity)? {
self.entity_manager.set_current_entity(entity)?;
if self
.entity_manager
.has_tag(const { CachedTag::from_tag(DES_TAG) })
&& !self.dont_kill.remove(&entity)
&& self
.entity_manager
.get_var(const { VarName::from_str("ew_gid_lid") })
.map(|var| {
if let Ok(n) = var.value_string().unwrap_or("NA".into()).parse::<u64>() {
!self.dont_kill_by_gid.remove(&Gid(n))
} else {
true
}
})
.unwrap_or(true)
{
if kill {
entity.kill();
}
} else {
if self
.entity_manager
.has_tag(const { CachedTag::from_tag("card_action") })
{
if let Some(cost) = self
.entity_manager
.try_get_first_component::<ItemCostComponent>(ComponentTag::None)
&& cost.stealable()?
{
cost.set_stealable(false)?;
self.entity_manager
.get_var_or_default(const { VarName::from_str("ew_was_stealable") })?;
}
if let Some(vel) = self
.entity_manager
.try_get_first_component::<VelocityComponent>(ComponentTag::None)
{
vel.set_gravity_y(0.0)?;
vel.set_air_friction(10.0)?;
}
}
self.to_track.push(entity);
}
} else if kill
&& !self.dont_kill.remove(&entity)
&& entity.has_tag(DES_TAG)
&& entity
.iter_all_components_of_type_including_disabled::<VariableStorageComponent>(None)?
.find(|var| var.name().unwrap_or("".into()) == "ew_gid_lid")
.get_var("ew_gid_lid")
.map(|var| {
if let Ok(n) = var.value_string().unwrap_or("NA".into()).parse::<u64>() {
!self.dont_kill_by_gid.remove(&Gid(n))
@ -489,54 +599,42 @@ impl Module for EntitySync {
})
.unwrap_or(true)
{
if kill {
entity.kill();
}
return Ok(());
}
if self.should_be_tracked(entity)? {
if entity.has_tag("card_action") {
if let Some(cost) = entity.try_get_first_component::<ItemCostComponent>(None)? {
if cost.stealable()? {
cost.set_stealable(false)?;
entity.get_var_or_default("ew_was_stealable")?;
}
}
if let Some(vel) = entity.try_get_first_component::<VelocityComponent>(None)? {
vel.set_gravity_y(0.0)?;
vel.set_air_friction(10.0)?;
}
}
self.to_track.push(entity);
entity.kill();
}
Ok(())
}
fn on_world_update(&mut self, ctx: &mut super::ModuleCtx) -> eyre::Result<()> {
let (x, y) = noita_api::raw::game_get_camera_pos()?;
fn on_world_update(&mut self, ctx: &mut ModuleCtx) -> eyre::Result<()> {
let start = std::time::Instant::now();
self.entity_manager.init_pos()?;
self.entity_manager.init_frame_num()?;
let (x, y) = self.entity_manager.camera_pos();
let pos = WorldPos::from_f64(x, y);
self.interest_tracker.set_center(x, y);
let frame_num = game_get_frame_num()? as usize;
let frame_num = self.entity_manager.frame_num();
if frame_num < 10 {
return Ok(());
}
if frame_num % 5 == 0 {
send_remotedes(
ctx,
ctx.net,
false,
Destination::Broadcast,
RemoteDes::InterestRequest(InterestRequest { pos }),
)?;
}
for (_, peer) in self.iter_peers(ctx.player_map) {
if frame_num % 5 == 1 {
send_remotedes(
ctx,
ctx.net,
false,
Destination::Peer(peer),
Destination::Peers(self.iter_peers(ctx.player_map).map(|(_, p)| p).collect()),
RemoteDes::CameraPos(pos),
)?;
}
for lost in self.interest_tracker.drain_lost_interest() {
send_remotedes(
ctx,
ctx.net,
true,
Destination::Peer(lost),
RemoteDes::ExitedInterest,
@ -544,78 +642,93 @@ impl Module for EntitySync {
}
self.look_current_entity = EntityID::max_in_use()?;
self.local_diff_model.enable_later()?;
self.local_diff_model.phys_later()?;
let t = self.local_diff_model.update_pending_authority()?;
self.local_diff_model
.enable_later(&mut self.entity_manager)?;
self.local_diff_model.phys_later(&mut self.entity_manager)?;
let mut times = vec![0; 4];
if self.log_performance {
times[0] = start.elapsed().as_micros();
}
self.local_diff_model
.update_pending_authority(start, &mut self.entity_manager)?;
if self.log_performance {
times[1] = start.elapsed().as_micros() - times[0];
}
for ent in self.look_current_entity.0.get() + 1..=EntityID::max_in_use()?.0.get() {
if let Ok(ent) = EntityID::try_from(ent) {
self.on_new_entity(ent, false)?;
}
self.on_new_entity(ent, false)?;
}
let start = std::time::Instant::now();
while let Some(entity) = self.to_track.pop() {
self.local_diff_model.track_and_upload_entity(entity)?;
if start.elapsed().as_micros() + t > 2000 {
break;
if entity.is_alive() {
self.local_diff_model
.track_and_upload_entity(entity, &mut self.entity_manager)?;
if start.elapsed().as_micros() > 2000 {
break;
}
} else {
self.entity_manager.remove_ent(&entity);
}
}
let mut t = start.elapsed().as_micros() + t;
if self.log_performance {
times[2] = start.elapsed().as_micros() - (times[0] + times[1]);
}
{
let (diff, dead);
(diff, dead, t, self.local_index) = self
.local_diff_model
.update_tracked_entities(ctx, self.local_index, t)
.wrap_err("Failed to update locally tracked entities")?;
let new_intersects = self.interest_tracker.got_any_new_interested();
let dead;
(dead, self.local_index) = match self
.local_diff_model
.update_tracked_entities(ctx, self.local_index, start, &mut self.entity_manager)
.wrap_err("Failed to update locally tracked entities")
{
Ok(ret) => ret,
Err(s) => {
self.clear_buffer(ctx, &new_intersects)?;
return Err(s);
}
};
self.clear_buffer(ctx, &new_intersects)?;
if !new_intersects.is_empty() {
let init = self.local_diff_model.make_init();
send_remotedes(
self.local_diff_model.make_init();
let res = std::mem::take(&mut self.local_diff_model.init_buffer);
let (RemoteDes::EntityInit(diff), err) = send_remotedes_ret(
ctx,
true,
Destination::Peers(new_intersects.clone()),
RemoteDes::EntityUpdate(init),
)?;
Destination::Peers(new_intersects),
RemoteDes::EntityInit(res),
) else {
unreachable!()
};
self.local_diff_model.init_buffer = diff;
self.local_diff_model.uninit();
err?;
}
{
let proj = &mut self.pending_fired_projectiles.lock().unwrap();
if !proj.is_empty() {
let mut data = Vec::new();
for (ent, mut proj) in proj.drain(..) {
if ent.is_alive() {
if let Some(vel) = ent
.try_get_first_component_including_disabled::<VelocityComponent>(
None,
)?
{
proj.vel = vel.m_velocity().ok()
}
}
data.push(proj)
}
let data = proj
.drain(..)
.map(|(ent, mut proj)| {
if ent.is_alive()
&& let Ok(Some(vel)) = ent
.try_get_first_component_including_disabled::<VelocityComponent>(
None,
)
{
proj.vel = vel.m_velocity().ok()
}
proj
})
.collect();
send_remotedes(
ctx,
ctx.net,
true,
Destination::Peers(self.interest_tracker.iter_interested().collect()),
RemoteDes::Projectiles(data),
)?;
}
}
if !diff.is_empty() {
send_remotedes(
ctx,
true,
Destination::Peers(
self.interest_tracker
.iter_interested()
.filter(|p| !new_intersects.contains(p))
.collect(),
),
RemoteDes::EntityUpdate(diff),
)?;
}
if !dead.is_empty() {
send_remotedes(
ctx,
ctx.net,
true,
Destination::Peers(
ctx.player_map
@ -631,6 +744,9 @@ impl Module for EntitySync {
RemoteDes::DeadEntities(dead),
)?;
}
if self.log_performance {
times[3] = start.elapsed().as_micros() - (times[0] + times[1] + times[2]);
}
}
if frame_num > 120 {
let mut to_remove = Vec::new();
@ -643,14 +759,16 @@ impl Module for EntitySync {
match self.remote_models.get_mut(owner) {
Some(remote_model) => {
let vi = self.remote_index.entry(*owner).or_insert(0);
let v;
(v, t) = remote_model
.apply_entities(ctx, *vi, t)
let v = remote_model
.apply_entities(ctx, *vi, start, &mut self.entity_manager)
.wrap_err("Failed to apply entity infos")?;
self.remote_index.insert(*owner, v);
if self.log_performance {
times.push(start.elapsed().as_micros() - times.iter().sum::<u128>());
}
for lid in remote_model.drain_grab_request() {
send_remotedes(
ctx,
ctx.net,
true,
Destination::Peer(*owner),
RemoteDes::RequestGrab(lid),
@ -673,7 +791,7 @@ impl Module for EntitySync {
// These entities shouldn't be tracked by us, as they were spawned by remote.
self.look_current_entity = EntityID::max_in_use()?;
for (_, remote_model) in self.remote_models.iter_mut() {
remote_model.kill_entities(ctx)?;
remote_model.kill_entities(ctx, &mut self.entity_manager)?;
}
for (entity, offending_peer) in self.kill_later.drain(..) {
if entity.is_alive() {
@ -684,23 +802,16 @@ impl Module for EntitySync {
entity.try_get_first_component::<DamageModelComponent>(None)?
{
damage.object_set_value("damage_multipliers", "curse", 1.0)?;
noita_api::raw::entity_inflict_damage(
entity.raw() as i32,
entity.inflict_damage(
damage.max_hp()? * 100.0,
"DAMAGE_CURSE".into(), //TODO should be enum
"kill sync".into(),
"NONE".into(),
0.0,
0.0,
responsible_entity.map(|e| e.raw() as i32),
None,
None,
None,
DamageType::DamageCurse,
"kill sync",
responsible_entity,
)?;
}
}
}
if let Err(s) = self.spawn_once(ctx) {
if let Err(s) = self.spawn_once(ctx, frame_num as usize, x, y) {
crate::print_error(s)?;
}
@ -712,17 +823,21 @@ impl Module for EntitySync {
},
))?;
}
let pos_data = self.local_diff_model.get_pos_data(frame_num);
let pos_data = self.local_diff_model.get_pos_data(frame_num as usize);
if !pos_data.is_empty() {
ctx.net
.send(&NoitaOutbound::DesToProxy(UpdatePositions(pos_data)))?;
}
if self.log_performance {
times.push(start.elapsed().as_micros() - times.iter().sum::<u128>());
noita_api::print!("{times:?}");
}
Ok(())
}
fn on_projectile_fired(
&mut self,
_ctx: &mut super::ModuleCtx,
_ctx: &mut ModuleCtx,
shooter: Option<EntityID>,
projectile: Option<EntityID>,
_initial_rng: i32,
@ -779,15 +894,37 @@ impl Module for EntitySync {
}
fn send_remotedes(
ctx: &mut super::ModuleCtx<'_>,
ctx: &mut NetManager,
reliable: bool,
destination: Destination<PeerId>,
remote_des: RemoteDes,
) -> Result<(), eyre::Error> {
ctx.net.send(&NoitaOutbound::RemoteMessage {
let message = NoitaOutbound::RemoteMessage {
reliable,
destination,
message: RemoteMessage::RemoteDes(remote_des),
})?;
Ok(())
};
ctx.send(&message)
}
fn send_remotedes_ret(
ctx: &mut ModuleCtx<'_>,
reliable: bool,
destination: Destination<PeerId>,
remote_des: RemoteDes,
) -> (RemoteDes, Result<(), eyre::Error>) {
let message = NoitaOutbound::RemoteMessage {
reliable,
destination,
message: RemoteMessage::RemoteDes(remote_des),
};
let err = ctx.net.send(&message);
let NoitaOutbound::RemoteMessage {
message: RemoteMessage::RemoteDes(des),
..
} = message
else {
unreachable!()
};
(des, err)
}

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,9 @@ pub(crate) struct InterestTracker {
impl InterestTracker {
pub(crate) fn new(radius_hysteresis: f64) -> Self {
assert!(radius_hysteresis > 0.0);
unsafe {
std::hint::assert_unchecked(radius_hysteresis > 0.0);
}
Self {
radius_hysteresis,
x: 0.0,
@ -49,7 +51,7 @@ impl InterestTracker {
std::mem::take(&mut self.added_any)
}
pub(crate) fn drain_lost_interest(&mut self) -> impl Iterator<Item = PeerId> + '_ {
pub(crate) fn drain_lost_interest(&mut self) -> impl DoubleEndedIterator<Item = PeerId> + '_ {
self.lost_interest.drain(..)
}

View file

@ -0,0 +1,322 @@
use crate::modules::{Module, ModuleCtx};
use crate::{WorldSync, my_peer_id};
use eyre::{ContextCompat, eyre};
use noita_api::noita::types::{CellType, FireCell, GasCell, LiquidCell, Vec2i};
use noita_api::noita::world::ParticleWorldState;
use noita_api::{game_print, heap};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use shared::NoitaOutbound;
use shared::world_sync::{
CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, Pixel, ProxyToWorldSync, WorldSyncToProxy,
};
use std::mem::MaybeUninit;
use std::ptr;
impl Module for WorldSync {
fn on_world_init(&mut self, _ctx: &mut ModuleCtx) -> eyre::Result<()> {
self.particle_world_state = MaybeUninit::new(ParticleWorldState::new()?);
Ok(())
}
fn on_world_update(&mut self, ctx: &mut ModuleCtx) -> eyre::Result<()> {
let Some(ent) = ctx.player_map.get_by_left(&my_peer_id()) else {
return Ok(());
};
let Some(ent) = ctx.globals.entity_manager.get_entity(ent.0.get() as usize) else {
return Ok(());
};
let (x, y) = (ent.transform.pos.x, ent.transform.pos.y);
let updates = (0..9)
.into_par_iter()
.filter_map(|i| {
let dx = i % 3;
let dy = i / 3;
let cx = (x as i32).div_euclid(CHUNK_SIZE as i32) - 1 + dx;
let cy = (y as i32).div_euclid(CHUNK_SIZE as i32) - 1 + dy;
let mut update = NoitaWorldUpdate {
coord: ChunkCoord(cx, cy),
pixels: std::array::from_fn(|_| Pixel::default()),
};
if unsafe {
self.particle_world_state
.assume_init_ref()
.encode_world(&mut update)
}
.is_ok()
{
Some(update)
} else {
None
}
})
.collect::<Vec<_>>();
let msg = NoitaOutbound::WorldSyncToProxy(WorldSyncToProxy::Updates(updates));
ctx.net.send(&msg)?;
let Vec2i { x: cx, y: cy } = ctx.globals.game_global.m_grid_world.cam_pos;
let msg = NoitaOutbound::WorldSyncToProxy(WorldSyncToProxy::End(
Some((
x.div_euclid(CHUNK_SIZE as f32) as i32,
y.div_euclid(CHUNK_SIZE as f32) as i32,
cx.div_euclid(CHUNK_SIZE as isize) as i32,
cy.div_euclid(CHUNK_SIZE as isize) as i32,
false,
)),
1,
self.world_num,
));
ctx.net.send(&msg)?;
Ok(())
}
}
impl WorldSync {
pub fn handle_remote(&mut self, msg: ProxyToWorldSync) -> eyre::Result<()> {
match msg {
ProxyToWorldSync::Updates(updates) => {
updates.into_par_iter().for_each(|chunk| unsafe {
let _ = self
.particle_world_state
.assume_init_ref()
.decode_world(chunk);
});
}
}
Ok(())
}
}
pub const SCALE: isize = (512 / CHUNK_SIZE as isize).ilog2() as isize;
#[allow(unused)]
trait WorldData {
unsafe fn encode_world(&self, chunk: &mut NoitaWorldUpdate) -> eyre::Result<()>;
unsafe fn decode_world(&self, chunk: NoitaWorldUpdate) -> eyre::Result<()>;
}
impl WorldData for ParticleWorldState {
unsafe fn encode_world(&self, chunk: &mut NoitaWorldUpdate) -> eyre::Result<()> {
let ChunkCoord(cx, cy) = chunk.coord;
let (cx, cy) = (cx as isize, cy as isize);
let chunk = &mut chunk.pixels;
let Some(pixel_array) = unsafe { self.world_ptr.as_mut() }
.wrap_err("no world")?
.chunk_map
.get(cx >> SCALE, cy >> SCALE)
else {
return Err(eyre!("chunk not loaded"));
};
let mut chunk_iter = chunk.iter_mut();
let (shift_x, shift_y) = self.get_shift::<CHUNK_SIZE>(cx, cy);
for j in shift_y..shift_y + CHUNK_SIZE as isize {
for i in shift_x..shift_x + CHUNK_SIZE as isize {
*chunk_iter.next().unwrap() = pixel_array.get_pixel(i, j);
}
}
Ok(())
}
unsafe fn decode_world(&self, chunk: NoitaWorldUpdate) -> eyre::Result<()> {
let chunk_coord = chunk.coord;
let (cx, cy) = (chunk_coord.0 as isize, chunk_coord.1 as isize);
let Some(pixel_array) = unsafe { self.world_ptr.as_mut() }
.wrap_err("no world")?
.chunk_map
.get_mut(cx >> SCALE, cy >> SCALE)
else {
return Err(eyre!("chunk not loaded"));
};
let (shift_x, shift_y) = self.get_shift::<CHUNK_SIZE>(cx, cy);
let start_x = cx * CHUNK_SIZE as isize;
let start_y = cy * CHUNK_SIZE as isize;
for (i, pixel) in chunk.pixels.into_iter().enumerate() {
let x = (i % CHUNK_SIZE) as isize;
let y = (i / CHUNK_SIZE) as isize;
let cell = pixel_array.get_mut_raw(shift_x + x, shift_y + y);
let xs = start_x + x;
let ys = start_y + y;
if pixel.is_air() {
*cell = ptr::null_mut();
} else {
let Some(mat) = self.material_list.get_static(pixel.mat() as usize) else {
return Err(eyre!("mat does not exist"));
};
match mat.cell_type {
CellType::None => {}
CellType::Liquid => {
let mut liquid = unsafe {
LiquidCell::create(mat, self.cell_vtables.liquid(), self.world_ptr)
};
liquid.x = xs;
liquid.y = ys;
*cell = heap::place_new(liquid).cast();
}
CellType::Gas => {
let mut gas = unsafe {
GasCell::create(mat, self.cell_vtables.gas(), self.world_ptr)
};
gas.x = xs;
gas.y = ys;
*cell = heap::place_new(gas).cast();
}
CellType::Solid => {}
CellType::Fire => {
let mut fire = unsafe {
FireCell::create(mat, self.cell_vtables.fire(), self.world_ptr)
};
fire.x = xs;
fire.y = ys;
*cell = heap::place_new(fire).cast();
}
}
}
}
Ok(())
}
}
#[test]
pub fn test_world() {
use noita_api::noita::types::{
Cell, CellData, CellVTable, CellVTables, Chunk, ChunkMap, GridWorld, GridWorldThreaded,
GridWorldThreadedVTable, GridWorldVTable, NoneCellVTable, StdVec,
};
let mut threaded = GridWorldThreaded {
grid_world_threaded_vtable: &GridWorldThreadedVTable {},
unknown: [0; 287],
update_region: Default::default(),
};
let mut chunks: [*mut Chunk; 512 * 512] = [ptr::null_mut(); 512 * 512];
let chunk_map = ChunkMap {
len: 0,
unknown: 0,
chunk_array: unsafe { std::mem::transmute::<&mut _, &'static mut _>(&mut chunks) },
chunk_count: 0,
min_chunk: Default::default(),
max_chunk: Default::default(),
min_pixel: Default::default(),
max_pixel: Default::default(),
};
let mut grid_world = GridWorld {
vtable: &GridWorldVTable {
unknown: [ptr::null(); 3],
get_chunk_map: ptr::null(),
unknownmagic: ptr::null(),
unknown2: [ptr::null(); 29],
},
rng: 0,
unk: [0; 292],
cam_pos: Default::default(),
cam_dimen: Default::default(),
unknown: [0; 6],
unk_cam: Default::default(),
unk2_cam: Default::default(),
unkown3: 0,
cam: Default::default(),
unkown2: 0,
unk_counter: 0,
world_update_count: 0,
chunk_map,
unknown2: [0; 40],
m_thread_impl: unsafe { std::mem::transmute::<&mut _, &'static mut _>(&mut threaded) },
};
let mut pws = ParticleWorldState {
world_ptr: &mut grid_world,
material_list: StdVec::new(),
cell_vtables: CellVTables(
[CellVTable {
none: &NoneCellVTable {
unknown: [ptr::null(); 41],
},
}; 5],
),
};
for i in 0..256 {
let mut celldata = CellData::default();
celldata.material_type = i;
pws.material_list.push(celldata);
}
let mut list = [0; 512 * 512];
{
let mut data: [*mut Cell; 512 * 512] = [ptr::null_mut(); 512 * 512];
for (i, d) in data.iter_mut().enumerate() {
let mut celldata = CellData::default();
celldata.material_type = rand::random::<u8>() as isize;
list[i] = celldata.material_type;
let cell = Cell::create(
heap::place_new_ref(celldata),
CellVTable {
none: &NoneCellVTable {
unknown: [ptr::null_mut(); 41],
},
},
);
*d = heap::place_new(cell);
}
let chunk = Chunk {
data: unsafe { std::mem::transmute::<&mut _, &'static mut _>(&mut data) },
};
unsafe { pws.world_ptr.as_mut() }
.unwrap()
.chunk_map
.insert(0, 0, chunk);
}
{
let mut data: [*mut Cell; 512 * 512] = [ptr::null_mut(); 512 * 512];
for d in data.iter_mut() {
let celldata = CellData::default();
let cell = Cell::create(
heap::place_new_ref(celldata),
CellVTable {
none: &NoneCellVTable {
unknown: [ptr::null_mut(); 41],
},
},
);
*d = heap::place_new_ref(cell);
}
let chunk = Chunk {
data: unsafe { std::mem::transmute::<&mut _, &'static mut _>(&mut data) },
};
unsafe { pws.world_ptr.as_mut() }
.unwrap()
.chunk_map
.insert(1, 1, chunk);
}
let mut upd = NoitaWorldUpdate {
coord: ChunkCoord(5, 5),
pixels: [Pixel::default(); CHUNK_SIZE * CHUNK_SIZE],
};
unsafe {
assert!(pws.encode_world(&mut upd).is_ok());
}
assert_eq!(
upd.pixels[0..128]
.iter()
.map(|a| a.mat())
.collect::<Vec<_>>(),
vec![0; 128]
);
let tmr = std::time::Instant::now();
upd.coord = ChunkCoord(0, 0);
unsafe {
assert!(pws.encode_world(&mut upd).is_ok());
}
println!("{}", tmr.elapsed().as_nanos());
assert_eq!(
upd.pixels[0..128]
.iter()
.map(|a| a.mat())
.collect::<Vec<_>>(),
list[0..128].iter().map(|a| *a as u16).collect::<Vec<_>>()
);
let tmr = std::time::Instant::now();
upd.coord = ChunkCoord(5, 5);
unsafe {
assert!(pws.decode_world(upd.clone()).is_ok());
}
println!("{}", tmr.elapsed().as_nanos());
upd.coord = ChunkCoord(0, 0);
unsafe {
assert!(pws.encode_world(&mut upd).is_ok());
}
assert_eq!(
upd.pixels[0..128]
.iter()
.map(|a| a.mat())
.collect::<Vec<_>>(),
list[0..128].iter().map(|a| *a as u16).collect::<Vec<_>>()
);
}

View file

@ -12,6 +12,7 @@ impl NetManager {
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or_else(|| SocketAddr::new("127.0.0.1".parse().unwrap(), 21251));
#[cfg(debug_assertions)]
println!("Connecting to {address:?}");
let socket = MessageSocket::connect(&address)?;

View file

@ -1,101 +0,0 @@
use std::{ffi::c_void, mem};
pub(crate) mod ntypes;
pub(crate) mod pixel;
pub(crate) struct ParticleWorldState {
pub(crate) _world_ptr: *mut c_void,
pub(crate) chunk_map_ptr: *mut c_void,
pub(crate) material_list_ptr: *const c_void,
pub(crate) runner: pixel::PixelRunner<pixel::RawPixel>,
}
impl ParticleWorldState {
fn get_cell_raw(&self, x: i32, y: i32) -> Option<&ntypes::Cell> {
let x = x as isize;
let y = y as isize;
let chunk_index = (((((y) >> 9) - 256) & 511) * 512 + ((((x) >> 9) - 256) & 511)) * 4;
// Deref 1/3
let chunk_arr = unsafe { self.chunk_map_ptr.offset(8).cast::<*const c_void>().read() };
// Deref 2/3
let chunk = unsafe { chunk_arr.offset(chunk_index).cast::<*const c_void>().read() };
if chunk.is_null() {
return None;
}
// Deref 3/3
let pixel_array = unsafe { chunk.cast::<*const c_void>().read() };
let pixel = unsafe { pixel_array.offset((((y & 511) << 9) | x & 511) * 4) };
if pixel.is_null() {
return None;
}
unsafe { pixel.cast::<*const ntypes::Cell>().read().as_ref() }
}
fn get_cell_material_id(&self, cell: &ntypes::Cell) -> u16 {
let mat_ptr = cell.material_ptr();
let offset = unsafe { mat_ptr.cast::<c_void>().offset_from(self.material_list_ptr) };
(offset / ntypes::CELLDATA_SIZE) as u16
}
fn get_cell_type(&self, cell: &ntypes::Cell) -> Option<ntypes::CellType> {
unsafe { Some(cell.material_ptr().as_ref()?.cell_type) }
}
pub(crate) unsafe fn encode_area(
&mut self,
start_x: i32,
start_y: i32,
end_x: i32,
end_y: i32,
mut pixel_runs: *mut pixel::NoitaPixelRun,
) -> usize {
// Allow compiler to generate better code.
assert_eq!(start_x % 128, 0);
assert_eq!(start_y % 128, 0);
assert!((end_x - start_x) <= 128);
assert!((end_y - start_y) <= 128);
for y in start_y..end_y {
for x in start_x..end_x {
let mut raw_pixel = pixel::RawPixel {
material: 0,
flags: 0,
};
let cell = self.get_cell_raw(x, y);
if let Some(cell) = cell {
let cell_type = self.get_cell_type(cell).unwrap_or(ntypes::CellType::None);
match cell_type {
ntypes::CellType::None => {}
// Nobody knows how box2d pixels work.
ntypes::CellType::Solid => {}
ntypes::CellType::Liquid => {
raw_pixel.material = self.get_cell_material_id(cell);
let cell: &ntypes::LiquidCell = unsafe { mem::transmute(cell) };
raw_pixel.flags = cell.is_static as u8;
}
ntypes::CellType::Gas | ntypes::CellType::Fire => {
raw_pixel.material = self.get_cell_material_id(cell);
}
// ???
_ => {}
}
}
self.runner.put_pixel(raw_pixel);
}
}
let built_runner = self.runner.build();
let runs = built_runner.len();
for run in built_runner {
let noita_pixel_run = unsafe { pixel_runs.as_mut().unwrap() };
noita_pixel_run.length = (run.length - 1) as u16;
noita_pixel_run.material = run.data.material;
noita_pixel_run.flags = run.data.flags;
pixel_runs = unsafe { pixel_runs.offset(1) };
}
self.runner.clear();
runs
}
}

View file

@ -1,83 +0,0 @@
// Type defs borrowed from NoitaPatcher.
use std::ffi::{c_char, c_void};
pub(crate) const CELLDATA_SIZE: isize = 0x290;
#[repr(C)]
#[derive(Debug)]
pub(crate) struct StdString {
buffer: *const i8,
sso_buffer: [i8; 12],
size: usize,
capacity: usize,
}
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)]
#[expect(dead_code)]
pub(crate) enum CellType {
None = 0,
Liquid = 1,
Gas = 2,
Solid = 3,
Fire = 4,
Invalid = 4294967295,
}
#[repr(C)]
pub(crate) struct CellData {
name: StdString,
ui_name: StdString,
material_type: i32,
id_2: i32,
pub(crate) cell_type: CellType,
// Has a bunch of other fields that aren't that relevant.
}
#[repr(C)]
pub(crate) struct CellVTable {}
#[repr(C)]
pub(crate) struct Cell {
pub(crate) vtable: *const CellVTable,
hp: i32,
unknown1: [u8; 8],
is_burning: bool,
unknown2: [u8; 3],
material_ptr: *const CellData,
}
#[repr(C)]
pub(crate) struct LiquidCell {
cell: Cell,
x: i32,
y: i32,
unknown1: c_char,
unknown2: c_char,
pub(crate) is_static: bool,
// Has a bunch of other fields that aren't that relevant.
}
impl Cell {
pub(crate) fn material_ptr(&self) -> *const CellData {
self.material_ptr
}
}
#[repr(C)]
pub(crate) struct Entity {
_unknown0: [u8; 8],
_filename_index: u32,
// More stuff, not that relevant currently.
}
#[repr(C)]
pub(crate) struct EntityManager {
_fld: c_void,
// Unknown
}
#[repr(C)]
pub(crate) struct ThiscallFn(c_void);

View file

@ -1,78 +0,0 @@
#[repr(C, packed)]
pub(crate) struct NoitaPixelRun {
pub(crate) length: u16,
pub(crate) material: u16,
pub(crate) flags: u8,
}
/// Copied from proxy.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
/// Copied from proxy.
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug)]
pub(crate) struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
/// Copied from proxy.
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub(crate) struct PixelRunner<Pixel> {
pub(crate) current_pixel: Option<Pixel>,
pub(crate) current_run_len: u32,
pub(crate) runs: Vec<PixelRun<Pixel>>,
}
impl<Pixel: Eq + Copy> Default for PixelRunner<Pixel> {
fn default() -> Self {
Self::new()
}
}
impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
pub(crate) fn new() -> Self {
Self {
current_pixel: None,
current_run_len: 0,
runs: Vec::new(),
}
}
pub(crate) fn put_pixel(&mut self, pixel: Pixel) {
if let Some(current) = self.current_pixel {
if pixel != current {
self.runs.push(PixelRun {
length: self.current_run_len,
data: current,
});
self.current_pixel = Some(pixel);
self.current_run_len = 1;
} else {
self.current_run_len += 1;
}
} else {
self.current_pixel = Some(pixel);
self.current_run_len = 1;
}
}
pub(crate) fn build(&mut self) -> &[PixelRun<Pixel>] {
if self.current_run_len > 0 {
self.runs.push(PixelRun {
length: self.current_run_len,
data: self.current_pixel.expect("has current pixel"),
});
}
&mut self.runs
}
pub(crate) fn clear(&mut self) {
self.current_pixel = None;
self.current_run_len = 0;
self.runs.clear();
}
}

65
flake.lock generated Normal file
View file

@ -0,0 +1,65 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1759070547,
"narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "647e5c14cbd5067f44ac86b74f014962df460840",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"systems": "systems"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1759199574,
"narHash": "sha256-w24RYly3VSVKp98rVfCI1nFYfQ0VoWmShtKPCbXgK6A=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "381776b12d0d125edd7c1930c2041a1471e586c0",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"flake": false,
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

40
flake.nix Normal file
View file

@ -0,0 +1,40 @@
{
description = "Noita Entangled Worlds";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
systems = {
url = "github:nix-systems/default";
flake = false;
};
};
outputs = { self, nixpkgs, rust-overlay, systems, }:
let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = [ self.overlays.default ];
});
in {
overlays = import ./nix/overlays { inherit self lib rust-overlay; };
packages = lib.mapAttrs (system: pkgs: {
default = self.packages.${system}.noita-proxy;
inherit (pkgs) noita-proxy;
}) pkgsFor;
devShells = lib.mapAttrs
(system: pkgs: { default = pkgs.callPackage ./nix/shell.nix { }; })
pkgsFor;
formatter =
eachSystem (system: nixpkgs.legacyPackages.${system}.nixfmt-classic);
};
}

View file

@ -1,12 +1,7 @@
## Noita Entangled Worlds v1.5.0
## Noita Entangled Worlds v1.6.2
- fix invincible enemys
- remove the ton of debug prints i forgot about to prob help performance
- some minor world sync changes
- fix chunk map on first enter
- add logs to lobby ui
## Accepted pull requests

27
nix/overlays/default.nix Normal file
View file

@ -0,0 +1,27 @@
{ self, lib, rust-overlay }:
let rustPackageOverlay = import ./rust-package.nix;
in {
default = lib.composeManyExtensions [
# This is to ensure that other overlays and invocations of `callPackage`
# receive `rust-bin`, but without hard-coding a specific derivation.
# This can be overridden by consumers.
self.overlays.rust-overlay
# Packages provided by this flake:
self.overlays.noita-proxy
];
# This flake exposes `overlays.rust-overlay` which is automatically applied
# by `overlays.default`. This overlay is intended to provide `rust-bin` for
# the package overlay, in the event it is not already present.
rust-overlay = final: prev:
if prev ? rust-bin then { } else rust-overlay.overlays.default final prev;
# The overlay definition uses `rust-bin` to construct a `rustPlatform`,
# and `rust-bin` is not provided by this particular overlay.
# Prefer using `overlays.default`, or composing with `rust-overlay` manually.
noita-proxy = rustPackageOverlay {
packageName = "noita-proxy";
sourceRoot = self;
};
}

View file

@ -0,0 +1,20 @@
# This function is imported as `rustPackageOverlay` in `nix/overlays/default.nix`.
#
# Supplies a stable `rustPlatform` from `rust-bin` to `callPackage`.
# The `rust-overlay` must have already been composed onto the `pkgs` set.
#
# This prevents `rust-bin` from being an input of the package, which would
# make it less portable.
{ packageName, sourceRoot }:
final: _prev:
let
rust-stable = final.rust-bin.stable.latest.minimal;
rustPlatform = final.makeRustPlatform {
cargo = rust-stable;
rustc = rust-stable;
};
in {
${packageName} = final.callPackage "${../packages}/${packageName}.nix" {
inherit sourceRoot rustPlatform;
};
}

View file

@ -0,0 +1,95 @@
{ sourceRoot, lib, runCommandNoCC, rustPlatform, copyDesktopItems
, makeDesktopItem, pkg-config, cmake, patchelf, imagemagick, openssl, libjack2
, alsa-lib, libopus, wayland, libxkbcommon, libGL }:
rustPlatform.buildRustPackage (finalAttrs:
let
inherit (finalAttrs) src pname version meta buildInputs steamworksRedist;
manifest = lib.importTOML "${src}/noita-proxy/Cargo.toml";
in {
pname = "noita-entangled-worlds-proxy";
inherit (manifest.package) version;
# The real root of the entire project.
# This is the only place `sourceRoot` is used.
src = sourceRoot;
# The root of this particular binary crate to build.
sourceRoot = "source/noita-proxy";
cargoLock.lockFile = "${src}/noita-proxy/Cargo.lock";
strictDeps = true;
nativeBuildInputs =
[ copyDesktopItems pkg-config cmake patchelf imagemagick ];
# TODO: Add dependencies for X11 desktop environments.
buildInputs = [
openssl
libjack2
alsa-lib
libopus
wayland
libxkbcommon
libGL
steamworksRedist
];
env = {
OPENSSL_DIR = "${lib.getDev openssl}";
OPENSSL_LIB_DIR = "${lib.getLib openssl}/lib";
OPENSSL_NO_VENDOR = 1;
};
checkFlags = [
# Disable networked tests
"--skip bookkeeping::releases::test::release_assets"
];
# TODO: Research which icon sizes are most important. These are what I found on my system.
postInstall = ''
for size in 16 20 22 24 32 48 64 96 128 144 180 192 256 512 1024; do
icon_dir=$out/share/icons/hicolor/''${size}x''${size}/apps
mkdir -p $icon_dir
magick assets/icon.png \
-strip -filter Point -resize ''${size}x''${size} \
$icon_dir/noita-proxy.png
done
'';
postFixup = ''
patchelf $out/bin/noita-proxy \
--set-rpath ${lib.makeLibraryPath buildInputs}
'';
# This attribute is defined here instead of a `let` block, because in this position,
# it can be overridden with `overrideAttrs`, and shares a `src` with the top-level.
steamworksRedist =
runCommandNoCC "${pname}-steamworks-redist" { inherit src; } ''
install -Dm555 $src/redist/libsteam_api.so -t $out/lib
'';
desktopItems = [
(makeDesktopItem {
name = "noita-proxy";
desktopName = "Noita Entangled Worlds";
comment = meta.description;
exec = "noita-proxy";
icon = "noita-proxy";
categories = [ "Game" "Utility" ];
keywords = [ "noita" "proxy" "server" "steam" "game" ];
terminal = false;
singleMainWindow = true;
})
];
meta = {
inherit (manifest.package) description;
homepage = "https://github.com/IntQuant/noita_entangled_worlds";
changelog =
"https://github.com/IntQuant/noita_entangled_worlds/releases/tag/v${version}";
license = with lib.licenses; [ mit asl20 ];
platforms = [ "x86_64-linux" ];
maintainers = with lib.maintainers; [ spikespaz ];
mainProgram = "noita-proxy";
};
})

26
nix/shell.nix Normal file
View file

@ -0,0 +1,26 @@
{ rust-bin, mkShell, noita-proxy }:
mkShell {
strictDeps = true;
inputsFrom = [ noita-proxy ];
packages = [
# Derivations in `rust-stable` provide the toolchain,
# must be listed first to take precedence over nightly.
(rust-bin.stable.latest.minimal.override {
extensions = [ "rust-src" "rust-docs" "clippy" ];
})
# Use rustfmt, and other tools that require nightly features.
(rust-bin.selectLatestNightlyWith (toolchain:
toolchain.minimal.override {
extensions = [ "rustfmt" "rust-analyzer" ];
}))
];
env = {
inherit (noita-proxy) OPENSSL_DIR OPENSSL_LIB_DIR OPENSSL_NO_VENDOR;
RUST_BACKTRACE = 1;
};
}

2984
noita-proxy/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,62 +4,65 @@ resolver = "2"
[package]
name = "noita-proxy"
description = "Noita Entangled Worlds companion app."
version = "1.5.0"
description = "Noita Entangled Worlds proxy application."
version = "1.6.2"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eframe = { version= "0.31.1", features = ["glow", "default_fonts", "wayland", "x11"], default-features = false }
rfd = "0.15.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
eframe = { version= "0.32.1", features = ["glow", "default_fonts", "wayland", "x11"], default-features = false }
rfd = "0.15.4"
egui_extras = { version = "0.32.1", features = ["all_loaders"] }
#egui_plot = "0.29.0"
image = { version = "0.25.1", default-features = false, features = ["png", "webp"] }
image = { version = "0.25.6", default-features = false, features = ["png", "webp"] }
wide = "0.7.30"
rayon = "1.10.0"
ron = "0.8.1"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing = "0.1.40"
wide = "0.7.33"
rayon = "1.11.0"
ron = "0.11.0"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
tracing = "0.1.41"
tangled = { path = "tangled" }
serde = { version = "1.0.207", features = ["serde_derive", "derive"] }
bitcode = "0.6.3"
lz4_flex = { version = "0.11.3", default-features = false, features = ["std"]}
rand = "0.9.0"
serde = { version = "1.0.219", features = ["serde_derive", "derive"] }
bitcode = "0.6.7"
lz4_flex = { version = "0.11.5", default-features = false, features = ["std"]}
rand = "0.9.2"
steamworks = "0.11.0"
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
clipboard = "0.5.0"
socket2 = { version = "0.5.7", features = ["all"] }
reqwest = { version = "0.12.12", features = ["blocking", "json"]}
arboard = { version = "3.6.1", features = ["wayland-data-control"]}
socket2 = { version = "0.6.0", features = ["all"] }
reqwest = { version = "0.12.23", features = ["blocking", "json"]}
poll-promise = "0.3.0"
zip = "2.2.0"
self-replace = "1.3.7"
bytemuck = { version = "1.16.0", features = ["derive"] }
rustc-hash = "2.0.0"
fluent-templates = "0.13.0"
unic-langid = { version = "0.9.5", features = ["serde"] }
fluent-bundle = "0.15.3"
argh = "0.1.12"
zip = "4.5.0"
self-replace = "1.5.0"
rustc-hash = "2.1.1"
fluent-templates = "0.13.1"
unic-langid = { version = "0.9.6", features = ["serde"] }
fluent-bundle = "0.16.0"
argh = "0.1.13"
shlex = "1.3.0"
quick-xml = { version = "0.37.0", features = ["serialize"] }
dashmap = "6.0.1"
quick-xml = { version = "0.38.3", features = ["serialize"] }
dashmap = "6.1.0"
eyre = "0.6.12"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
tracing-appender = "0.2.3"
shared = {path = "../shared"}
rstar = "0.12.2"
cpal = {version="0.15.3", features=["jack"]}
rodio = "0.20.1"
cpal = {version= "0.16.0", features=["jack"]}
rodio = "0.21.1"
opus = "0.3.0"
rubato = "0.16.1"
rubato = "0.16.2"
directories = "6.0.0"
#fundsp = {version = "0.20.0", default-features = false, features = ["std"]}
[target.'cfg(windows)'.dependencies]
winapi={version="0.3.9",features = ["wincon"]}
[dev-dependencies]
serial_test = "3.2.0"
[build-dependencies]
winresource = "0.1.17"
winresource = "0.1.23"
[profile.dev]
opt-level = 1

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>noita-proxy</string>
<key>CFBundleDisplayName</key>
<string>noita proxy</string>
<key>CFBundleIdentifier</key>
<string>com.intquant.entangled</string>
<key>CFBundleVersion</key>
<string>1.5.5</string>
<key>CFBundleShortVersionString</key>
<string>1.5.5</string>
<key>CFBundleExecutable</key>
<string>noita-proxy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

View file

@ -171,6 +171,7 @@ hide-cursors-checkbox-tooltip = Manchmal kann man die Cursor anderer Spieler mit
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -168,6 +168,7 @@ hide-cursors-checkbox = Disable others' cursors
hide-cursors-checkbox-tooltip = Sometimes you can confuse your friends' cursors with yours. In that case, you can disable them altogether with this checkbox.
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -171,6 +171,7 @@ hide-cursors-checkbox-tooltip = Il arrive de confondre le curseur de tes amis av
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -144,6 +144,7 @@ Info = Info
connect_settings_random_ports = Don't use a predetermined port. Makes things a bit more robust and allows multiple proxies to be launched on the same computer, but Noita will have to be launched through the proxy.
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -169,6 +169,7 @@ hide-cursors-checkbox-tooltip = 가끔씩 다른 플레이어의 커서를 자
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -169,6 +169,7 @@ hide-cursors-checkbox-tooltip = De vez em quando você pode confundir o cursor d
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -65,8 +65,8 @@ error_lobby_does_not_exist = Лобби не существует.
launcher_already_started = Noita уже запущена.
launcher_no_command = Не получается запустить Noita: отсутствует команда запуска.
launcher_no_command_2 = Launch command can be specified with --launch-cmd <command> option.
launcher_no_command_3 = You can put `noita-proxy --launch-cmd "%command%"` in steam's launch options to intercept whatever command steam uses to start the game.
launcher_no_command_2 = Команду запуска можно уточнить с помощью опции --launch-cmd <команда>.
launcher_no_command_3 = Вы можете вставить `noita-proxy --launch-cmd "%command%"` в настройки запуска Steam чтобы перехватить команду, которую Steam использует для запуска.
launcher_start_game = Запустить Noita
launcher_end_run = Закончить забег
launcher_end_run_confirm = Подтвердить
@ -89,8 +89,8 @@ connect_settings_autostart = Запускать игру автоматичес
## Game settings
Player-have-same-starting-loadout = Player have same starting loadout
connect_settings_spacewars = Allow using steam networking even if you don't have the game on steam, in case you have the gog version of the game. All players need this ticked to work, restart proxy to take effect
Player-have-same-starting-loadout = У игроков одинаковые начальные предметы
connect_settings_spacewars = Разрешить использование сетевой игры через Steam при отсутствии лицензионной версии игры в Steam, в случае покупки GOG версии. У всех игроков должна быть включена настройка, применяется после перезапуска.
Health-per-player = Стартовое здоровье
Enable-friendly-fire = Включить дружественный огонь
Have-perk-pools-be-independent-of-each-other = Сделать перки локальными для каждого игрока
@ -146,11 +146,11 @@ ip_wait_for_connection = Подключение к ip...
## Info
info_stress_tests = We're doing public lobbies (a.k.a stress tests) every saturday, 18:00 UTC. Join our discord for more info.
Info = Info
info_stress_tests = Мы создаём публичные лобби (стресс тесты) каждую субботу, 21:00 по МСК. Посетите Discord для дополнительной информации.
Info = Информация
## Local settings
connect_settings_random_ports = Don't use a predetermined port. Makes things a bit more robust and allows multiple proxies to be launched on the same computer, but Noita will have to be launched through the proxy.
connect_settings_random_ports = Не использовать предопределенный порт. Делает вещи слегка надёжнее и позволяет запускать несколько прокси на одном ПК, но Noita придётся запускать через прокси.
## UX settings
@ -165,14 +165,15 @@ hide-cursors-checkbox-tooltip = Иногда можно перепутать к
## Steam connect
Make-lobby-public = Make lobby public
Switch-mode-and-restart = Переключить режим и перезагрузиться
Make-lobby-public = Сделать лобби публичным
## Lobby list
Open-lobby-list = Open lobby list
Only-EW-lobbies = Only EW lobbies
Join = Join
Not-Entangled-Worlds-lobby = Not Entangled Worlds lobby
No-public-lobbies-at-the-moment = No public lobbies at the moment :(
Lobby-list-pending = Lobby list pending...
Refresh = Refresh
Lobby-list = Lobby list
Open-lobby-list = Открыть список лобби
Only-EW-lobbies = Только лобби EW
Join = Присоединиться
Not-Entangled-Worlds-lobby = Не лобби Entangled Worlds
No-public-lobbies-at-the-moment = Сейчас нет публичных лобби :(
Lobby-list-pending = Загрузка списка лобби...
Refresh = Поиск
Lobby-list = Список лобби

View file

@ -167,6 +167,7 @@ hide-cursors-checkbox = 禁用其他人的光标
hide-cursors-checkbox-tooltip = 有时候你可能会把朋友的光标和自己的混淆。在这种情况下,你可以通过这个复选框完全禁用它们。
## Steam connect
Switch-mode-and-restart = Switch mode and restart
Make-lobby-public = Make lobby public
## Lobby list

View file

@ -2,5 +2,7 @@
pub mod mod_manager;
pub mod noita_launcher;
pub mod releases;
pub mod save_paths;
pub mod save_state;
pub mod self_restart;
pub mod self_update;

View file

@ -323,7 +323,7 @@ impl Modmanager {
}
}
State::EyreErrorReport(err) => {
ui.label(format!("Encountered an error: \n {:?}", err));
ui.label(format!("Encountered an error: \n {err:?}"));
if ui.button(tr("button_retry")).clicked() {
self.state = State::JustStarted;
}
@ -368,7 +368,7 @@ fn mod_downloader_for(
.build()
.wrap_err("Failed to build client")?;
get_release_by_tag(&client, tag.clone())
.wrap_err_with(|| format!("while getting release for tag {:?}", tag))
.wrap_err_with(|| format!("while getting release for tag {tag:?}"))
.and_then(|release| {
release
.get_release_assets(&client)

View file

@ -89,7 +89,7 @@ impl NoitaLauncher {
}
}
pub fn launch_token(&mut self) -> LaunchTokenResult {
pub fn launch_token(&mut self) -> LaunchTokenResult<'_> {
if self.check_if_noita_running() {
return LaunchTokenResult::AlreadyStarted;
}
@ -128,12 +128,13 @@ fn linux_try_get_noita_start_cmd(
.ok()?;
let runtime_appid = BufReader::new(tool_manifest)
.lines()
.nth(4)
.map(|a| a.unwrap().split('"').nth(3).map(|b| b.parse::<u32>()));
.map(|l| l.unwrap())
.find(|l| l.contains("require_tool_appid"))
.map(|a| a.split('"').nth(3).map(|b| b.parse::<u32>()));
match (steam_state, runtime_appid) {
(Some(state), Some(Some(Ok(appid)))) => {
(Some(state), Some(Some(Ok(1628350)))) => {
let apps = state.client.apps();
let app_id = AppId::from(appid);
let app_id = AppId::from(1628350);
let app_install_dir = apps.app_install_dir(app_id);
Some(NoitaStartCmd {
executable: PathBuf::from(app_install_dir)
@ -208,6 +209,8 @@ impl LaunchToken<'_> {
info!("Steam install: {}", steam_install.display());
info!("Compat data: {}", compat_data.display());
info!("Game path: {}", game_path.display());
info!("Exe path: {}", start_cmd.executable.to_str().unwrap());
info!("Args: {:?}", start_cmd.args);
Command::new(&start_cmd.executable)
.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", steam_install)

View file

@ -64,15 +64,12 @@ fn download_thread(
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-agent", "noita proxy")
.send()
.wrap_err_with(|| format!("Failed to download from {}", url))?;
.wrap_err_with(|| format!("Failed to download from {url}"))?;
let mut buf = [0; 4096];
loop {
let len = response.read(&mut buf).wrap_err_with(|| {
format!(
"Failed to download from {}: couldn't read from response",
url
)
format!("Failed to download from {url}: couldn't read from response")
})?;
shared
.progress
@ -81,7 +78,7 @@ fn download_thread(
break;
}
file.write_all(&buf[..len])
.wrap_err_with(|| format!("Failed to download from {}: couldn't write to file", url))?;
.wrap_err_with(|| format!("Failed to download from {url}: couldn't write to file"))?;
}
Ok(())
@ -109,7 +106,7 @@ impl Downloader {
pub fn show_progress(&self, ui: &mut Ui) {
let (current, max) = self.progress();
ui.label(format!("{} out of {} bytes", current, max));
ui.label(format!("{current} out of {max} bytes"));
ui.add(egui::ProgressBar::new(current as f32 / max as f32));
ui.ctx().request_repaint_after(Duration::from_millis(200));
}
@ -230,12 +227,9 @@ pub fn get_release_by_tag(client: &Client, tag: Tag) -> Result<Release, Releases
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-agent", "noita proxy")
.send()
.wrap_err_with(|| format!("Failed to get release by tag from {}", url))?;
.wrap_err_with(|| format!("Failed to get release by tag from {url}"))?;
let response = response.error_for_status().wrap_err_with(|| {
format!(
"Failed to get release by tag from {}: response returned an error",
url
)
format!("Failed to get release by tag from {url}: response returned an error")
})?;
Ok(response.json()?)
}

View file

@ -0,0 +1,80 @@
use std::{
fs::{self, File},
io::{Read, Write},
path::PathBuf,
};
use directories::ProjectDirs;
use tracing::{info, warn};
use crate::Settings;
pub(crate) struct SavePaths {
settings_path: PathBuf,
pub save_state_path: PathBuf,
}
impl SavePaths {
pub fn new() -> Self {
if Self::settings_next_to_exe_path().exists() {
Self::new_next_to_exe()
} else if let Some(project_dirs) = Self::project_dirs() {
info!("Using 'system' paths to store things");
let me = Self {
settings_path: project_dirs.config_dir().join("proxy.ron"),
save_state_path: project_dirs.data_dir().join("save_state"),
};
info!("Settings path: {}", me.settings_path.display());
let _ = fs::create_dir_all(project_dirs.config_dir());
let _ = fs::create_dir_all(&me.save_state_path);
me
} else {
warn!("Failed to get project dirst!");
Self::new_next_to_exe()
}
}
fn new_next_to_exe() -> Self {
info!("Using 'next to exe' path to store things");
Self {
settings_path: Self::settings_next_to_exe_path(),
save_state_path: Self::next_to_exe_path().join("save_state"),
}
}
fn project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("", "quant", "entangledworlds")
}
fn next_to_exe_path() -> PathBuf {
std::env::current_exe()
.map(|p| p.parent().unwrap().to_path_buf())
.unwrap_or(".".into())
}
fn settings_next_to_exe_path() -> PathBuf {
let base_path = std::env::current_exe()
.map(|p| p.parent().unwrap().to_path_buf())
.unwrap_or(".".into());
let config_name = "proxy.ron";
base_path.join(config_name)
}
pub fn load_settings(&self) -> Settings {
if let Ok(mut file) = File::open(&self.settings_path) {
let mut s = String::new();
let _ = file.read_to_string(&mut s);
ron::from_str::<Settings>(&s).unwrap_or_default()
} else {
info!("Failed to load settings file, returing default settings");
Settings::default()
}
}
pub fn save_settings(&self, settings: Settings) {
let settings = ron::to_string(&settings).unwrap();
if let Ok(mut file) = File::create(&self.settings_path) {
file.write_all(settings.as_bytes()).unwrap();
}
}
}

View file

@ -1,6 +1,6 @@
use std::{
fs, io,
path::PathBuf,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{self, AtomicBool},
@ -26,13 +26,16 @@ pub struct SaveState {
}
impl SaveState {
pub(crate) fn new(path: PathBuf) -> Self {
let has_savestate = path.join("run_info.bit").exists();
pub(crate) fn new(path: impl AsRef<Path>) -> Self {
let has_savestate = path.as_ref().join("run_info.bit").exists();
info!("Has savestate: {has_savestate}");
if let Err(err) = fs::create_dir_all(&path) {
error!("Error while creating directories: {err}");
}
let path = path.canonicalize().unwrap_or(path);
let path = path
.as_ref()
.canonicalize()
.unwrap_or(path.as_ref().to_path_buf());
info!("Will save to: {}", path.display());
Self {
path,

View file

@ -0,0 +1,37 @@
use std::{
convert::Infallible,
io,
process::{Command, exit},
};
use crate::lobby_code::{LobbyCode, LobbyKind};
pub struct SelfRestarter {
command: Command,
}
impl SelfRestarter {
pub fn new() -> io::Result<Self> {
// Hopefully fine to restart ourselves?
let exe_path = std::env::current_exe()?;
let command = Command::new(exe_path);
Ok(Self { command })
}
pub fn override_lobby_kind(&mut self, lobby_mode: LobbyKind) -> &mut Self {
self.command.arg("--override-lobby-kind");
self.command.arg(format!("{lobby_mode:?}"));
self
}
pub fn connect_to(&mut self, lobby: LobbyCode) -> &mut Self {
self.command.arg("--auto-connect-to");
self.command.arg(lobby.serialize());
self
}
pub fn restart(&mut self) -> io::Result<Infallible> {
self.command.spawn()?;
exit(0)
}
}

View file

@ -15,6 +15,8 @@ use crate::{
releases::{Downloader, ReleasesError, Version, get_latest_release},
};
use super::self_restart::SelfRestarter;
struct VersionCheckResult {
newest: Version,
ord: Ordering,
@ -126,7 +128,7 @@ impl SelfUpdateManager {
None => {}
}
}
Some(Err(err)) => self.state = State::ReleasesError2(format!("{:?}", err)),
Some(Err(err)) => self.state = State::ReleasesError2(format!("{err:?}")),
None => {
ui.label(tr("selfupdate_receiving_rel_info"));
ui.spinner();
@ -135,9 +137,10 @@ impl SelfUpdateManager {
State::Unpack(promise) => match promise.ready() {
Some(Ok(_)) => {
ui.label(tr("selfupdate_updated"));
let _ = SelfRestarter::new().and_then(|mut r| r.restart());
}
Some(Err(err)) => {
ui.label(format!("Could not update proxy: {}", err));
ui.label(format!("Could not update proxy: {err}"));
}
None => {
ctx.request_repaint();
@ -146,10 +149,10 @@ impl SelfUpdateManager {
}
},
State::ReleasesError(err) => {
ui.label(format!("Encountered an error: {:?}", err));
ui.label(format!("Encountered an error: {err:?}"));
}
State::ReleasesError2(err) => {
ui.label(format!("Encountered an error:\n{}", err));
ui.label(format!("Encountered an error:\n{err}"));
}
}
}
@ -158,6 +161,8 @@ impl SelfUpdateManager {
fn proxy_asset_name() -> &'static str {
if cfg!(target_os = "windows") {
"noita-proxy-win.zip"
} else if cfg!(target_os = "macos") {
"noita-proxy-macos-arm64.zip"
} else {
"noita-proxy-linux.zip"
}
@ -166,11 +171,23 @@ fn proxy_asset_name() -> &'static str {
fn proxy_bin_name() -> &'static str {
if cfg!(target_os = "windows") {
"noita_proxy.exe"
} else if cfg!(target_os = "macos") {
"noita_proxy"
} else {
"noita_proxy.x86_64"
}
}
fn steam_lib_name() -> &'static str {
if cfg!(target_os = "windows") {
"steam_api64.dll"
} else if cfg!(target_os = "macos") {
"libsteam_api.dylib"
} else {
"libsteam_api.so"
}
}
fn proxy_downloader_for(download_path: PathBuf) -> Result<Downloader, ReleasesError> {
let client = Client::builder().timeout(None).build()?;
get_latest_release(&client)
@ -180,19 +197,38 @@ fn proxy_downloader_for(download_path: PathBuf) -> Result<Downloader, ReleasesEr
}
fn extract_and_remove_zip(zip_file: PathBuf) -> Result<(), ReleasesError> {
let reader = File::open(&zip_file)?;
let exe_path = std::env::current_exe()?;
let current = std::env::current_dir()?;
if let Some(exe_dir) = exe_path.parent() {
std::env::set_current_dir(exe_dir)?;
} else {
Err(eyre::eyre!("exe does not have a parent"))?
}
let extract_to = Path::new("tmp.exec");
let bin_name = proxy_bin_name();
let reader = File::open(&zip_file)?;
let mut zip = zip::ZipArchive::new(reader)?;
info!("Extracting zip file");
let mut src = zip.by_name(bin_name)?;
let mut dst = File::create(extract_to)?;
io::copy(&mut src, &mut dst)?;
{
let mut src = zip.by_name(bin_name)?;
let mut dst = File::create(extract_to)?;
io::copy(&mut src, &mut dst)?;
}
let lib_name = steam_lib_name();
let extract_lib_to = Path::new("tmp.lib");
{
let mut src = zip.by_name(lib_name)?;
let mut dst = File::create(extract_lib_to)?;
io::copy(&mut src, &mut dst)?;
fs::rename(lib_name, format!("{lib_name}.tmp"))?;
fs::rename(extract_lib_to, lib_name)?;
}
self_replace::self_replace(extract_to)?;
info!("Zip file extracted");
fs::remove_file(&zip_file).ok();
fs::remove_file(extract_to).ok();
std::env::set_current_dir(current)?;
fs::remove_file(&zip_file).ok();
Ok(())
}

View file

@ -1,17 +1,18 @@
use arboard::Clipboard;
use bitcode::{Decode, Encode};
use bookkeeping::{
noita_launcher::{LaunchTokenResult, NoitaLauncher},
releases::Version,
save_paths::SavePaths,
save_state::SaveState,
self_restart::SelfRestarter,
};
use clipboard::{ClipboardContext, ClipboardProvider};
use cpal::traits::{DeviceTrait, HostTrait};
use eframe::egui::load::TexturePoll;
use eframe::egui::{
self, Align2, Button, Color32, ComboBox, Context, DragValue, FontDefinitions, FontFamily,
ImageButton, InnerResponse, Key, Layout, Margin, OpenUrl, OutputCommand, Rect, RichText,
ScrollArea, Sense, SizeHint, Slider, TextureOptions, ThemePreference, Ui, UiBuilder, Vec2,
Visuals, Window, pos2,
ImageButton, InnerResponse, Key, Layout, Margin, OpenUrl, Rect, RichText, ScrollArea, Sense,
SizeHint, Slider, TextureOptions, ThemePreference, Ui, UiBuilder, Vec2, Visuals, Window, pos2,
};
use eframe::epaint::TextureHandle;
use image::DynamicImage::ImageRgba8;
@ -28,8 +29,6 @@ use player_cosmetics::PlayerPngDesc;
use rustc_hash::FxHashMap;
use self_update::SelfUpdateManager;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{Read, Write};
use std::process::exit;
use std::thread::sleep;
use std::{collections::HashMap, fs, str::FromStr};
@ -56,13 +55,13 @@ use util::{args::Args, steam_helper::LobbyExtraData};
mod bookkeeping;
use crate::net::messages::NetMsg;
use crate::net::omni::OmniPeerId;
use crate::net::world::world_model::ChunkCoord;
use crate::player_cosmetics::{
display_player_skin, get_player_skin, player_path, player_select_current_color_slot,
player_skin_display_color_picker, shift_hue,
};
pub use bookkeeping::{mod_manager, releases, self_update};
use shared::WorldPos;
use shared::world_sync::ChunkCoord;
mod lobby_code;
pub mod net;
mod player_cosmetics;
@ -96,7 +95,7 @@ impl Display for GameMode {
GameMode::LocalHealth(LocalHealthMode::PermaDeath) => "LocalPermadeath",
GameMode::LocalHealth(LocalHealthMode::PvP) => "PvP",
};
write!(f, "{}", desc)
write!(f, "{desc}")
}
}
@ -127,7 +126,7 @@ pub enum LocalHealthMode {
#[serde(default)]
pub struct GameSettings {
seed: u64,
world_num: u16,
world_num: u8,
debug_mode: Option<bool>,
use_constant_seed: bool,
duplicate: Option<bool>,
@ -658,6 +657,7 @@ enum AppState {
SelfUpdate,
LangPick,
AskSavestateReset,
GogModeIssue(LobbyCode),
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -1086,7 +1086,7 @@ impl ImageMap {
) {
for (p, (coord, is_dead, does_exist, img)) in map {
if !self.players.contains_key(p) {
let name = format!("{}", p);
let name = format!("{p}");
let size = [img.width() as usize, img.height() as usize];
let color_image = egui::ColorImage::from_rgba_unmultiplied(
size,
@ -1122,7 +1122,15 @@ impl ImageMap {
}
if self.notplayer.is_none() {
self.notplayer = egui::include_image!("../assets/notplayer.png")
.load(ctx, TextureOptions::NEAREST, SizeHint::Size(7, 17))
.load(
ctx,
TextureOptions::NEAREST,
SizeHint::Size {
width: 7,
height: 17,
maintain_aspect_ratio: true,
},
)
.ok();
}
{
@ -1200,14 +1208,14 @@ impl ImageMap {
}
}
let tile_size = self.zoom * 128.0;
if let Some(peer) = self.centered_on {
if let Some((Some(pos), _, _, _)) = self.players.get(&peer) {
self.offset = Vec2::new(ui.available_width() / 2.0, ui.available_height() / 2.0)
- Vec2::new(
pos.x as f32 * tile_size / 128.0,
(pos.y - 12) as f32 * tile_size / 128.0,
)
}
if let Some(peer) = self.centered_on
&& let Some((Some(pos), _, _, _)) = self.players.get(&peer)
{
self.offset = Vec2::new(ui.available_width() / 2.0, ui.available_height() / 2.0)
- Vec2::new(
pos.x as f32 * tile_size / 128.0,
(pos.y - 12) as f32 * tile_size / 128.0,
)
}
let painter = ui.painter();
for (coord, tex) in &self.textures {
@ -1240,15 +1248,15 @@ impl ImageMap {
Vec2::new(7.0 * tile_size / 128.0, 16.0 * tile_size / 128.0),
);
if *is_dead {
if let Some(tex) = &self.notplayer {
if let Some(id) = tex.texture_id() {
painter.image(
id,
rect,
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
Color32::WHITE,
);
}
if let Some(tex) = &self.notplayer
&& let Some(id) = tex.texture_id()
{
painter.image(
id,
rect,
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
Color32::WHITE,
);
}
} else {
painter.image(
@ -1318,6 +1326,8 @@ pub struct App {
noitalog_number: usize,
noitalog: Vec<String>,
proxylog: String,
save_paths: SavePaths,
clipboard: Option<Clipboard>,
}
fn filled_group<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
@ -1360,47 +1370,12 @@ pub struct Settings {
audio: AudioSettings,
}
fn settings_get() -> Settings {
if let Ok(s) = std::env::current_exe() {
let file = s.parent().unwrap().join("proxy.ron");
if let Ok(mut file) = File::open(file) {
let mut s = String::new();
let _ = file.read_to_string(&mut s);
ron::from_str::<Settings>(&s).unwrap_or_default()
} else {
Settings::default()
}
} else {
Settings::default()
}
}
fn settings_set(
app: AppSavedState,
color: PlayerAppearance,
modmanager: ModmanagerSettings,
audio: AudioSettings,
) {
if let Ok(s) = std::env::current_exe() {
let settings = Settings {
app,
color,
modmanager,
audio,
};
let file = s.parent().unwrap().join("proxy.ron");
let settings = ron::to_string(&settings).unwrap();
if let Ok(mut file) = File::create(file) {
file.write_all(settings.as_bytes()).unwrap();
}
}
}
impl App {
pub fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
cc.egui_ctx.set_visuals(Visuals::dark());
cc.egui_ctx.set_theme(ThemePreference::Dark);
let settings = settings_get();
let save_paths = SavePaths::new();
let settings = save_paths.load_settings();
let mut saved_state: AppSavedState = settings.app;
let modmanager_settings: ModmanagerSettings = settings.modmanager;
let appearance: PlayerAppearance = settings.color;
@ -1419,8 +1394,16 @@ impl App {
info!("Installing image loaders...");
egui_extras::install_image_loaders(&cc.egui_ctx);
let my_lobby_kind = args.override_lobby_kind.unwrap_or({
if saved_state.spacewars {
LobbyKind::Gog
} else {
LobbyKind::Steam
}
});
info!("Initializing steam state...");
let steam_state = steam_helper::SteamState::new(saved_state.spacewars);
let steam_state = steam_helper::SteamState::new(my_lobby_kind == LobbyKind::Gog);
info!("Checking if running on steam deck...");
let running_on_steamdeck = steam_state
@ -1438,11 +1421,7 @@ impl App {
cc.egui_ctx
.set_zoom_factor(args.ui_zoom_factor.unwrap_or(default_zoom_factor));
info!("Creating the app...");
let run_save_state = if let Ok(path) = std::env::current_exe() {
SaveState::new(path.parent().unwrap().join("save_state"))
} else {
SaveState::new("./save_state/".into())
};
let run_save_state = SaveState::new(&save_paths.save_state_path);
let path = player_path(modmanager_settings.mod_path());
let player_image = if path.exists() {
image::open(path)
@ -1453,13 +1432,7 @@ impl App {
RgbaImage::new(7, 17)
};
let my_lobby_kind = if saved_state.spacewars {
LobbyKind::Gog
} else {
LobbyKind::Steam
};
Self {
let mut me = Self {
state,
audio,
modmanager: Modmanager::default(),
@ -1486,19 +1459,27 @@ impl App {
noitalog_number: 0,
noitalog: Vec::new(),
proxylog: String::new(),
save_paths,
clipboard: Clipboard::new().ok(),
};
if let Some(connect_to) = me.args.auto_connect_to {
me.start_steam_connect(connect_to.code);
}
me
}
fn set_settings(&self) {
let mut audio = self.audio.clone();
audio.input_devices.clear();
audio.output_devices.clear();
settings_set(
self.app_saved_state.clone(),
self.appearance.clone(),
self.modmanager_settings.clone(),
self.save_paths.save_settings(Settings {
color: self.appearance.clone(),
app: self.app_saved_state.clone(),
modmanager: self.modmanager_settings.clone(),
audio,
)
});
}
fn get_netman_init(&self) -> NetManagerInit {
@ -1551,10 +1532,10 @@ impl App {
None
};
let mut my_nickname = self.app_saved_state.nickname.clone().or(steam_nickname);
if let Some(n) = &my_nickname {
if n.trim().is_empty() {
my_nickname = None;
}
if let Some(n) = &my_nickname
&& n.trim().is_empty()
{
my_nickname = None;
}
my_nickname.unwrap_or(default)
@ -1679,7 +1660,7 @@ impl App {
fn connect_screen(&mut self, ctx: &Context) {
egui::CentralPanel::default().show(ctx, |ui| {
if self.app_saved_state.times_started % 20 == 0 {
if self.app_saved_state.times_started.is_multiple_of(20) {
let image = egui::Image::new(egui::include_image!("../assets/longleg.png"))
.texture_options(TextureOptions::NEAREST);
image.paint_at(ui, ctx.screen_rect());
@ -1698,7 +1679,7 @@ impl App {
let (steam_connect_rect, other_rect) = right.split_top_bottom_at_fraction(0.33);
let (ip_connect_rect, info_rect) = other_rect.split_top_bottom_at_fraction(0.5);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(bottom_panel.shrink(group_shrink)),
..Default::default()
@ -1715,7 +1696,7 @@ impl App {
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(info_rect.shrink(group_shrink)),
..Default::default()
@ -1723,13 +1704,13 @@ impl App {
|ui| {
filled_group(ui, |ui| {
ui.set_min_size(ui.available_size());
heading_with_underline(ui, tr("Info"));
ui.label(tr("info_stress_tests"));
// heading_with_underline(ui, tr("Info"));
// ui.label(tr("info_stress_tests"));
});
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(right_b_panel.shrink(group_shrink)),
..Default::default()
@ -1743,7 +1724,7 @@ impl App {
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(settings_rect.shrink(group_shrink)),
..Default::default()
@ -1772,7 +1753,7 @@ impl App {
});
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(steam_connect_rect.shrink(group_shrink)),
..Default::default()
@ -1785,7 +1766,7 @@ impl App {
});
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(ip_connect_rect.shrink(group_shrink)),
..Default::default()
@ -1841,7 +1822,7 @@ impl App {
let color = game_mode.color();
ui.colored_label(
color,
tr(&format!("game_mode_{}", game_mode)),
tr(&format!("game_mode_{game_mode}")),
);
}
},
@ -1856,7 +1837,7 @@ impl App {
} else {
Color32::RED
};
ui.colored_label(color, format!("EW {}", version));
ui.colored_label(color, format!("EW {version}"));
} else if info.is_noita_online {
ui.colored_label(
Color32::LIGHT_BLUE,
@ -1939,14 +1920,17 @@ impl App {
);
ui.checkbox(&mut self.app_saved_state.allow_friends, "Allow friends");
if ui.button(tr("connect_steam_connect")).clicked() {
let id = ClipboardProvider::new()
.and_then(|mut ctx: ClipboardContext| ctx.get_contents());
let id = self.clipboard.as_mut().and_then(|c| c.get_text().ok());
match id {
Ok(id) => {
Some(id) => {
self.set_settings();
self.connect_to_steam_lobby(id);
}
Err(error) => self.notify_error(error),
None => self.notify_error(if self.clipboard.is_none() {
"no clipboard"
} else {
"clipboard failed"
}),
}
}
if ui.button(tr("Open-lobby-list")).clicked() {
@ -1964,7 +1948,7 @@ impl App {
}
}
Err(err) => {
ui.label(format!("Could not init steam networking: {}", err));
ui.label(format!("Could not init steam networking: {err}"));
}
}
}
@ -1987,11 +1971,11 @@ impl App {
let addr = addr.or(addr2);
ui.add_enabled_ui(addr.is_ok(), |ui| {
if ui.button(tr("ip_connect")).clicked() {
if let Ok(addr) = addr {
self.set_settings();
self.start_connect(addr);
}
if ui.button(tr("ip_connect")).clicked()
&& let Ok(addr) = addr
{
self.set_settings();
self.start_connect(addr);
}
});
}
@ -2055,10 +2039,7 @@ impl App {
if self.my_lobby_kind == lobby.kind {
self.start_steam_connect(lobby.code)
} else {
self.notify_error(format!(
"Mismathing modes: Host is in {:?} mode, you're in {:?} mode",
lobby.kind, self.my_lobby_kind
));
self.state = AppState::GogModeIssue(lobby);
}
}
Err(LobbyError::NotALobbyCode) => {
@ -2247,8 +2228,8 @@ impl App {
}
ui.separator();
}
if let Some(game) = self.modmanager_settings.game_exe_path.parent() {
if let Ok(s) = fs::read_to_string(game.join("logger.txt")) {
if let Some(game) = self.modmanager_settings.game_exe_path.parent()
&& let Ok(s) = fs::read_to_string(game.join("logger.txt")) {
let l = self.noitalog.len();
if l != 0 && s.len() >= self.noitalog[l - 1].len() {
if s.len() != self.noitalog[l - 1].len() {
@ -2259,7 +2240,6 @@ impl App {
self.noitalog.push(s);
}
}
}
match self.connected_menu {
ConnectedMenu::Normal => {
if netman.peer.is_steam() {
@ -2269,10 +2249,9 @@ impl App {
kind: self.my_lobby_kind,
code: id,
};
ui.output_mut(|o| {
o.commands
.push(OutputCommand::CopyText(lobby_code.serialize()))
});
if let Some(clipboard) = self.clipboard.as_mut() {
let _ = clipboard.set_text(lobby_code.serialize());
}
self.copied_lobby = true;
}
} else {
@ -2345,7 +2324,7 @@ impl App {
}
{
let mut temp = netman.no_chunkmap.load(Ordering::Relaxed);
if ui.checkbox(&mut temp, "don't save chunk map").changed() {
if ui.checkbox(&mut temp, "don't save chunk map, chunkmap is disabled by default do to current implementation ram/vram leaking on long runs").changed() {
netman.no_chunkmap.store(temp, Ordering::Relaxed);
}
}
@ -2428,11 +2407,17 @@ impl App {
}
ConnectedMenu::NoitaLog => {
if !self.noitalog.is_empty() {
let l = self.noitalog.len();
if l > 1 {
ui.add(Slider::new(&mut self.noitalog_number, 0..=l - 1));
}
let mut s = self.noitalog[self.noitalog_number].clone() + "\n";
ui.horizontal(|ui| {
let l = self.noitalog.len();
if l > 1 {
ui.add(Slider::new(&mut self.noitalog_number, 0..=l - 1));
}
if let Some(clipboard) = self.clipboard.as_mut()
&& ui.button("save to clipboard").clicked() {
let _ = clipboard.set_text(&s);
}
});
ScrollArea::vertical()
.auto_shrink([false; 2])
.stick_to_bottom(true)
@ -2451,11 +2436,10 @@ impl App {
path.parent().unwrap().join("ew_log.txt")
} else {
"ew_log.txt".into()
}) {
if s.len() > self.proxylog.len() {
})
&& s.len() > self.proxylog.len() {
self.proxylog = s
}
}
let mut s = self.proxylog.clone() + "\n";
ScrollArea::vertical()
.auto_shrink([false; 2])
@ -2624,6 +2608,31 @@ impl eframe::App for App {
self.start_connect_step_2(peer);
}
}
AppState::GogModeIssue(target_lobby) => {
let mut button_back = false;
let mut button_restart = false;
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!(
"Mismathing modes: Host is in {:?} mode, you're in {:?} mode",
target_lobby.kind, self.my_lobby_kind
));
button_restart = ui
.add(Button::new(tr("Switch-mode-and-restart")).fill(Color32::RED))
.clicked();
button_back = ui.button(tr("button_back")).clicked();
});
if button_restart {
let Err(err) = SelfRestarter::new().and_then(|mut r| {
r.override_lobby_kind(target_lobby.kind)
.connect_to(*target_lobby)
.restart()
});
self.notify_error(format!("Failed to self-restart: {err}"));
}
if button_back {
self.state = AppState::Connect;
}
}
};
}
fn on_exit(&mut self, _: Option<&eframe::glow::Context>) {
@ -2783,7 +2792,7 @@ fn cli_setup(
AudioSettings,
steamworks::LobbyType,
) {
let settings = settings_get();
let settings = SavePaths::new().load_settings();
let saved_state: AppSavedState = settings.app;
let mut mod_manager: ModmanagerSettings = settings.modmanager;
let appearance: PlayerAppearance = settings.color;
@ -2804,7 +2813,7 @@ fn cli_setup(
let run_save_state = if let Ok(path) = std::env::current_exe() {
SaveState::new(path.parent().unwrap().join("save_state"))
} else {
SaveState::new("./save_state/".into())
SaveState::new("./save_state/")
};
let player_path = player_path(mod_manager.mod_path());
let mut cosmetics = (false, false, false);
@ -2858,7 +2867,7 @@ fn cli_setup(
pub fn connect_cli(lobby: String, args: Args) {
let (state, netmaninit, kind, audio, _) = cli_setup(args);
let varient = if lobby.contains(':') {
let variant = if lobby.contains(':') {
let p = Peer::connect(lobby.parse().unwrap(), None).unwrap();
while p.my_id().is_none() {
sleep(Duration::from_millis(100))
@ -2875,14 +2884,16 @@ pub fn connect_cli(lobby: String, args: Args) {
exit(1)
};
let player_path = netmaninit.player_path.clone();
let netman = net::NetManager::new(varient, netmaninit, audio);
let netman = net::NetManager::new(variant, netmaninit, audio);
netman.start_inner(player_path, Some(kind)).unwrap();
}
pub fn host_cli(port: u16, args: Args) {
/// Bind to the provided `bind_addr` with `args` with CLI output only.
///
/// The `bind_addr` is either `Some` address/port pair to bind to, or `None` to use Steam networking.
pub fn host_cli(bind_addr: Option<SocketAddr>, args: Args) {
let (state, netmaninit, kind, audio, lobbytype) = cli_setup(args);
let varient = if port != 0 {
let bind_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
let variant = if let Some(bind_addr) = bind_addr {
let peer = Peer::host(bind_addr, None).unwrap();
PeerVariant::Tangled(peer)
} else if let Some(state) = state {
@ -2901,6 +2912,6 @@ pub fn host_cli(port: u16, args: Args) {
exit(1)
};
let player_path = netmaninit.player_path.clone();
let netman = net::NetManager::new(varient, netmaninit, audio);
let netman = net::NetManager::new(variant, netmaninit, audio);
netman.start_inner(player_path, Some(kind)).unwrap();
}

View file

@ -1,3 +1,5 @@
use std::fmt::{self};
use steamworks::LobbyId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -18,6 +20,15 @@ pub enum LobbyError {
CodeVersionMismatch,
}
impl fmt::Display for LobbyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LobbyError::NotALobbyCode => write!(f, "Not a lobby code"),
LobbyError::CodeVersionMismatch => write!(f, "Code version mismatch"),
}
}
}
impl LobbyCode {
const VERSION: char = '0';
const BASE: u64 = 0x0186000000000000;

View file

@ -9,6 +9,7 @@ use std::{
backtrace, fs,
fs::File,
io::{self, BufWriter},
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
panic,
};
use tracing::{error, info, level_filters::LevelFilter};
@ -17,6 +18,13 @@ use tracing_subscriber::EnvFilter;
#[allow(clippy::needless_return)]
#[tokio::main(worker_threads = 2)]
async fn main() {
#[cfg(target_os = "windows")]
{
use winapi::um::wincon::{ATTACH_PARENT_PROCESS, AttachConsole};
unsafe {
AttachConsole(ATTACH_PARENT_PROCESS);
}
}
let log = if let Ok(path) = std::env::current_exe() {
path.parent().unwrap().join("ew_log.txt")
} else {
@ -81,12 +89,23 @@ async fn main() {
info!("Launch command: {:?}", args.launch_cmd);
if let Some(host) = args.clone().host {
let port = if host.eq_ignore_ascii_case("steam") {
0
let bind_addr = if host.eq_ignore_ascii_case("steam") {
None
} else {
host.parse::<u16>().unwrap_or(5123)
// allows binding to both IPv6 and IPv4
host.parse::<SocketAddr>()
.ok()
// compatibility with providing only the port (which then proceeds to bind to IPv4 only)
.or_else(|| {
Some(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
host.parse().ok()?,
)))
})
.map(Some)
.expect("host argument is neither SocketAddr nor port")
};
host_cli(port, args)
host_cli(bind_addr, args)
} else if let Some(lobby) = args.clone().lobby {
connect_cli(lobby, args)
} else {

View file

@ -25,12 +25,11 @@ use std::{
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use world::{NoitaWorldUpdate, WorldManager};
use world::WorldManager;
use crate::lobby_code::LobbyKind;
use crate::mod_manager::{ModmanagerSettings, get_mods};
use crate::net::world::world_model::chunk::{Pixel, PixelFlags};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::net::world::world_model::ChunkData;
use crate::player_cosmetics::{PlayerPngDesc, create_player_png, get_player_skin};
use crate::steam_helper::LobbyExtraData;
use crate::{
@ -38,6 +37,7 @@ use crate::{
bookkeeping::save_state::{SaveState, SaveStateEntry},
};
use shared::des::ProxyToDes;
use shared::world_sync::{ChunkCoord, Pixel, PixelFlags, ProxyToWorldSync};
use tangled::Reliability;
use tracing::{error, info, warn};
mod audio;
@ -50,7 +50,7 @@ pub mod world;
pub(crate) fn ws_encode_proxy(key: &'static str, value: impl Display) -> NoitaInbound {
let mut buf = Vec::new();
buf.push(2);
write!(buf, "{} {}", key, value).unwrap();
write!(buf, "{key} {value}").unwrap();
NoitaInbound::RawMessage(buf)
}
@ -91,13 +91,13 @@ pub(crate) struct NetInnerState {
impl NetInnerState {
pub(crate) fn try_ms_write(&mut self, data: &NoitaInbound) {
if let Some(ws) = &mut self.ms {
if let Err(err) = ws.write(data) {
error!("Error occured while sending to websocket: {}", err);
self.ms = None;
self.had_a_disconnect = true;
};
}
if let Some(ws) = &mut self.ms
&& let Err(err) = ws.write(data)
{
error!("Error occured while sending to websocket: {}", err);
self.ms = None;
self.had_a_disconnect = true;
};
}
pub(crate) fn try_ws_write_option(&mut self, key: &str, value: impl ProxyOpt) {
let mut buf = Vec::new();
@ -252,8 +252,8 @@ impl NetManager {
chunk_map: Default::default(),
players_sprite: Default::default(),
reset_map: AtomicBool::new(false),
no_chunkmap_to_players: AtomicBool::new(false),
no_chunkmap: AtomicBool::new(false),
no_chunkmap_to_players: AtomicBool::new(true),
no_chunkmap: AtomicBool::new(true),
colors: Default::default(),
}
.into()
@ -428,12 +428,12 @@ impl NetManager {
);
while self.continue_running.load(Ordering::Relaxed) {
if let Some(k) = kind {
if let Some(n) = self.peer.lobby_id() {
let c = crate::lobby_code::LobbyCode { kind: k, code: n };
info!("Lobby ID: {}", c.serialize());
kind = None
}
if let Some(k) = kind
&& let Some(n) = self.peer.lobby_id()
{
let c = crate::lobby_code::LobbyCode { kind: k, code: n };
info!("Lobby ID: {}", c.serialize());
kind = None
}
if self.end_run.load(Ordering::Relaxed) {
for id in self.peer.iter_peer_ids() {
@ -459,10 +459,10 @@ impl NetManager {
}
}
}
if let Some(ws) = &mut state.ms {
if let Err(err) = ws.flush() {
warn!("Websocket flush not ok: {err}");
}
if let Some(ws) = &mut state.ms
&& let Err(err) = ws.flush()
{
warn!("Websocket flush not ok: {err}");
}
let mut to_kick = self.kick_list.lock().unwrap();
let mut dont_kick = self.dont_kick.lock().unwrap();
@ -535,11 +535,11 @@ impl NetManager {
for msg in state.world.get_emitted_msgs() {
self.do_message_request(msg)
}
state.world.update();
let updates = state.world.get_noita_updates();
for update in updates {
state.try_ms_write(&ws_encode_proxy_bin(0, &update));
let updates = state.world.update();
if !updates.is_empty() {
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(ProxyToWorldSync::Updates(
updates,
)));
}
if state.had_a_disconnect {
@ -718,6 +718,10 @@ impl NetManager {
sendm: &Sender<FxHashMap<u16, u32>>,
) {
match net_msg {
NetMsg::ForwardWorldSyncToProxy(msg) => state.world.handle_noita_msg(src, msg),
NetMsg::ForwardProxyToWorldSync(msg) => {
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(msg));
}
NetMsg::AudioData(data, global, tx, ty, vol) => {
if !self.is_cess.load(Ordering::Relaxed) {
let audio = self.audio.lock().unwrap().clone();
@ -808,19 +812,19 @@ impl NetManager {
.lock()
.unwrap()
.insert(src, get_player_skin(player_image.clone(), rgb));
if let Some(id) = pong {
if id != self.peer.my_id() {
self.send(
id,
&NetMsg::PlayerColor(
self.init_settings.player_png_desc,
self.is_host(),
None,
self.init_settings.my_nickname.clone(),
),
Reliability::Reliable,
);
}
if let Some(id) = pong
&& id != self.peer.my_id()
{
self.send(
id,
&NetMsg::PlayerColor(
self.init_settings.player_png_desc,
self.is_host(),
None,
self.init_settings.my_nickname.clone(),
),
Reliability::Reliable,
);
}
}
NetMsg::Kick => self.back_out.store(true, Ordering::Relaxed),
@ -889,10 +893,10 @@ impl NetManager {
}
}
NetMsg::RespondFlagNormal(flag, new) => {
state.try_ms_write(&ws_encode_proxy("normal_flag", format!("{} {}", flag, new)));
state.try_ms_write(&ws_encode_proxy("normal_flag", format!("{flag} {new}")));
}
NetMsg::RespondFlagSlow(ent, new) => {
state.try_ms_write(&ws_encode_proxy("slow_flag", format!("{} {}", ent, new)));
state.try_ms_write(&ws_encode_proxy("slow_flag", format!("{ent} {new}")));
}
NetMsg::RespondFlagMoon(x, y, b) => {
state.try_ms_write(&ws_encode_proxy(
@ -1144,8 +1148,6 @@ impl NetManager {
},
);
}
// Binary message to proxy
3 => self.handle_bin_message_to_proxy(&raw_msg[1..], state),
0 => {
let flags = String::from_utf8_lossy(&raw_msg[1..]).into();
let msg = NetMsg::Flags(flags);
@ -1167,6 +1169,19 @@ impl NetManager {
);
}
}
NoitaOutbound::WorldSyncToProxy(world_sync_msg) => {
if self.is_host() {
state
.world
.handle_noita_msg(self.peer.my_id(), world_sync_msg)
} else {
self.send(
self.peer.host_id(),
&NetMsg::ForwardWorldSyncToProxy(world_sync_msg),
Reliability::Reliable,
);
}
}
NoitaOutbound::RemoteMessage {
reliable,
destination,
@ -1432,29 +1447,6 @@ impl NetManager {
}
}
fn handle_bin_message_to_proxy(&self, msg: &[u8], state: &mut NetInnerState) {
let key = msg[0];
let data = &msg[1..];
match key {
// world frame
0 => {
let update = NoitaWorldUpdate::load(data);
state.world.add_update(update);
}
// world end
1 => {
let pos = data[1..]
.split(|b| *b == b':')
.map(|s| String::from_utf8_lossy(s).parse::<i32>().unwrap_or(0))
.collect::<Vec<i32>>();
state.world.add_end(data[0], &pos);
}
key => {
error!("Unknown bin msg from mod: {:?}", key)
}
}
}
fn end_run(&self, state: &mut NetInnerState) {
self.init_settings.save_state.reset();
{
@ -1468,7 +1460,7 @@ impl NetManager {
.modmanager_settings
.get_progress()
.unwrap_or_default();
if settings.world_num == u16::MAX {
if settings.world_num == u8::MAX {
settings.world_num = 0
} else {
settings.world_num += 1
@ -1517,10 +1509,7 @@ impl ExplosionData {
ray,
hole,
liquid,
mat: Pixel {
flags: PixelFlags::Normal,
material: mat,
},
mat: Pixel::new(mat, PixelFlags::Normal),
prob,
}
}

View file

@ -3,7 +3,7 @@ use crate::net::omni::OmniPeerId;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use opus::{Application, Channels, Decoder, Encoder};
use rodio::buffer::SamplesBuffer;
use rodio::{OutputStream, OutputStreamHandle, Sink};
use rodio::{OutputStream, OutputStreamBuilder, Sink};
use rubato::{FftFixedIn, Resampler};
use std::collections::HashMap;
use std::ops::Mul;
@ -93,7 +93,7 @@ struct PlayerInfo {
pub(crate) struct AudioManager {
per_player: HashMap<OmniPeerId, PlayerInfo>,
stream_handle: Option<(OutputStream, OutputStreamHandle)>,
stream_handle: Option<OutputStream>,
decoder: Decoder,
rx: Receiver<Vec<u8>>,
}
@ -163,10 +163,9 @@ impl AudioManager {
if let Ok(len) = encoder.encode_float(
&resamp.process(&[&extra[..FRAME_SIZE]], None).unwrap()[0],
&mut compressed,
) {
if len != 0 {
v.push(compressed[..len].to_vec())
}
) && len != 0
{
v.push(compressed[..len].to_vec())
}
extra.drain(..FRAME_SIZE);
}
@ -206,30 +205,7 @@ impl AudioManager {
warn!("input device not found")
}
});
let stream_handle: Option<(OutputStream, OutputStreamHandle)> = {
let output = audio.output_device.clone();
if audio.disabled {
None
} else if output.is_none() {
host.default_output_device()
} else if let Some(d) = host
.output_devices()
.map(|mut d| d.find(|d| d.name().ok() == output))
.ok()
.flatten()
{
Some(d)
} else {
host.default_output_device()
}
}
.and_then(|device| {
device
.default_output_config()
.map(|config| OutputStream::try_from_device_config(&device, config).ok())
.ok()
.flatten()
});
let stream_handle: Option<OutputStream> = OutputStreamBuilder::open_default_stream().ok();
let sink: HashMap<OmniPeerId, PlayerInfo> = Default::default();
Self {
decoder,
@ -254,18 +230,16 @@ impl AudioManager {
sound_pos: (i32, i32),
iv: f32,
) {
if let std::collections::hash_map::Entry::Vacant(e) = self.per_player.entry(src) {
if let Some(stream_handle) = &self.stream_handle {
if let Ok(s) = Sink::try_new(&stream_handle.1) {
//let (pitch_control, dsp) = make_dsp();
e.insert(PlayerInfo {
sink: s,
//tracker: VelocityTracker::default(),
//pitch_control,
//dsp,
});
}
}
if let std::collections::hash_map::Entry::Vacant(e) = self.per_player.entry(src)
&& let Some(stream_handle) = &self.stream_handle
{
//let (pitch_control, dsp) = make_dsp();
e.insert(PlayerInfo {
sink: Sink::connect_new(stream_handle.mixer()),
//tracker: VelocityTracker::default(),
//pitch_control,
//dsp,
});
}
self.per_player.entry(src).and_modify(|player_info| {
/*player_info
@ -294,10 +268,10 @@ impl AudioManager {
let mut dec: Vec<f32> = Vec::new();
for data in data {
let mut out = vec![0f32; FRAME_SIZE];
if let Ok(len) = self.decoder.decode_float(&data, &mut out, false) {
if len != 0 {
dec.extend(&out[..len])
}
if let Ok(len) = self.decoder.decode_float(&data, &mut out, false)
&& len != 0
{
dec.extend(&out[..len])
}
}
if !dec.is_empty() {

View file

@ -1,9 +1,9 @@
use super::{omni::OmniPeerId, world::WorldNetMessage};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::net::world::world_model::ChunkData;
use crate::{GameSettings, player_cosmetics::PlayerPngDesc};
use bitcode::{Decode, Encode};
use rustc_hash::FxHashMap;
use shared::world_sync::ChunkCoord;
pub(crate) type Destination = shared::Destination<OmniPeerId>;
pub(crate) struct MessageRequest<T> {
@ -27,7 +27,9 @@ pub(crate) enum NetMsg {
PlayerColor(PlayerPngDesc, bool, Option<OmniPeerId>, String),
RemoteMsg(shared::RemoteMessage),
ForwardDesToProxy(shared::des::DesToProxy),
ForwardWorldSyncToProxy(shared::world_sync::WorldSyncToProxy),
ForwardProxyToDes(shared::des::ProxyToDes),
ForwardProxyToWorldSync(shared::world_sync::ProxyToWorldSync),
NoitaDisconnected,
Flags(String),
RespondFlagNormal(String, bool),

View file

@ -20,19 +20,19 @@ impl ProxyOpt for bool {
impl ProxyOpt for u32 {
fn write_opt(self, buf: &mut Vec<u8>, key: &str) {
write!(buf, "proxy_opt_num {} {}", key, self).unwrap();
write!(buf, "proxy_opt_num {key} {self}").unwrap();
}
}
impl ProxyOpt for f32 {
fn write_opt(self, buf: &mut Vec<u8>, key: &str) {
write!(buf, "proxy_opt_num {} {}", key, self).unwrap();
write!(buf, "proxy_opt_num {key} {self}").unwrap();
}
}
impl ProxyOpt for &str {
fn write_opt(self, buf: &mut Vec<u8>, key: &str) {
write!(buf, "proxy_opt {} {}", key, self).unwrap();
write!(buf, "proxy_opt {key} {self}").unwrap();
}
}
@ -40,10 +40,10 @@ impl ProxyOpt for GameMode {
fn write_opt(self, buf: &mut Vec<u8>, key: &str) {
match self {
GameMode::SharedHealth => {
write!(buf, "proxy_opt {} shared_health", key).unwrap();
write!(buf, "proxy_opt {key} shared_health").unwrap();
}
GameMode::LocalHealth(_) => {
write!(buf, "proxy_opt {} local_health", key).unwrap();
write!(buf, "proxy_opt {key} local_health").unwrap();
}
}
}

View file

@ -2,7 +2,7 @@ use std::{fmt::Display, mem, sync::Mutex};
use crossbeam::channel;
use dashmap::DashMap;
use fluent_bundle::FluentValue;
use fluent_templates::fluent_bundle::FluentValue;
use steamworks::{
CallbackHandle, ClientManager, LobbyChatUpdate, LobbyId, LobbyType, SteamError, SteamId,
networking_sockets::{ListenSocket, NetPollGroup},
@ -46,7 +46,7 @@ impl Display for ConnectError {
),
],
);
write!(f, "{}", translated)
write!(f, "{translated}")
}
ConnectError::VersionMissing => write!(f, "{}", tr("error_missing_version_field")),
ConnectError::LobbyDoesNotExist => write!(f, "{}", tr("error_lobby_does_not_exist")),
@ -148,10 +148,10 @@ impl Connections {
}
fn flush(&self) {
for i in &self.peers {
if let Some(c) = i.connection() {
if let Err(err) = c.flush_messages() {
warn!("Error while flushing a message: {err:?}")
}
if let Some(c) = i.connection()
&& let Err(err) = c.flush_messages()
{
warn!("Error while flushing a message: {err:?}")
}
}
}

View file

@ -4,21 +4,15 @@ use rand::{Rng, rng};
use rayon::iter::IntoParallelIterator;
use rayon::iter::ParallelIterator;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::f32::consts::TAU;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::time::Duration;
use std::{cmp, env, mem, thread};
use std::{cmp, mem, thread};
use tracing::{debug, info, warn};
use wide::f32x8;
use world_model::{
CHUNK_SIZE, ChunkCoord, ChunkData, ChunkDelta, WorldModel,
chunk::{Chunk, Pixel},
};
pub use world_model::encoding::NoitaWorldUpdate;
use world_model::{ChunkData, ChunkDelta, WorldModel, chunk::Chunk};
use crate::bookkeeping::save_state::{SaveState, SaveStateEntry};
@ -30,12 +24,6 @@ use super::{
pub mod world_model;
#[derive(Debug, Serialize, Deserialize)]
pub enum WorldUpdateKind {
Update(NoitaWorldUpdate),
End,
}
#[derive(Debug, Decode, Encode, Clone)]
pub(crate) enum WorldNetMessage {
// Authority request
@ -75,13 +63,13 @@ pub(crate) enum WorldNetMessage {
RelinquishAuthority {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
world_num: u8,
},
// Ttell how to update a chunk storage
// Tell how to update a chunk storage
UpdateStorage {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
chunk_data: ChunkData,
world_num: u8,
priority: Option<u8>,
},
// When listening
@ -200,7 +188,7 @@ pub(crate) struct WorldManager {
chunk_last_update: FxHashMap<ChunkCoord, u64>,
/// Stores last priority we used for that chunk, in case transfer fails and we'll need to request authority normally.
last_request_priority: FxHashMap<ChunkCoord, u8>,
world_num: i32,
world_num: u8,
pub materials: FxHashMap<u16, (u32, u32, CellType, u32)>,
is_storage_recent: FxHashSet<ChunkCoord>,
explosion_pointer: FxHashMap<ChunkCoord, Vec<usize>>,
@ -330,66 +318,22 @@ impl WorldManager {
self.chunk_storage.clone()
}
pub(crate) fn add_update(&mut self, update: NoitaWorldUpdate) {
self.outbound_model
.apply_noita_update(&update, &mut self.is_storage_recent);
}
pub(crate) fn add_end(&mut self, priority: u8, pos: &[i32]) {
let updated_chunks = self
.outbound_model
.updated_chunks()
.iter()
.copied()
.collect::<Vec<_>>();
self.current_update += 1;
let chunks_to_send: Vec<Vec<(OmniPeerId, u8)>> = updated_chunks
.iter()
.map(|chunk| self.chunk_updated_locally(*chunk, priority, pos))
.collect();
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
for (chunk, who_sending) in updated_chunks.iter().zip(chunks_to_send.iter()) {
let Some(delta) = self.outbound_model.get_chunk_delta(*chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(*peer)
.or_default()
.push((delta.clone(), *pri));
}
}
let mut emit_queue = Vec::new();
for (peer, chunkpacket) in chunk_packet {
emit_queue.push((
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
));
}
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
fn chunk_updated_locally(
&mut self,
chunk: ChunkCoord,
priority: u8,
pos: &[i32],
) -> Vec<(OmniPeerId, u8)> {
if pos.len() == 6 {
self.my_pos = (pos[0], pos[1]);
self.cam_pos = (pos[2], pos[3]);
self.is_notplayer = pos[4] == 1;
if self.world_num != pos[5] {
self.world_num = pos[5];
fn update_world_info(&mut self, pos: Option<(i32, i32, i32, i32, bool)>, world_num: u8) {
if let Some((px, py, cx, cy, is_not)) = pos {
self.my_pos = (px, py);
self.cam_pos = (cx, cy);
self.is_notplayer = is_not;
if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
} else if self.world_num != pos[0] {
self.world_num = pos[0];
} else if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
}
fn chunk_updated_locally(&mut self, chunk: ChunkCoord, priority: u8) -> Vec<(OmniPeerId, u8)> {
let entry = self.chunk_state.entry(chunk).or_insert_with(|| {
debug!("Created entry for {chunk:?}");
ChunkState::RequestAuthority {
@ -453,9 +397,6 @@ impl WorldManager {
new_authority,
stop_sending,
} => {
let Some(delta) = self.outbound_model.get_chunk_delta(chunk, false) else {
return Vec::new();
};
if *pri != priority {
*pri = priority;
emit_queue.push((
@ -482,6 +423,11 @@ impl WorldManager {
new_auth_got = true
}
if take_auth {
let Some(delta) = self.outbound_model.get_chunk_delta(chunk, false)
else {
return Vec::new();
};
emit_queue.retain(|(a, _)| matches!(a, Destination::Host));
emit_queue.push((
Destination::Peer(listener),
WorldNetMessage::ListenUpdate {
@ -490,7 +436,8 @@ impl WorldManager {
take_auth,
},
));
chunks_to_send = Vec::new()
chunks_to_send = Vec::new();
break;
} else {
chunks_to_send.push((listener, priority));
}
@ -508,7 +455,7 @@ impl WorldManager {
chunks_to_send
}
pub(crate) fn update(&mut self) {
pub(crate) fn update(&mut self) -> Vec<NoitaWorldUpdate> {
fn should_kill(
my_pos: (i32, i32),
cam_pos: (i32, i32),
@ -638,18 +585,11 @@ impl WorldManager {
}
retain
});
self.get_noita_updates()
}
pub(crate) fn get_noita_updates(&mut self) -> Vec<Vec<u8>> {
// Sends random data to noita to check if it crashes.
if env::var_os("NP_WORLD_SYNC_TEST").is_some() && self.current_update % 10 == 0 {
let chunk_data = ChunkData::make_random();
self.inbound_model
.apply_chunk_data(ChunkCoord(0, 0), &chunk_data)
}
let updates = self.inbound_model.get_all_noita_updates();
self.inbound_model.reset_change_tracking();
updates
pub(crate) fn get_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
self.inbound_model.get_all_noita_updates()
}
pub(crate) fn reset(&mut self) {
@ -772,15 +712,19 @@ impl WorldManager {
}
}
}
WorldNetMessage::GetChunk { chunk, priority } => self.emit_msg(
Destination::Host,
WorldNetMessage::UpdateStorage {
chunk,
chunk_data: self.outbound_model.get_chunk_data(chunk),
world_num: self.world_num,
priority: Some(priority),
},
),
WorldNetMessage::GetChunk { chunk, priority } => {
if let Some(chunk_data) = self.outbound_model.get_chunk_data(chunk) {
self.emit_msg(
Destination::Host,
WorldNetMessage::UpdateStorage {
chunk,
chunk_data,
world_num: self.world_num,
priority: Some(priority),
},
)
}
}
WorldNetMessage::AskForAuthority { chunk, priority } => {
self.emit_msg(
Destination::Host,
@ -846,12 +790,12 @@ impl WorldManager {
if let Some(chunk_data) = chunk_data {
self.inbound_model.apply_chunk_data(chunk, &chunk_data);
self.outbound_model.apply_chunk_data(chunk, &chunk_data);
} else {
} else if let Some(chunk_data) = self.outbound_model.get_chunk_data(chunk) {
self.emit_msg(
Destination::Host,
WorldNetMessage::UpdateStorage {
chunk,
chunk_data: self.outbound_model.get_chunk_data(chunk),
chunk_data,
world_num: self.world_num,
priority: None,
},
@ -871,15 +815,11 @@ impl WorldManager {
if world_num != self.world_num {
return;
}
if let Some(chunk_data) = chunk_data {
let _ = self.tx.send((chunk, chunk_data.clone()));
self.chunk_storage.insert(chunk, chunk_data);
if let Some(p) = priority {
self.cut_through_world_explosion_chunk(chunk);
self.emit_got_authority(chunk, source, p)
}
} else if priority.is_some() {
warn!("{} sent give auth without chunk", source)
let _ = self.tx.send((chunk, chunk_data.clone()));
self.chunk_storage.insert(chunk, chunk_data);
if let Some(p) = priority {
self.cut_through_world_explosion_chunk(chunk);
self.emit_got_authority(chunk, source, p)
}
}
WorldNetMessage::RelinquishAuthority {
@ -894,13 +834,13 @@ impl WorldManager {
if world_num != self.world_num {
return;
}
if let Some(state) = self.authority_map.get(&chunk) {
if state.0 != source {
debug!(
"{source} sent RelinquishAuthority for {chunk:?}, but isn't currently an authority"
);
return;
}
if let Some(state) = self.authority_map.get(&chunk)
&& state.0 != source
{
debug!(
"{source} sent RelinquishAuthority for {chunk:?}, but isn't currently an authority"
);
return;
}
self.authority_map.remove(&chunk);
if let Some(chunk_data) = chunk_data {
@ -1100,16 +1040,17 @@ impl WorldManager {
},
);
self.chunk_state.insert(chunk, ChunkState::UnloadPending);
let chunk_data = self.outbound_model.get_chunk_data(chunk);
self.emit_msg(
Destination::Host,
WorldNetMessage::UpdateStorage {
chunk,
chunk_data,
world_num: self.world_num,
priority: None,
},
);
if let Some(chunk_data) = self.outbound_model.get_chunk_data(chunk) {
self.emit_msg(
Destination::Host,
WorldNetMessage::UpdateStorage {
chunk,
chunk_data,
world_num: self.world_num,
priority: None,
},
);
}
} else {
self.emit_msg(
Destination::Peer(source),
@ -1242,10 +1183,7 @@ impl WorldManager {
let start = x - radius;
let end = x + radius;
let air_pixel = Pixel {
flags: PixelFlags::Normal,
material: 0,
};
let air_pixel = Pixel::default();
let chunk_storage: Vec<(ChunkCoord, ChunkData)> = self
.chunk_storage
.clone()
@ -1327,10 +1265,7 @@ impl WorldManager {
let dm2 = ((dmx.unsigned_abs() as u64 * dmx.unsigned_abs() as u64
+ dmy.unsigned_abs() as u64 * dmy.unsigned_abs() as u64) as f64)
.recip();
let air_pixel = Pixel {
flags: PixelFlags::Normal,
material: 0,
};
let air_pixel = Pixel::default();
let close_check = max_cx == min_cx || max_cy == min_cy;
let iter_check = [
(x + r, y),
@ -1430,10 +1365,10 @@ impl WorldManager {
if dx * dx + dy * dy <= r {
let px = icy as usize * CHUNK_SIZE + icx as usize;
if (no_info
|| chunk.pixel(px).flags == PixelFlags::Unknown
|| chunk.pixel(px).flags() == PixelFlags::Unknown
|| self
.materials
.get(&chunk.pixel(px).material)
.get(&chunk.pixel(px).mat())
.map(|(_, _, cell, _)| cell.can_remove(true, false))
.unwrap_or(true))
&& (chance == 100
@ -1478,10 +1413,7 @@ impl WorldManager {
(y - r).div_euclid(CHUNK_SIZE as i32),
(y + r).div_euclid(CHUNK_SIZE as i32),
);
let air_pixel = Pixel {
flags: PixelFlags::Normal,
material: mat.unwrap_or(0),
};
let air_pixel = Pixel::default();
let (chunkx, chunky) = (
x.div_euclid(CHUNK_SIZE as i32),
y.div_euclid(CHUNK_SIZE as i32),
@ -1535,10 +1467,10 @@ impl WorldManager {
if dd + dy * dy <= rs {
let px = icy as usize * CHUNK_SIZE + icx as usize;
if (no_info
|| chunk.pixel(px).flags == PixelFlags::Unknown
|| chunk.pixel(px).flags() == PixelFlags::Unknown
|| self
.materials
.get(&chunk.pixel(px).material)
.get(&chunk.pixel(px).mat())
.map(|(_, _, cell, _)| cell.can_remove(true, false))
.unwrap_or(true))
&& (chance == 100
@ -1641,7 +1573,7 @@ impl WorldManager {
let icy = y.rem_euclid(CHUNK_SIZE as i32);
let px = icy as usize * CHUNK_SIZE + icx as usize;
let pixel = working_chunk.pixel(px);
if let Some(stats) = self.materials.get(&pixel.material) {
if let Some(stats) = self.materials.get(&pixel.mat()) {
let h = (stats.1 as f64 * mult as f64) as u64;
if stats.0 > d || ray < h {
return (last_coord, 0, None);
@ -1743,32 +1675,28 @@ impl WorldManager {
self.is_storage_recent.insert(entry.0);
}
}
if let Some((coord, rays)) = entry.unloaded {
if self.nice_terraforming {
exists = true;
let lst = rays
.iter()
.filter_map(|i| {
if raydata[*i] == 0 {
None
} else if let Some(n) = data.get(i) {
Some(*n)
} else {
let n = self.explosion_data.len();
self.explosion_data.push((
m,
*i,
ExTarget::Ray(raydata[*i]),
0,
));
data.insert(*i, n);
Some(n)
}
})
.collect::<Vec<usize>>();
if !lst.is_empty() {
self.explosion_pointer.entry(coord).or_default().extend(lst)
}
if let Some((coord, rays)) = entry.unloaded
&& self.nice_terraforming
{
exists = true;
let lst = rays
.iter()
.filter_map(|i| {
if raydata[*i] == 0 {
None
} else if let Some(n) = data.get(i) {
Some(*n)
} else {
let n = self.explosion_data.len();
self.explosion_data
.push((m, *i, ExTarget::Ray(raydata[*i]), 0));
data.insert(*i, n);
Some(n)
}
})
.collect::<Vec<usize>>();
if !lst.is_empty() {
self.explosion_pointer.entry(coord).or_default().extend(lst)
}
}
}
@ -1805,10 +1733,7 @@ impl WorldManager {
(y - r as i32).div_euclid(CHUNK_SIZE as i32),
(y + r as i32).div_euclid(CHUNK_SIZE as i32),
);
let air_pixel = Pixel {
flags: PixelFlags::Normal,
material: 0,
};
let air_pixel = Pixel::default();
let (chunkx, chunky) = (
x.div_euclid(CHUNK_SIZE as i32),
y.div_euclid(CHUNK_SIZE as i32),
@ -1952,7 +1877,7 @@ impl WorldManager {
} {
if self
.materials
.get(&chunk.pixel(px).material)
.get(&chunk.pixel(px).mat())
.map(|(dur, _, cell, _)| *dur <= d && cell.can_remove(hole, liquid))
.unwrap_or(true)
{
@ -2111,10 +2036,7 @@ impl WorldManager {
grouped.entry(key).or_default().push((a, b));
}
let data: Vec<(usize, Vec<(usize, u64)>)> = grouped.into_iter().collect();
let air_pixel = Pixel {
flags: PixelFlags::Normal,
material: 0,
};
let air_pixel = Pixel::default();
let mut chunk = Chunk::default();
let mut chunk_delta = Chunk::default();
self.chunk_storage.get(&coord)?.apply_to_chunk(&mut chunk);
@ -2173,7 +2095,7 @@ impl WorldManager {
data.iter().any(|(i, r)| j == *i && dd <= *r)
}) && self
.materials
.get(&chunk.pixel(px).material)
.get(&chunk.pixel(px).mat())
.map(|(dur, _, cell, _)| *dur <= d && cell.can_remove(hole, liquid))
.unwrap_or(true)
{
@ -2267,7 +2189,7 @@ impl WorldManager {
let icy = y.rem_euclid(CHUNK_SIZE as i32);
let px = icy as usize * CHUNK_SIZE + icx as usize;
let pixel = working_chunk.pixel(px);
if let Some(stats) = self.materials.get(&pixel.material) {
if let Some(stats) = self.materials.get(&pixel.mat()) {
let h = (stats.1 as f64 * mult as f64) as u64;
avg += h;
count2 += 1;
@ -2367,7 +2289,7 @@ impl WorldManager {
}
let p = icy as usize * CHUNK_SIZE + icx as usize;
*px = image::Luma([
((working_chunk.pixel(p).material * 255) as usize / self.materials.len()) as u8
((working_chunk.pixel(p).mat() * 255) as usize / self.materials.len()) as u8
])
}
}
@ -2382,14 +2304,14 @@ fn create_image(chunk: ChunkData, materials: &FxHashMap<u16, u32>) -> RgbaImage
let y = i / w as usize;
let p = y * CHUNK_SIZE + x;
let m = working_chunk.pixel(p);
if m.flags != PixelFlags::Unknown {
if let Some(c) = materials.get(&m.material) {
let a = (c >> 24) & 0xFFu32;
let r = (c >> 16) & 0xFFu32;
let g = (c >> 8) & 0xFFu32;
let b = c & 0xFF;
*px = image::Rgba([r as u8, g as u8, b as u8, a as u8])
}
if m.flags() != PixelFlags::Unknown
&& let Some(c) = materials.get(&m.mat())
{
let a = (c >> 24) & 0xFFu32;
let r = (c >> 16) & 0xFFu32;
let g = (c >> 8) & 0xFFu32;
let b = c & 0xFF;
*px = image::Rgba([r as u8, g as u8, b as u8, a as u8])
}
}
image
@ -3136,11 +3058,13 @@ fn test_explosion_img_big_many() {
}*/
#[cfg(test)]
use crate::net::LiquidType;
use crate::net::world::world_model::chunk::PixelFlags;
#[cfg(test)]
use rand::seq::SliceRandom;
#[cfg(test)]
use serial_test::serial;
use shared::world_sync::{
CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, Pixel, PixelFlags, WorldSyncToProxy,
};
#[cfg(test)]
#[test]
#[serial]
@ -3151,11 +3075,8 @@ fn test_explosion_perf() {
let mut total = 0;
let iters = 64;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3203,11 +3124,8 @@ fn test_explosion_perf_unloaded() {
let iters = 4;
let mut n = 0;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3271,11 +3189,8 @@ fn test_explosion_perf_large() {
let mut total = 0;
let iters = 16;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3322,11 +3237,8 @@ fn test_line_perf() {
let mut total = 0;
let iters = 64;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3361,11 +3273,8 @@ fn test_circle_perf() {
let mut total = 0;
let iters = 64;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3400,11 +3309,8 @@ fn test_cut_perf() {
let mut total = 0;
let iters = 64;
for _ in 0..iters {
let (mut world, _, _, _, _) = WorldManager::new(
true,
OmniPeerId(0),
SaveState::new("/tmp/ew_tmp_save".parse().unwrap()),
);
let (mut world, _, _, _, _) =
WorldManager::new(true, OmniPeerId(0), SaveState::new("/tmp/ew_tmp_save"));
world
.materials
.insert(0, (0, 100, CellType::Liquid(LiquidType::Liquid), 0));
@ -3428,3 +3334,46 @@ fn test_cut_perf() {
}
println!("total micros: {}", total / iters);
}
impl WorldManager {
pub fn handle_noita_msg(&mut self, _: OmniPeerId, msg: WorldSyncToProxy) {
match msg {
WorldSyncToProxy::Updates(updates) => {
for update in updates {
self.outbound_model
.apply_noita_update(update, &mut self.is_storage_recent)
}
}
WorldSyncToProxy::End(pos, priority, world_num) => {
let updated_chunks = self.outbound_model.updated_chunks().clone();
self.current_update += 1;
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
self.update_world_info(pos, world_num);
for chunk in updated_chunks {
// who sending may be better to be after the let some
// but im too lazy to figure out for sure
let who_sending = self.chunk_updated_locally(chunk, priority);
let Some(delta) = self.outbound_model.get_chunk_delta(chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(peer)
.or_default()
.push((delta.clone(), pri));
}
}
let emit_queue = chunk_packet.into_iter().map(|(peer, chunkpacket)| {
(
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
)
});
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
}
}
}

View file

@ -1,20 +1,14 @@
use std::num::NonZeroU16;
use std::sync::Arc;
use bitcode::{Decode, Encode};
use chunk::{Chunk, CompactPixel, Pixel, PixelFlags};
use encoding::{NoitaWorldUpdate, PixelRun, PixelRunner};
use chunk::Chunk;
use encoding::PixelRunner;
use rustc_hash::{FxHashMap, FxHashSet};
use shared::world_sync::{CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, Pixel, PixelRun};
use tracing::info;
pub(crate) mod chunk;
pub mod encoding;
pub(crate) const CHUNK_SIZE: usize = 128;
#[derive(Debug, Encode, Decode, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ChunkCoord(pub i32, pub i32);
#[derive(Default)]
pub(crate) struct WorldModel {
chunks: FxHashMap<ChunkCoord, Chunk>,
@ -27,18 +21,18 @@ pub(crate) struct WorldModel {
/// Kinda close to ChunkDelta, but doesn't assume we know anything about the chunk.
#[derive(Debug, Encode, Decode, Clone)]
pub(crate) struct ChunkData {
pub runs: Vec<PixelRun<CompactPixel>>,
pub runs: Vec<PixelRun<Pixel>>,
}
/// Contains a diff, only pixels that were updated, for a given chunk.
#[derive(Debug, Encode, Decode, Clone)]
pub(crate) struct ChunkDelta {
pub chunk_coord: ChunkCoord,
runs: Arc<Vec<PixelRun<Option<CompactPixel>>>>,
runs: Arc<Vec<PixelRun<Option<Pixel>>>>,
}
impl ChunkData {
pub(crate) fn make_random() -> Self {
/*pub(crate) fn make_random() -> Self {
let mut runner = PixelRunner::new();
for i in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel(
@ -51,32 +45,26 @@ impl ChunkData {
}
let runs = runner.build();
ChunkData { runs }
}
}*/
#[cfg(test)]
pub(crate) fn new(mat: u16) -> Self {
let mut runner = PixelRunner::new();
for _ in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel(
Pixel {
flags: PixelFlags::Normal,
material: mat,
}
.to_compact(),
)
runner.put_pixel(Pixel::new(mat, shared::world_sync::PixelFlags::Normal))
}
let runs = runner.build();
ChunkData { runs }
}
pub(crate) fn apply_to_chunk(&self, chunk: &mut Chunk) {
let nil = CompactPixel(NonZeroU16::new(4095).unwrap());
let nil = Pixel::NIL;
let mut offset = 0;
for run in &self.runs {
let pixel = run.data;
if pixel != nil {
for _ in 0..run.length {
chunk.set_compact_pixel(offset, pixel);
chunk.set_pixel(offset, pixel);
offset += 1;
}
} else {
@ -85,14 +73,14 @@ impl ChunkData {
}
}
pub(crate) fn apply_delta(&mut self, delta: ChunkData) {
let nil = CompactPixel(NonZeroU16::new(4095).unwrap());
let nil = Pixel::NIL;
let mut chunk = Chunk::default();
self.apply_to_chunk(&mut chunk);
let mut offset = 0;
for run in delta.runs.iter() {
if run.data != nil {
for _ in 0..run.length {
chunk.set_compact_pixel(offset, run.data);
chunk.set_pixel(offset, run.data);
offset += 1;
}
} else {
@ -104,13 +92,10 @@ impl ChunkData {
}
impl WorldModel {
fn get_chunk_coords(x: i32, y: i32) -> (ChunkCoord, usize) {
let chunk_x = x.div_euclid(CHUNK_SIZE as i32);
let chunk_y = y.div_euclid(CHUNK_SIZE as i32);
fn get_chunk_offset(x: i32, y: i32) -> usize {
let x = x.rem_euclid(CHUNK_SIZE as i32) as usize;
let y = y.rem_euclid(CHUNK_SIZE as i32) as usize;
let offset = x + y * CHUNK_SIZE;
(ChunkCoord(chunk_x, chunk_y), offset)
x + y * CHUNK_SIZE
}
/*fn set_pixel(&mut self, x: i32, y: i32, pixel: Pixel) {
@ -123,92 +108,47 @@ impl WorldModel {
self.updated_chunks.insert(chunk_coord);
}*/
fn get_pixel(&self, x: i32, y: i32) -> Pixel {
/*fn get_pixel(&self, x: i32, y: i32) -> Pixel {
let (chunk_coord, offset) = Self::get_chunk_coords(x, y);
self.chunks
.get(&chunk_coord)
.map(|chunk| chunk.pixel(offset))
.unwrap_or_default()
}
}*/
pub fn apply_noita_update(
&mut self,
update: &NoitaWorldUpdate,
update: NoitaWorldUpdate,
changed: &mut FxHashSet<ChunkCoord>,
) {
fn set_pixel(pixel: Pixel, chunk: &mut Chunk, offset: usize) -> bool {
let current = chunk.pixel(offset);
if current != pixel {
chunk.set_pixel(offset, pixel);
true
} else {
false
}
}
let header = &update.header;
let runs = &update.runs;
let mut x = 0;
let mut y = 0;
let (mut chunk_coord, _) = Self::get_chunk_coords(header.x, header.y);
let mut chunk = self.chunks.entry(chunk_coord).or_default();
for run in runs {
let flags = if run.data.flags > 0 {
PixelFlags::Fluid
} else {
PixelFlags::Normal
};
for _ in 0..run.length {
let xs = header.x + x;
let ys = header.y + y;
let (new_chunk_coord, offset) = Self::get_chunk_coords(xs, ys);
if chunk_coord != new_chunk_coord {
chunk_coord = new_chunk_coord;
chunk = self.chunks.entry(chunk_coord).or_default();
}
if set_pixel(
Pixel {
material: run.data.material,
flags,
},
chunk,
offset,
) {
self.updated_chunks.insert(chunk_coord);
if changed.contains(&chunk_coord) {
changed.remove(&chunk_coord);
}
}
x += 1;
if x == i32::from(header.w) + 1 {
x = 0;
y += 1;
}
let (start_x, start_y) = (
update.coord.0 * CHUNK_SIZE as i32,
update.coord.1 * CHUNK_SIZE as i32,
);
let chunk_coord = update.coord;
let chunk = self.chunks.entry(update.coord).or_default();
for (i, pixel) in update.pixels.into_iter().enumerate() {
let x = (i % CHUNK_SIZE) as i32;
let y = (i / CHUNK_SIZE) as i32;
let xs = start_x + x;
let ys = start_y + y;
let offset = Self::get_chunk_offset(xs, ys);
if chunk.set_pixel(offset, pixel) {
self.updated_chunks.insert(chunk_coord);
changed.remove(&chunk_coord);
}
}
}
pub fn get_noita_update(&self, x: i32, y: i32, w: u32, h: u32) -> NoitaWorldUpdate {
assert!(w <= 256);
assert!(h <= 256);
let mut runner = PixelRunner::new();
for j in 0..(h as i32) {
for i in 0..(w as i32) {
runner.put_pixel(self.get_pixel(x + i, y + j).to_raw())
}
}
runner.into_noita_update(x, y, (w - 1) as u8, (h - 1) as u8)
}
pub fn get_all_noita_updates(&self) -> Vec<Vec<u8>> {
pub fn get_all_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
let mut updates = Vec::new();
for chunk_coord in &self.updated_chunks {
let update = self.get_noita_update(
chunk_coord.0 * (CHUNK_SIZE as i32),
chunk_coord.1 * (CHUNK_SIZE as i32),
CHUNK_SIZE as u32,
CHUNK_SIZE as u32,
);
updates.push(update.save());
for coord in self.updated_chunks.drain() {
if let Some(chunk) = self.chunks.get_mut(&coord) {
updates.push(NoitaWorldUpdate {
coord,
pixels: chunk.pixels,
});
}
}
updates
}
@ -220,7 +160,7 @@ impl WorldModel {
for run in delta.runs.iter() {
if let Some(pixel) = run.data {
for _ in 0..run.length {
chunk.set_compact_pixel(offset, pixel);
chunk.set_pixel(offset, pixel);
offset += 1;
}
} else {
@ -237,7 +177,7 @@ impl WorldModel {
let chunk = self.chunks.get(&chunk_coord)?;
let mut runner = PixelRunner::new();
for i in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel((ignore_changed || chunk.changed(i)).then(|| chunk.compact_pixel(i)))
runner.put_pixel((ignore_changed || chunk.changed(i)).then(|| chunk.pixel(i)))
}
let runs = runner.build().into();
Some(ChunkDelta { chunk_coord, runs })

View file

@ -1,111 +1,61 @@
use std::num::NonZeroU16;
use bitcode::{Decode, Encode};
use crossbeam::atomic::AtomicCell;
use super::{
CHUNK_SIZE, ChunkData,
encoding::{PixelRunner, RawPixel},
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)]
pub enum PixelFlags {
/// Actual material isn't known yet.
#[default]
Unknown,
Normal,
Fluid,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)]
pub struct Pixel {
pub flags: PixelFlags,
pub material: u16,
}
impl Pixel {
pub fn to_raw(self) -> RawPixel {
RawPixel {
material: if self.flags != PixelFlags::Unknown {
self.material
} else {
u16::MAX
},
flags: if self.flags == PixelFlags::Normal {
0
} else {
1
},
}
}
pub fn to_compact(self) -> CompactPixel {
let flag_bit = if self.flags == PixelFlags::Normal {
0
} else {
1
};
let material = (self.material + 1) & 2047; // 11 bits for material
let raw = if self.flags == PixelFlags::Unknown {
CompactPixel::UNKNOWN_RAW
} else {
(material << 1) | flag_bit
};
CompactPixel(NonZeroU16::new(raw).unwrap())
}
fn from_compact(compact: CompactPixel) -> Self {
let raw = u16::from(compact.0);
let material = (raw >> 1) - 1;
let flags = if raw & 1 == 1 {
PixelFlags::Fluid
} else {
PixelFlags::Normal
};
if raw == CompactPixel::UNKNOWN_RAW {
Pixel {
flags: PixelFlags::Unknown,
material: 0,
}
} else {
Pixel { flags, material }
}
}
}
/// An entire pixel packed into 12 bits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
#[repr(transparent)]
pub struct CompactPixel(pub NonZeroU16);
impl CompactPixel {
const UNKNOWN_RAW: u16 = 4095;
fn from_raw(val: u16) -> Self {
CompactPixel(NonZeroU16::new(val).unwrap())
}
fn raw(self) -> u16 {
u16::from(self.0)
}
}
impl Default for CompactPixel {
fn default() -> Self {
Self(NonZeroU16::new(CompactPixel::UNKNOWN_RAW).unwrap())
}
}
use super::{ChunkData, encoding::PixelRunner};
use shared::world_sync::{CHUNK_SIZE, Pixel};
pub struct Chunk {
pixels: [u16; CHUNK_SIZE * CHUNK_SIZE],
changed: [bool; CHUNK_SIZE * CHUNK_SIZE],
pub pixels: [Pixel; CHUNK_SQUARE],
changed: Changed<bool, CHUNK_SQUARE>,
any_changed: bool,
crc: AtomicCell<Option<u64>>,
}
struct Changed<T: Default, const N: usize>([T; N]);
#[cfg(test)]
impl Changed<u128, CHUNK_SIZE> {
fn get(&self, n: usize) -> bool {
self.0[n / CHUNK_SIZE] & (1 << (n % CHUNK_SIZE)) != 0
}
fn set(&mut self, n: usize) {
self.0[n / CHUNK_SIZE] |= 1 << (n % CHUNK_SIZE)
}
}
const CHUNK_SQUARE: usize = CHUNK_SIZE * CHUNK_SIZE;
impl Changed<bool, CHUNK_SQUARE> {
fn get(&self, n: usize) -> bool {
self.0[n]
}
fn set(&mut self, n: usize) {
self.0[n] = true
}
}
#[test]
fn test_changed() {
let tmr = std::time::Instant::now();
for _ in 0..8192 {
let mut chunk = Changed([0; CHUNK_SIZE]);
for i in 0..CHUNK_SQUARE {
std::hint::black_box(chunk.get(i));
chunk.set(CHUNK_SQUARE - i - 1);
}
std::hint::black_box(chunk);
}
println!("u128 {}", tmr.elapsed().as_nanos());
let tmr = std::time::Instant::now();
for _ in 0..8192 {
let mut chunk = Changed([false; CHUNK_SQUARE]);
for i in 0..CHUNK_SQUARE {
std::hint::black_box(chunk.get(i));
chunk.set(CHUNK_SQUARE - i - 1);
}
std::hint::black_box(chunk);
}
println!("bool {}", tmr.elapsed().as_nanos())
}
impl Default for Chunk {
fn default() -> Self {
Self {
pixels: [4095; CHUNK_SIZE * CHUNK_SIZE],
changed: [false; CHUNK_SIZE * CHUNK_SIZE],
pixels: [Pixel::NIL; CHUNK_SQUARE],
changed: Changed([false; CHUNK_SQUARE]),
any_changed: false,
crc: None.into(),
}
}
}
@ -113,47 +63,37 @@ impl Default for Chunk {
/// Chunk of pixels. Stores pixels and tracks if they were changed.
impl Chunk {
pub fn pixel(&self, offset: usize) -> Pixel {
Pixel::from_compact(CompactPixel::from_raw(self.pixels[offset]))
self.pixels[offset]
}
pub fn compact_pixel(&self, offset: usize) -> CompactPixel {
CompactPixel::from_raw(self.pixels[offset])
}
pub fn set_pixel(&mut self, offset: usize, pixel: Pixel) {
let px = pixel.to_compact().raw();
if self.pixels[offset] != px {
self.pixels[offset] = px;
pub fn set_pixel(&mut self, offset: usize, pixel: Pixel) -> bool {
if self.pixels[offset] != pixel {
self.pixels[offset] = pixel;
self.mark_changed(offset);
true
} else {
false
}
}
pub fn set_compact_pixel(&mut self, offset: usize, pixel: CompactPixel) {
let px = pixel.raw();
if self.pixels[offset] != px {
self.pixels[offset] = px;
self.mark_changed(offset);
}
}
pub fn changed(&self, offset: usize) -> bool {
self.changed[offset]
self.changed.get(offset)
}
pub fn mark_changed(&mut self, offset: usize) {
self.changed[offset] = true;
self.changed.set(offset);
self.any_changed = true;
self.crc.store(None);
}
pub fn clear_changed(&mut self) {
self.changed = [false; CHUNK_SIZE * CHUNK_SIZE];
self.changed = Changed([false; CHUNK_SQUARE]);
self.any_changed = false;
}
pub fn to_chunk_data(&self) -> ChunkData {
let mut runner = PixelRunner::new();
for i in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel(self.compact_pixel(i))
for i in 0..CHUNK_SQUARE {
runner.put_pixel(self.pixel(i))
}
let runs = runner.build();
ChunkData { runs }

View file

@ -1,46 +1,12 @@
use bitcode::{Decode, Encode};
use bytemuck::{AnyBitPattern, NoUninit, bytes_of, pod_read_unaligned};
use serde::{Deserialize, Serialize};
use std::mem::size_of;
#[derive(Debug, Clone, Copy, AnyBitPattern, NoUninit, Serialize, Deserialize, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct Header {
pub x: i32,
pub y: i32,
pub w: u8,
pub h: u8,
pub run_count: u16,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NoitaWorldUpdate {
pub(crate) header: Header,
pub(crate) runs: Vec<PixelRun<RawPixel>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
struct ByteParser<'a> {
use shared::world_sync::PixelRun;
/*struct ByteParser<'a> {
data: &'a [u8],
}
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode)]
pub struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
}*/
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub struct PixelRunner<Pixel> {
current_pixel: Option<Pixel>,
current_run_len: u32,
current_run_len: u16,
runs: Vec<PixelRun<Pixel>>,
}
@ -86,24 +52,7 @@ impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
}
}
impl PixelRunner<RawPixel> {
/// Note: w/h are actualy width/height -1
pub fn into_noita_update(self, x: i32, y: i32, w: u8, h: u8) -> NoitaWorldUpdate {
let runs = self.build();
NoitaWorldUpdate {
header: Header {
x,
y,
w,
h,
run_count: runs.len() as u16,
},
runs,
}
}
}
impl<'a> ByteParser<'a> {
/*impl<'a> ByteParser<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data }
}
@ -117,7 +66,7 @@ impl<'a> ByteParser<'a> {
fn next_run(&mut self) -> PixelRun<RawPixel> {
PixelRun {
length: u32::from(self.next::<u16>()) + 1,
length: self.next::<u16>() + 1,
data: RawPixel {
material: self.next(),
flags: self.next(),
@ -125,37 +74,4 @@ impl<'a> ByteParser<'a> {
}
}
}
impl NoitaWorldUpdate {
pub fn load(data: &[u8]) -> Self {
let mut parser = ByteParser::new(data);
let header: Header = parser.next();
let mut runs = Vec::with_capacity(header.run_count.into());
for _ in 0..header.run_count {
runs.push(parser.next_run());
}
assert!(parser.data.is_empty());
Self { header, runs }
}
pub fn save(&self) -> Vec<u8> {
let header = Header {
run_count: self.runs.len() as u16,
..self.header
};
let mut buf = Vec::new();
buf.extend_from_slice(bytes_of(&header));
for run in &self.runs {
let len = u16::try_from(run.length - 1).unwrap();
buf.extend_from_slice(bytes_of(&len));
buf.extend_from_slice(bytes_of(&run.data.material));
buf.extend_from_slice(bytes_of(&run.data.flags));
}
buf
}
}
*/

View file

@ -396,11 +396,11 @@ pub fn create_player_png(
let (arrows_path, ping_path, map_icon) = arrows_path(tmp_path.into(), is_host);
let cursor_path = cursor_path(tmp_path.into());
let player_lukki = tmp_path.join("unmodified_lukki.png");
icon.save(tmp_path.join(format!("tmp/{}_icon.png", id)))
icon.save(tmp_path.join(format!("tmp/{id}_icon.png")))
.unwrap();
replace_colors(
player_path.into(),
tmp_path.join(format!("tmp/{}.png", id)),
tmp_path.join(format!("tmp/{id}.png")),
&rgb,
);
{
@ -414,49 +414,48 @@ pub fn create_player_png(
for px in img.pixels_mut() {
px.0[3] = px.0[3].min(64)
}
img.save(tmp_path.join(format!("tmp/{}_dc.png", id)))
.unwrap();
img.save(tmp_path.join(format!("tmp/{id}_dc.png"))).unwrap();
}
replace_colors(
player_lukki,
tmp_path.join(format!("tmp/{}_lukki.png", id)),
tmp_path.join(format!("tmp/{id}_lukki.png")),
&rgb,
);
replace_colors_opt(
arrows_path,
tmp_path.join(format!("tmp/{}_arrow.png", id)),
tmp_path.join(format!("tmp/{id}_arrow.png")),
&rgb,
inv,
);
replace_colors_opt(
ping_path,
tmp_path.join(format!("tmp/{}_ping.png", id)),
tmp_path.join(format!("tmp/{id}_ping.png")),
&rgb,
inv,
);
replace_colors_opt(
cursor_path,
tmp_path.join(format!("tmp/{}_cursor.png", id)),
tmp_path.join(format!("tmp/{id}_cursor.png")),
&rgb,
inv,
);
replace_colors(
tmp_path.join("knee.png"),
tmp_path.join(format!("tmp/{}_knee.png", id)),
tmp_path.join(format!("tmp/{id}_knee.png")),
&rgb,
);
replace_colors(
tmp_path.join("limb_a.png"),
tmp_path.join(format!("tmp/{}_limb_a.png", id)),
tmp_path.join(format!("tmp/{id}_limb_a.png")),
&rgb,
);
replace_colors(
tmp_path.join("limb_b.png"),
tmp_path.join(format!("tmp/{}_limb_b.png", id)),
tmp_path.join(format!("tmp/{id}_limb_b.png")),
&rgb,
);
replace_colors(map_icon, tmp_path.join(format!("tmp/{}_map.png", id)), &rgb);
let ragdoll_path = tmp_path.join(format!("tmp/{}_ragdoll.txt", id));
replace_colors(map_icon, tmp_path.join(format!("tmp/{id}_map.png")), &rgb);
let ragdoll_path = tmp_path.join(format!("tmp/{id}_ragdoll.txt"));
if ragdoll_path.exists() {
remove_file(ragdoll_path.clone()).unwrap()
}
@ -476,21 +475,16 @@ pub fn create_player_png(
.rev()
{
let f = tmp_path.join(s);
replace_colors(f, tmp_path.join(format!("tmp/{}_ragdoll_{}", id, s)), &rgb);
files = format!(
"{}mods/quant.ew/files/system/player/tmp/{}_ragdoll_{}\n",
files, id, s
);
replace_colors(f, tmp_path.join(format!("tmp/{id}_ragdoll_{s}")), &rgb);
files = format!("{files}mods/quant.ew/files/system/player/tmp/{id}_ragdoll_{s}\n");
}
ragdoll.write_all(files.as_bytes()).unwrap();
let img = create_arm(Rgba::from(to_u8(rgb.player_forearm)));
let path = tmp_path.join(format!("tmp/{}_arm.png", id));
let path = tmp_path.join(format!("tmp/{id}_arm.png"));
img.save(path).unwrap();
edit_nth_line(
tmp_path.join("unmodified_cape.xml").into(),
tmp_path
.join(format!("tmp/{}_cape.xml", id))
.into_os_string(),
tmp_path.join(format!("tmp/{id}_cape.xml")).into_os_string(),
vec![16, 16],
vec![
format!("cloth_color=\"0xFF{}\"", rgb_to_hex(to_u8(rgb.player_cape))),
@ -502,7 +496,7 @@ pub fn create_player_png(
);
edit_nth_line(
tmp_path.join("unmodified.xml").into(),
tmp_path.join(format!("tmp/{}_dc.xml", id)).into_os_string(),
tmp_path.join(format!("tmp/{id}_dc.xml")).into_os_string(),
vec![1],
vec![format!(
"filename=\"mods/quant.ew/files/system/player/tmp/{}_dc.png\"",
@ -511,7 +505,7 @@ pub fn create_player_png(
);
edit_nth_line(
tmp_path.join("unmodified.xml").into(),
tmp_path.join(format!("tmp/{}.xml", id)).into_os_string(),
tmp_path.join(format!("tmp/{id}.xml")).into_os_string(),
vec![1],
vec![format!(
"filename=\"mods/quant.ew/files/system/player/tmp/{}.png\"",
@ -523,7 +517,7 @@ pub fn create_player_png(
tmp_path.join("tmp/".to_owned() + &id.clone() + "_lukki.xml"),
&[(
"MARKER_LUKKI_PNG",
format!("mods/quant.ew/files/system/player/tmp/{}_lukki.png", id),
format!("mods/quant.ew/files/system/player/tmp/{id}_lukki.png"),
)],
);
edit_by_replacing(
@ -559,31 +553,29 @@ pub fn create_player_png(
),
(
"MARKER_MAIN_SPRITE",
format!("mods/quant.ew/files/system/player/tmp/{}.xml", id),
format!("mods/quant.ew/files/system/player/tmp/{id}.xml"),
),
(
"MARKER_LUKKI_SPRITE",
format!("mods/quant.ew/files/system/player/tmp/{}_lukki.xml", id),
format!("mods/quant.ew/files/system/player/tmp/{id}_lukki.xml"),
),
(
"MARKER_ARM_SPRITE",
format!("mods/quant.ew/files/system/player/tmp/{}_arm.xml", id),
format!("mods/quant.ew/files/system/player/tmp/{id}_arm.xml"),
),
(
"MARKER_CAPE",
format!("mods/quant.ew/files/system/player/tmp/{}_cape.xml", id),
format!("mods/quant.ew/files/system/player/tmp/{id}_cape.xml"),
),
(
"RAGDOLL_MARKER",
format!("mods/quant.ew/files/system/player/tmp/{}_ragdoll.txt", id),
format!("mods/quant.ew/files/system/player/tmp/{id}_ragdoll.txt"),
),
],
);
edit_nth_line(
tmp_path.join("unmodified_arm.xml").into(),
tmp_path
.join(format!("tmp/{}_arm.xml", id))
.into_os_string(),
tmp_path.join(format!("tmp/{id}_arm.xml")).into_os_string(),
vec![1],
vec![format!(
"filename=\"mods/quant.ew/files/system/player/tmp/{}_arm.png\"",
@ -609,7 +601,7 @@ fn edit_nth_line(path: OsString, exit: OsString, v: Vec<usize>, newline: Vec<Str
}
let mut file = File::create(exit).unwrap();
for line in lines {
writeln!(file, "{}", line).unwrap();
writeln!(file, "{line}").unwrap();
}
}

View file

@ -1,6 +1,8 @@
use std::path::PathBuf;
use argh::FromArgs;
use argh::{FromArgValue, FromArgs};
use crate::lobby_code::{LobbyCode, LobbyKind};
#[derive(FromArgs, PartialEq, Debug, Clone)]
/// Noita proxy.
@ -23,4 +25,28 @@ pub struct Args {
/// language for gui
#[argh(option)]
pub language: Option<String>,
// Used internally.
/// override lobby mode to use. Options: "Gog", "Steam".
#[argh(option)]
pub override_lobby_kind: Option<LobbyKind>,
/// used internally.
#[argh(option)]
pub auto_connect_to: Option<LobbyCode>,
}
impl FromArgValue for LobbyKind {
fn from_arg_value(value: &str) -> Result<Self, String> {
match value {
"Steam" => Ok(LobbyKind::Steam),
"Gog" => Ok(LobbyKind::Gog),
_ => Err("Unknown mode".to_string()),
}
}
}
impl FromArgValue for LobbyCode {
fn from_arg_value(value: &str) -> Result<Self, String> {
LobbyCode::parse(value).map_err(|e| e.to_string())
}
}

View file

@ -1,4 +1,4 @@
use fluent_bundle::FluentValue;
use fluent_templates::fluent_bundle::FluentValue;
use fluent_templates::{LanguageIdentifier, Loader};
use std::borrow::Cow;
use std::{collections::HashMap, sync::RwLock};

View file

@ -13,16 +13,16 @@ name = "chat"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossbeam = "0.8.2"
tracing = "0.1.36"
dashmap = "6.0.1"
quinn = "0.11.5"
rcgen = "0.13.1"
thiserror = "2.0.3"
tokio = { version = "1.40.0", features = ["macros", "io-util", "sync"] }
bitcode = "0.6.3"
socket2 = "0.5.8"
crossbeam = "0.8.4"
tracing = "0.1.41"
dashmap = "6.1.0"
quinn = "0.11.9"
rcgen = "0.14.3"
thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["macros", "io-util", "sync"] }
bitcode = "0.6.7"
socket2 = "0.6.0"
[dev-dependencies]
test-log = { version = "0.2.16", default-features = false, features = ["trace"]}
test-log = { version = "0.2.18", default-features = false, features = ["trace"]}
tracing-subscriber = {version = "0.3", features = ["env-filter", "fmt"]}

View file

@ -514,7 +514,7 @@ impl ConnectionManager {
fn default_server_config() -> ServerConfig {
let cert = rcgen::generate_simple_self_signed(vec!["tangled".into()]).unwrap();
let cert_der = CertificateDer::from(cert.cert);
let priv_key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der());
let priv_key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der());
let mut config =
ServerConfig::with_single_cert(vec![cert_der.clone()], priv_key.into()).unwrap();

View file

@ -24,7 +24,7 @@ impl Display for NetError {
NetError::UnknownPeer => write!(f, "No peer with this id"),
NetError::Disconnected => write!(f, "Not connected"),
NetError::MessageTooLong => {
write!(f, "Message len exceeds the limit of {}", MAX_MESSAGE_LEN)
write!(f, "Message len exceeds the limit of {MAX_MESSAGE_LEN}")
}
NetError::Dropped => write!(f, "Message dropped"),
NetError::Other => write!(f, "Other"),

View file

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "arrayvec"
version = "0.7.6"
@ -16,9 +22,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitcode"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18c1406a27371b2f76232a2259df6ab607b91b5a0a7476a7729ff590df5a969a"
checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74"
dependencies = [
"arrayvec",
"bitcode_derive",
@ -29,9 +35,9 @@ dependencies = [
[[package]]
name = "bitcode_derive"
version = "0.6.5"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23"
checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e"
dependencies = [
"proc-macro2",
"quote",
@ -40,15 +46,55 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.22.0"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "eyre"
@ -61,10 +107,20 @@ dependencies = [
]
[[package]]
name = "glam"
version = "0.30.0"
name = "flate2"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fcdf9683c406c2fc4d124afd29c0d595e22210d633cbdb8695ba9935ab1dc6"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "glam"
version = "0.30.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d1aab06663bdce00d6ca5e5ed586ec8d18033a771906c993a1e3755b368d85"
[[package]]
name = "heck"
@ -73,10 +129,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indenter"
version = "0.3.3"
name = "iced-x86"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
dependencies = [
"lazy_static",
]
[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]]
name = "itoa"
@ -85,10 +150,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libloading"
version = "0.8.6"
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets",
@ -96,19 +167,33 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "noita_api"
version = "0.1.0"
version = "1.6.0"
dependencies = [
"base64",
"eyre",
"iced-x86",
"libloading",
"noita_api_macro",
"object",
"rayon",
"rustc-hash",
"shared",
"smallvec",
]
[[package]]
@ -123,10 +208,21 @@ dependencies = [
]
[[package]]
name = "once_cell"
version = "1.21.0"
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"flate2",
"memchr",
"ruzstd",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pin-project-lite"
@ -136,9 +232,9 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@ -153,10 +249,39 @@ dependencies = [
]
[[package]]
name = "rustversion"
version = "1.0.20"
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "ruzstd"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c"
dependencies = [
"twox-hash",
]
[[package]]
name = "ryu"
@ -186,9 +311,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
@ -207,32 +332,37 @@ dependencies = [
]
[[package]]
name = "strum"
version = "0.27.1"
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@ -252,9 +382,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.28"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
@ -263,13 +393,19 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
]
[[package]]
name = "twox-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56"
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -277,11 +413,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "windows-targets"
version = "0.52.6"
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-targets"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
@ -294,48 +437,48 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

16
noita_api/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "noita_api"
version = "1.6.0"
edition = "2024"
[dependencies]
eyre = "0.6.12"
libloading = "0.8.8"
noita_api_macro = {path = "noita_api_macro"}
shared = {path = "../shared"}
base64 = "0.22.1"
rustc-hash = "2.1.1"
smallvec = "1.15.1"
object = "0.37.3"
rayon = "1.11.0"
iced-x86 = "1.21.0"

View file

@ -16,9 +16,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "noita_api_macro"
@ -33,9 +33,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@ -77,9 +77,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
@ -89,9 +89,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",

View file

@ -8,7 +8,7 @@ proc-macro = true
[dependencies]
heck = "0.5.0"
proc-macro2 = "1.0.89"
quote = "1.0.37"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
proc-macro2 = "1.0.101"
quote = "1.0.40"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"

View file

@ -178,9 +178,9 @@ fn generate_code_for_component(com: Component) -> proc_macro2::TokenStream {
let impls = com.fields.iter().filter_map(|field| {
let field_name_raw = &field.field;
let field_name_s = convert_field_name(&field.field);
let field_name = format_ident!("{}", field_name_s);
let field_name = format_ident!("{field_name_s}");
let field_doc = &field.desc;
let set_method_name = format_ident!("set_{}", field_name);
let set_method_name = format_ident!("set_{field_name}");
match field.typ {
Typ::Other => None,
_ => {

View file

@ -0,0 +1,275 @@
use std::{os::raw::c_void, ptr};
use crate::lua::LuaState;
use crate::noita::types::{
ComponentSystemManager, ComponentTypeManager, EntityManager, GameGlobal, GlobalStats,
Inventory, Mods, Platform, StdString, StdVec, TagManager, TranslationManager,
};
use iced_x86::{Decoder, DecoderOptions, Mnemonic};
pub(crate) unsafe fn grab_addr_from_instruction(
func: *const c_void,
offset: isize,
expected_mnemonic: Mnemonic,
) -> *mut c_void {
let instruction_addr = func.wrapping_offset(offset);
// We don't really have an idea of how many bytes the instruction takes, so just take *enough* bytes for most cases.
let instruction_bytes = unsafe { ptr::read_unaligned(instruction_addr.cast::<[u8; 16]>()) };
let mut decoder = Decoder::with_ip(
32,
&instruction_bytes,
instruction_addr as u64,
DecoderOptions::NONE,
);
let instruction = decoder.decode();
#[cfg(debug_assertions)]
if instruction.mnemonic() != expected_mnemonic {
println!("Encountered unexpected mnemonic: {instruction}");
}
assert_eq!(instruction.mnemonic(), expected_mnemonic);
instruction.memory_displacement32() as *mut c_void
}
// This only stores pointers that are constant, so should be safe to share between threads.
unsafe impl Sync for Globals {}
unsafe impl Send for Globals {}
#[derive(Debug)]
pub struct GlobalsRef {
pub world_seed: usize,
pub new_game_count: usize,
pub game_global: &'static GameGlobal,
pub entity_manager: &'static EntityManager,
pub entity_tag_manager: &'static TagManager<u16>,
pub component_type_manager: &'static ComponentTypeManager,
pub component_tag_manager: &'static TagManager<u8>,
pub translation_manager: &'static TranslationManager,
pub platform: &'static Platform,
pub global_stats: &'static GlobalStats,
pub filenames: &'static StdVec<StdString>,
pub inventory: &'static Inventory,
pub mods: &'static Mods,
pub max_component: &'static usize,
pub component_manager: &'static ComponentSystemManager,
}
#[derive(Debug)]
pub struct GlobalsMut {
pub world_seed: &'static mut usize,
pub new_game_count: &'static mut usize,
pub game_global: &'static mut GameGlobal,
pub entity_manager: &'static mut EntityManager,
pub entity_tag_manager: &'static mut TagManager<u16>,
pub component_type_manager: &'static mut ComponentTypeManager,
pub component_tag_manager: &'static mut TagManager<u8>,
pub translation_manager: &'static mut TranslationManager,
pub platform: &'static mut Platform,
pub global_stats: &'static mut GlobalStats,
pub filenames: &'static mut StdVec<StdString>,
pub inventory: &'static mut Inventory,
pub mods: &'static mut Mods,
pub max_component: &'static mut usize,
pub component_manager: &'static mut ComponentSystemManager,
}
#[derive(Debug, Default)]
pub struct Globals {
pub world_seed: *mut usize,
pub new_game_count: *mut usize,
pub game_global: *const *mut GameGlobal,
pub entity_manager: *const *mut EntityManager,
pub entity_tag_manager: *const *mut TagManager<u16>,
pub component_type_manager: *mut ComponentTypeManager,
pub component_tag_manager: *const *mut TagManager<u8>,
pub translation_manager: *mut TranslationManager,
pub platform: *mut Platform,
pub global_stats: *mut GlobalStats,
pub filenames: *mut StdVec<StdString>,
pub inventory: *mut Inventory,
pub mods: *mut Mods,
pub max_component: *mut usize,
pub component_manager: *mut ComponentSystemManager,
}
#[allow(clippy::mut_from_ref)]
impl Globals {
pub fn world_seed(&self) -> usize {
unsafe { self.world_seed.as_ref().copied().unwrap() }
}
pub fn new_game_count(&self) -> usize {
unsafe { self.new_game_count.as_ref().copied().unwrap() }
}
pub fn game_global(&self) -> &'static GameGlobal {
unsafe { self.game_global.as_ref().unwrap().as_ref().unwrap() }
}
pub fn entity_manager(&self) -> &'static EntityManager {
unsafe { self.entity_manager.as_ref().unwrap().as_ref().unwrap() }
}
pub fn entity_tag_manager(&self) -> &'static TagManager<u16> {
unsafe { self.entity_tag_manager.as_ref().unwrap().as_ref().unwrap() }
}
pub fn component_type_manager(&self) -> &'static ComponentTypeManager {
unsafe { self.component_type_manager.as_ref().unwrap() }
}
pub fn component_tag_manager(&self) -> &'static TagManager<u8> {
unsafe {
self.component_tag_manager
.as_ref()
.unwrap()
.as_ref()
.unwrap()
}
}
pub fn component_manager(&self) -> &'static ComponentSystemManager {
unsafe { self.component_manager.as_ref().unwrap() }
}
pub fn translation_manager(&self) -> &'static TranslationManager {
unsafe { self.translation_manager.as_ref().unwrap() }
}
pub fn platform(&self) -> &'static Platform {
unsafe { self.platform.as_ref().unwrap() }
}
pub fn global_stats(&self) -> &'static GlobalStats {
unsafe { self.global_stats.as_ref().unwrap() }
}
pub fn filenames(&self) -> &'static StdVec<StdString> {
unsafe { self.filenames.as_ref().unwrap() }
}
pub fn inventory(&self) -> &'static Inventory {
unsafe { self.inventory.as_ref().unwrap() }
}
pub fn mods(&self) -> &'static Mods {
unsafe { self.mods.as_ref().unwrap() }
}
pub fn max_component(&self) -> &'static usize {
unsafe { self.max_component.as_ref().unwrap() }
}
pub fn world_seed_mut(&self) -> &'static mut usize {
unsafe { self.world_seed.as_mut().unwrap() }
}
pub fn new_game_count_mut(&self) -> &'static mut usize {
unsafe { self.new_game_count.as_mut().unwrap() }
}
pub fn game_global_mut(&self) -> &'static mut GameGlobal {
unsafe { self.game_global.as_ref().unwrap().as_mut().unwrap() }
}
pub fn entity_manager_mut(&self) -> &'static mut EntityManager {
unsafe { self.entity_manager.as_ref().unwrap().as_mut().unwrap() }
}
pub fn entity_tag_manager_mut(&self) -> &'static mut TagManager<u16> {
unsafe { self.entity_tag_manager.as_ref().unwrap().as_mut().unwrap() }
}
pub fn component_type_manager_mut(&self) -> &'static mut ComponentTypeManager {
unsafe { self.component_type_manager.as_mut().unwrap() }
}
pub fn component_tag_manager_mut(&self) -> &'static mut TagManager<u8> {
unsafe {
self.component_tag_manager
.as_ref()
.unwrap()
.as_mut()
.unwrap()
}
}
pub fn translation_manager_mut(&self) -> &'static mut TranslationManager {
unsafe { self.translation_manager.as_mut().unwrap() }
}
pub fn platform_mut(&self) -> &'static mut Platform {
unsafe { self.platform.as_mut().unwrap() }
}
pub fn global_stats_mut(&self) -> &'static mut GlobalStats {
unsafe { self.global_stats.as_mut().unwrap() }
}
pub fn filenames_mut(&self) -> &'static mut StdVec<StdString> {
unsafe { self.filenames.as_mut().unwrap() }
}
pub fn inventory_mut(&self) -> &'static mut Inventory {
unsafe { self.inventory.as_mut().unwrap() }
}
pub fn mods_mut(&self) -> &'static mut Mods {
unsafe { self.mods.as_mut().unwrap() }
}
pub fn max_component_mut(&self) -> &'static mut usize {
unsafe { self.max_component.as_mut().unwrap() }
}
pub fn component_manager_mut(&self) -> &'static mut ComponentSystemManager {
unsafe { self.component_manager.as_mut().unwrap() }
}
pub fn as_ref(&self) -> GlobalsRef {
GlobalsRef {
world_seed: self.world_seed(),
new_game_count: self.new_game_count(),
game_global: self.game_global(),
entity_manager: self.entity_manager(),
entity_tag_manager: self.entity_tag_manager(),
component_type_manager: self.component_type_manager(),
component_tag_manager: self.component_tag_manager(),
translation_manager: self.translation_manager(),
platform: self.platform(),
global_stats: self.global_stats(),
filenames: self.filenames(),
inventory: self.inventory(),
mods: self.mods(),
max_component: self.max_component(),
component_manager: self.component_manager(),
}
}
pub fn as_mut(&self) -> GlobalsMut {
GlobalsMut {
world_seed: self.world_seed_mut(),
new_game_count: self.new_game_count_mut(),
game_global: self.game_global_mut(),
entity_manager: self.entity_manager_mut(),
entity_tag_manager: self.entity_tag_manager_mut(),
component_type_manager: self.component_type_manager_mut(),
component_tag_manager: self.component_tag_manager_mut(),
translation_manager: self.translation_manager_mut(),
platform: self.platform_mut(),
global_stats: self.global_stats_mut(),
filenames: self.filenames_mut(),
inventory: self.inventory_mut(),
mods: self.mods_mut(),
max_component: self.max_component_mut(),
component_manager: self.component_manager_mut(),
}
}
pub fn new(lua: LuaState) -> Self {
lua.get_global(c"EntityGetFilename");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let entity_manager: *const *mut EntityManager = unsafe {
grab_addr_from_instruction(base, 0x00797821 - 0x00797570, Mnemonic::Mov).cast()
};
lua.pop_last();
let world_seed = 0x1205004 as *mut usize;
let new_game_count = 0x1205024 as *mut usize;
let global_stats = 0x1208940 as *mut GlobalStats;
let game_global = 0x122374c as *const *mut GameGlobal;
let entity_tag_manager = 0x1206fac as *const *mut TagManager<u16>;
let component_type_manager = 0x1223c88 as *mut ComponentTypeManager;
let component_tag_manager = 0x1204b30 as *const *mut TagManager<u8>;
let translation_manager = 0x1207c28 as *mut TranslationManager;
let platform = 0x1221bc0 as *mut Platform;
let filenames = 0x1207bd4 as *mut StdVec<StdString>;
let inventory = 0x12224f0 as *mut Inventory;
let mods = 0x1207e90 as *mut Mods;
let max_component = 0x1152ff0 as *mut usize;
let component_manager = 0x12236e8 as *mut ComponentSystemManager;
Self {
world_seed,
new_game_count,
game_global,
entity_manager,
entity_tag_manager,
component_type_manager,
component_tag_manager,
translation_manager,
platform,
global_stats,
filenames,
inventory,
mods,
max_component,
component_manager,
}
}
}

41
noita_api/src/heap.rs Normal file
View file

@ -0,0 +1,41 @@
use std::sync::LazyLock;
struct Msvcr {
op_new: unsafe extern "C" fn(n: std::os::raw::c_uint) -> *mut std::os::raw::c_void,
// op_delete: unsafe extern "C" fn(*const std::os::raw::c_void),
// op_delete_array: unsafe extern "C" fn(*const std::os::raw::c_void),
}
static MSVCR: LazyLock<Msvcr> = LazyLock::new(|| unsafe {
let lib = libloading::Library::new("./msvcr120.dll").expect("library to exist");
let op_new = *lib.get(b"??2@YAPAXI@Z\0").expect("symbol to exist");
// let op_delete = *lib.get(b"operator_delete\0").expect("symbol to exist");
// let op_delete_array = *lib.get(b"operator_delete[]\0").expect("symbol to exist");
Msvcr {
op_new,
// op_delete,
// op_delete_array,
}
});
/// Allocate some memory, using the same allocator noita uses.
pub fn raw_new(size: usize) -> *mut std::os::raw::c_void {
let size = size as std::os::raw::c_uint;
assert!(size > 0, "Doesn't make sense to allocate memory of size 0");
unsafe { (MSVCR.op_new)(size) }
}
/// Allocates memory using noita's allocator and moves *value* to it.
pub fn place_new<T>(value: T) -> *mut T {
let size = size_of::<T>();
let place = raw_new(size) as *mut T;
unsafe {
place.copy_from_nonoverlapping(&value, size);
}
place
}
/// Same as place_new, but returns &'static mut
pub fn place_new_ref<T>(value: T) -> &'static mut T {
unsafe { &mut *place_new(value) }
}

1819
noita_api/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
pub mod lua_bindings;
use eyre::{Context, OptionExt, bail, eyre};
use lua_bindings::{LUA_GLOBALSINDEX, Lua51, lua_CFunction, lua_State};
use std::{
array,
borrow::Cow,
cell::Cell,
ffi::{CStr, c_char, c_int},
mem, slice,
slice,
str::FromStr,
sync::LazyLock,
};
use eyre::{Context, OptionExt, bail};
use lua_bindings::{LUA_GLOBALSINDEX, Lua51, lua_CFunction, lua_State};
use crate::{Color, ComponentID, EntityID, GameEffectEnum, Obj, PhysicsBodyID};
thread_local! {
@ -52,6 +52,18 @@ impl LuaState {
unsafe { LUA.lua_tointeger(self.lua, index) }
}
pub fn to_integer_array(
&self,
index: i32,
len: usize,
) -> impl DoubleEndedIterator<Item = isize> {
(1..=len).map(move |i| unsafe {
LUA.lua_pushinteger(self.lua, i as lua_bindings::lua_Integer);
LUA.lua_gettable(self.lua, index);
LUA.lua_tointeger(self.lua, -1)
})
}
pub fn to_number(&self, index: i32) -> f64 {
unsafe { LUA.lua_tonumber(self.lua, index) }
}
@ -226,7 +238,7 @@ impl<R: LuaFnRet> LuaFnRet for eyre::Result<R> {
match self {
Ok(ok) => ok.do_return(lua),
Err(err) => unsafe {
lua.raise_error(format!("Error in rust call: {:?}", err));
lua.raise_error(format!("Error in rust call: {err:?}"));
},
}
}
@ -292,7 +304,7 @@ impl LuaPutValue for isize {
impl LuaPutValue for u32 {
fn put(&self, lua: LuaState) {
lua.push_integer(unsafe { mem::transmute::<u32, i32>(*self) as isize });
lua.push_integer(self.cast_signed() as isize);
}
}
@ -421,7 +433,7 @@ impl LuaGetValue for isize {
impl LuaGetValue for u32 {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(unsafe { mem::transmute::<i32, u32>(lua.to_integer(index) as i32) })
Ok((lua.to_integer(index) as i32).cast_unsigned())
}
}
@ -531,6 +543,30 @@ impl<T: LuaGetValue> LuaGetValue for Vec<T> {
}
}
impl<T: LuaGetValue, const N: usize> LuaGetValue for [T; N] {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
if T::size_on_stack() != 1 {
bail!(
"Encountered [T; N] where T needs more than 1 slot on the stack. This isn't supported"
);
}
let len = lua.objlen(index);
if len != N {
return Err(eyre!("mis matched length {}", len));
}
let mut res: [Option<T>; N] = array::from_fn(|_| None);
for (i, res) in res.iter_mut().enumerate() {
lua.index_table(index, i);
let get = T::get(lua, -1);
lua.pop_last();
*res = Some(get?)
}
let mut res = res.into_iter();
let res: [T; N] = array::from_fn(|_| res.next().unwrap().unwrap());
Ok(res)
}
}
impl LuaGetValue for GameEffectEnum {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(GameEffectEnum::from_str(&lua.to_string(index)?)?)

View file

@ -0,0 +1,81 @@
use crate::noita::types;
use crate::noita::types::{
CellVTable, CellVTables, FireCellVTable, GasCellVTable, LiquidCellVTable, NoneCellVTable,
SolidCellVTable,
};
use eyre::ContextCompat;
use object::{Object, ObjectSection};
use std::arch::asm;
use std::ffi::c_void;
use std::fs::File;
use std::io::Read;
pub fn get_functions() -> eyre::Result<(types::CellVTables, *mut types::GameGlobal)> {
let exe = std::env::current_exe()?;
let mut file = File::open(exe)?;
let mut vec = Vec::with_capacity(15460864);
file.read_to_end(&mut vec)?;
let obj = object::File::parse(vec.as_slice())?;
let text = obj.section_by_name(".text").wrap_err("obj err")?;
let data = text.data()?;
let game_global: &[u8] = &[0xe8];
let game_global2 = &[0x8b, 0x40, 0x48, 0x8b, 0x00, 0xc1, 0xe8, 0x02, 0xa8, 0x01];
let start = text.address() as *const c_void;
let (game_global, offset) = find_pattern_global(data, game_global, game_global2)?;
let ptr = unsafe { start.add(game_global) };
let game_global_ptr = get_rela_call(ptr, offset);
let game_global_ptr = get_global(game_global_ptr);
let cellvtables = unsafe {
let none = CellVTable {
none: (0xff2040 as *const NoneCellVTable).as_ref().unwrap(),
};
let liquid = CellVTable {
liquid: (0x100bb90 as *const LiquidCellVTable).as_ref().unwrap(),
};
let gas = CellVTable {
gas: (0x1007bcc as *const GasCellVTable).as_ref().unwrap(),
};
let solid = CellVTable {
solid: (0xff8a6c as *const SolidCellVTable).as_ref().unwrap(),
};
let fire = CellVTable {
fire: (0x10096e0 as *const FireCellVTable).as_ref().unwrap(),
};
CellVTables([none, liquid, gas, solid, fire])
};
Ok((cellvtables, game_global_ptr))
}
fn get_global(global: *const c_void) -> *mut types::GameGlobal {
unsafe {
let ptr: *mut types::GameGlobal;
asm!(
"call {global}",
global = in(reg) global,
clobber_abi("C"),
out("eax") ptr,
);
ptr
}
}
/*fn find_pattern(data: &[u8], pattern: &[u8]) -> eyre::Result<usize> {
data.windows(pattern.len())
.position(|window| window == pattern)
.wrap_err("match err")
}*/
fn find_pattern_global(data: &[u8], pattern: &[u8], other: &[u8]) -> eyre::Result<(usize, isize)> {
let r = data
.windows(pattern.len() + 4 + other.len())
.enumerate()
.filter(|(_, window)| window.starts_with(pattern) && window.ends_with(other))
.nth(1)
.map(|(i, _)| i)
.wrap_err("match err")?;
let mut iter = data[r + 1..=r + 4].iter();
let bytes = std::array::from_fn(|_| *iter.next().unwrap());
Ok((r, isize::from_ne_bytes(bytes)))
}
fn get_rela_call(ptr: *const c_void, offset: isize) -> *const c_void {
unsafe {
let next_instruction = ptr.add(5);
next_instruction.offset(offset)
}
}

View file

@ -0,0 +1,3 @@
pub mod init_data;
pub mod types;
pub mod world;

View file

@ -0,0 +1,598 @@
mod component;
mod component_data;
mod entity;
mod misc;
mod objects;
mod platform;
mod world;
pub use component::*;
pub use component_data::*;
pub use entity::*;
pub use misc::*;
pub use objects::*;
pub use platform::*;
use std::alloc::Layout;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::fmt::{Debug, Display, Formatter};
use std::ops::{Index, IndexMut};
use std::{alloc, ptr, slice};
pub use world::*;
use crate::heap;
#[repr(C)]
union Buffer {
buffer: *const u8,
sso_buffer: [u8; 16],
}
impl Default for Buffer {
fn default() -> Self {
Buffer {
sso_buffer: [0; 16],
}
}
}
#[repr(C)]
#[derive(Default)]
pub struct StdString {
buffer: Buffer,
size: usize,
capacity: usize,
}
impl StdString {
pub fn get(&self, index: usize) -> u8 {
unsafe {
if self.capacity <= 16 {
self.buffer.sso_buffer[index]
} else {
self.buffer.buffer.add(index).read()
}
}
}
}
impl AsRef<str> for StdString {
fn as_ref(&self) -> &str {
let slice: &[u8] = unsafe {
if self.capacity <= 16 {
&self.buffer.sso_buffer
} else {
slice::from_raw_parts(self.buffer.buffer, self.size)
}
};
let actual_len = slice.iter().position(|&b| b == 0).unwrap_or(self.size);
str::from_utf8(&slice[..actual_len]).unwrap_or("UTF8_ERR")
}
}
impl From<&str> for StdString {
fn from(value: &str) -> Self {
let mut res = StdString {
buffer: Default::default(),
capacity: value.len(),
size: value.len(),
};
if res.capacity > 16 {
let buffer = heap::place_new(value);
res.buffer.buffer = buffer.cast();
} else {
let mut iter = value.as_bytes().iter();
res.buffer.sso_buffer = std::array::from_fn(|_| iter.next().copied().unwrap_or(0))
}
res
}
}
impl StdString {
pub const fn from_str(value: &'static str) -> Self {
let mut res = StdString {
buffer: Buffer {
sso_buffer: [0; 16],
},
capacity: value.len(),
size: value.len(),
};
if res.capacity > 16 {
res.buffer.buffer = value.as_ptr();
} else {
let iter = value.as_bytes();
res.buffer.sso_buffer = [
if 0 < res.size { iter[0] } else { 0 },
if 1 < res.size { iter[1] } else { 0 },
if 2 < res.size { iter[2] } else { 0 },
if 3 < res.size { iter[3] } else { 0 },
if 4 < res.size { iter[4] } else { 0 },
if 5 < res.size { iter[5] } else { 0 },
if 6 < res.size { iter[6] } else { 0 },
if 7 < res.size { iter[7] } else { 0 },
if 8 < res.size { iter[8] } else { 0 },
if 9 < res.size { iter[9] } else { 0 },
if 10 < res.size { iter[10] } else { 0 },
if 11 < res.size { iter[11] } else { 0 },
if 12 < res.size { iter[12] } else { 0 },
if 13 < res.size { iter[13] } else { 0 },
if 14 < res.size { iter[14] } else { 0 },
if 15 < res.size { iter[15] } else { 0 },
]
}
res
}
}
impl Display for StdString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl Debug for StdString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("StdString").field(&self.as_ref()).finish()
}
}
impl PartialEq for StdString {
fn eq(&self, other: &Self) -> bool {
if self.size == other.size {
self.as_ref() == other.as_ref()
} else {
false
}
}
}
impl PartialOrd for StdString {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for StdString {}
impl Ord for StdString {
fn cmp(&self, other: &Self) -> Ordering {
let smallest = self.size.min(other.size);
for i in 0..smallest {
match self.get(i).cmp(&other.get(i)) {
Ordering::Equal => continue,
non_eq => return non_eq,
}
}
self.size.cmp(&other.size)
}
}
#[repr(transparent)]
pub struct CString(pub *const u8);
impl From<&str> for CString {
fn from(value: &str) -> Self {
let value = value.to_owned() + "\0";
let str = heap::place_new(value).cast();
CString(str)
}
}
impl CString {
pub const fn from_str(value: &'static str) -> Self {
CString(value.as_ptr())
}
}
#[test]
fn test_str() {
let str;
let str2;
{
str = CString::from("test");
str2 = const { CString::from_str("test\0") };
assert_eq!(str.to_string(), "test");
assert_eq!(str2.to_string(), "test")
}
assert_eq!(str.to_string(), "test");
assert_eq!(str2.to_string(), "test");
assert_eq!(WalletComponent::NAME, WalletComponent::C_NAME.to_string());
assert_eq!(WalletComponent::NAME, WalletComponent::STD_NAME.to_string());
}
#[test]
fn test_cstring() {
let a = CString::from_str("test");
let b = CString::from_str("test");
let c = CString::from_str("testb");
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(c, a);
}
impl PartialEq for CString {
fn eq(&self, other: &Self) -> bool {
unsafe {
let mut ptra = self.0;
let mut ptrb = other.0;
let mut a = ptra.read();
let mut b = ptrb.read();
while a != 0 {
if a != b {
return false;
}
ptra = ptra.offset(1);
a = ptra.read();
ptrb = ptrb.offset(1);
b = ptrb.read();
}
b == 0
}
}
}
impl Display for CString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.0.is_null() {
return write!(f, "");
}
let mut string = String::new();
unsafe {
let mut ptr = self.0;
let mut c = ptr.read();
while c != 0 {
string.push(char::from(c));
ptr = ptr.offset(1);
c = ptr.read();
}
}
write!(f, "{string}")
}
}
impl Debug for CString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CString").field(&self.to_string()).finish()
}
}
#[repr(transparent)]
pub struct CStr<const N: usize>(pub [u8; N]);
impl<const N: usize> Display for CStr<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut string = String::new();
for c in self.0 {
if c == 0 {
break;
}
string.push(char::from(c));
}
write!(f, "{string}")
}
}
impl<const N: usize> Debug for CStr<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CStr").field(&self.to_string()).finish()
}
}
#[repr(C)]
pub struct StdVec<T> {
pub start: *mut T,
pub end: *mut T,
pub cap: *mut T,
}
impl<T> Index<usize> for StdVec<T> {
type Output = T;
fn index(&self, index: usize) -> &Self::Output {
self.get(index).unwrap()
}
}
impl<T> IndexMut<usize> for StdVec<T> {
fn index_mut(&mut self, index: usize) -> &mut T {
self.get_mut(index).unwrap()
}
}
impl<T> AsRef<[T]> for StdVec<T> {
fn as_ref(&self) -> &[T] {
if self.start.is_null() {
&[]
} else {
unsafe { slice::from_raw_parts(self.start, self.len()) }
}
}
}
impl<T> AsMut<[T]> for StdVec<T> {
fn as_mut(&mut self) -> &mut [T] {
unsafe { slice::from_raw_parts_mut(self.start, self.len()) }
}
}
impl<T: Debug> Debug for StdVec<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("StdVec").field(&self.as_ref()).finish()
}
}
impl<T> Default for StdVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> StdVec<T> {
pub fn null() -> Self {
Self {
start: ptr::null_mut(),
end: ptr::null_mut(),
cap: ptr::null_mut(),
}
}
pub fn copy(&self) -> Self {
Self {
start: self.start,
end: self.end,
cap: self.cap,
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.as_ref().iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.as_mut().iter_mut()
}
pub fn capacity(&self) -> usize {
unsafe { self.cap.offset_from_unsigned(self.start) }
}
pub fn len(&self) -> usize {
unsafe { self.end.offset_from_unsigned(self.start) }
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
pub fn get_static(&self, index: usize) -> Option<&'static T> {
let ptr = unsafe { self.start.add(index) };
if self.end > ptr {
unsafe { ptr.as_ref() }
} else {
None
}
}
pub fn get(&self, index: usize) -> Option<&T> {
let ptr = unsafe { self.start.add(index) };
if self.end > ptr {
unsafe { ptr.as_ref() }
} else {
None
}
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
let ptr = unsafe { self.start.add(index) };
if self.end > ptr {
unsafe { ptr.as_mut() }
} else {
None
}
}
fn alloc(&mut self, n: usize) {
if self.cap < unsafe { self.end.add(n) } {
let old_len = self.len();
let old_cap = self.capacity();
let new_cap = if old_cap == 0 { 4 } else { old_cap * 2 }; //TODO deal with n > 1
let layout = Layout::array::<T>(new_cap).unwrap();
let new_ptr = unsafe { alloc::alloc(layout) }.cast();
if old_len > 0 {
unsafe {
ptr::copy_nonoverlapping(self.start, new_ptr, old_len);
}
let old_layout = Layout::array::<T>(old_cap).unwrap();
unsafe {
alloc::dealloc(self.start.cast(), old_layout);
}
}
self.start = new_ptr;
self.end = unsafe { new_ptr.add(old_len) };
self.cap = unsafe { new_ptr.add(new_cap) };
}
}
pub fn with_capacity(n: usize) -> StdVec<T> {
let mut v = Self::null();
v.alloc(n);
v
}
pub fn new() -> StdVec<T> {
Self::with_capacity(1)
}
pub fn push(&mut self, value: T) {
self.alloc(1);
unsafe {
self.end.write(value);
self.end = self.end.add(1);
}
}
pub fn pop(&mut self) -> Option<T> {
if self.start == self.end {
return None;
}
unsafe {
self.end = self.end.sub(1);
let ret = self.end.read();
Some(ret)
}
}
pub fn last(&self) -> Option<&T> {
unsafe { self.end.sub(1).as_ref() }
}
pub fn last_mut(&mut self) -> Option<&mut T> {
unsafe { self.end.sub(1).as_mut() }
}
pub fn insert(&mut self, index: usize, value: T) {
self.alloc(1);
for i in (index..self.len()).rev() {
unsafe { self.start.add(i + 1).write(self.start.add(i).read()) }
}
unsafe {
self.end = self.end.add(1);
self.start.add(index).write(value);
}
}
pub fn remove(&mut self, index: usize) -> T {
unsafe {
let ret = self.start.add(index).read();
for i in index..self.len() - 1 {
self.start.add(i).write(self.start.add(i + 1).read())
}
self.end = self.end.sub(1);
ret
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct StdMapNode<K, V> {
pub left: *mut StdMapNode<K, V>,
pub parent: *mut StdMapNode<K, V>,
pub right: *mut StdMapNode<K, V>,
pub color: bool,
pub end: bool,
unk: [u8; 2],
pub key: K,
pub value: V,
}
impl<K: Default, V: Default> Default for StdMapNode<K, V> {
fn default() -> Self {
Self {
left: ptr::null_mut(),
parent: ptr::null_mut(),
right: ptr::null_mut(),
color: false,
end: true,
unk: [0, 0],
key: Default::default(),
value: Default::default(),
}
}
}
impl<K, V> StdMapNode<K, V> {
pub fn new(key: K, value: V) -> Self {
Self {
left: ptr::null_mut(),
parent: ptr::null_mut(),
right: ptr::null_mut(),
color: false,
end: true,
unk: [0, 0],
key,
value,
}
}
}
#[repr(C)]
pub struct StdMap<K: 'static, V: 'static> {
pub root: &'static mut StdMapNode<K, V>,
pub len: usize,
}
impl<K: Default + 'static, V: Default + 'static> Default for StdMap<K, V> {
fn default() -> Self {
Self {
root: unsafe { &mut *heap::place_new(StdMapNode::default()) },
len: 0,
}
}
}
impl<K: Debug + 'static, V: Debug + 'static> Debug for StdMap<K, V> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("StdMap")
.field(&self.iter().collect::<Vec<_>>())
.finish()
}
}
#[derive(Debug)]
pub struct StdMapIter<K: 'static, V: 'static> {
pub root: *mut StdMapNode<K, V>,
pub current: *const StdMapNode<K, V>,
pub parents: Vec<*const StdMapNode<K, V>>,
}
impl<K: 'static, V: 'static> Iterator for StdMapIter<K, V> {
type Item = (&'static K, &'static V);
fn next(&mut self) -> Option<Self::Item> {
if self.current == self.root {
return None;
}
let tag = unsafe { self.current.as_ref()? };
self.current = if tag.right != self.root {
if tag.left == self.root {
tag.right
} else {
self.parents.push(tag.right);
tag.left
}
} else if tag.left == self.root {
self.parents.pop().unwrap_or(self.root)
} else {
tag.left
};
Some((&tag.key, &tag.value))
}
}
impl<K: 'static, V: 'static> StdMap<K, V> {
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
if self.is_empty() {
self.len += 1;
let node = StdMapNode::new(key, value);
self.root.parent = heap::place_new(node);
None
} else {
todo!()
}
}
pub fn iter(&self) -> impl Iterator<Item = (&'static K, &'static V)> + '_ {
StdMapIter {
root: (self.root as *const StdMapNode<K, V>).cast_mut(),
current: self.root.parent,
parents: Vec::with_capacity(8),
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn iter_keys(&self) -> impl Iterator<Item = &'static K> {
self.iter().map(|(k, _)| k)
}
pub fn iter_values(&self) -> impl Iterator<Item = &'static V> {
self.iter().map(|(_, v)| v)
}
}
impl<K: 'static + Ord, V: 'static> StdMap<K, V> {
pub fn get(&self, key: &K) -> Option<&'static V> {
let mut node = unsafe { self.root.parent.as_ref()? };
loop {
let next = match key.cmp(&node.key) {
Ordering::Less => node.left,
Ordering::Greater => node.right,
Ordering::Equal => return Some(&node.value),
};
if next == (self.root as *const StdMapNode<K, V>).cast_mut() {
return None;
}
node = unsafe { next.as_ref()? };
}
}
}
#[repr(transparent)]
pub struct ThiscallFn(c_void);
#[derive(Debug, Default)]
#[repr(C)]
pub struct LensValueBool {
pub value: bool,
pub valueb: bool,
padding: [u8; 2],
pub frame: isize,
}
#[derive(Debug, Default)]
#[repr(C)]
pub struct LensValue<T> {
pub value: T,
pub valueb: T,
pub frame: isize,
}
#[derive(Debug, Default)]
#[repr(C)]
pub struct ValueRange {
pub min: f32,
pub max: f32,
}
#[derive(Debug, Default)]
#[repr(C)]
pub struct ValueRangeInt {
pub min: isize,
pub max: isize,
}

View file

@ -0,0 +1,459 @@
use crate::{
heap,
noita::types::{
BitSet, CString, Component, Entity, EntityManager, StdMap, StdString, StdVec, TagManager,
},
};
use std::ptr;
#[repr(C)]
#[derive(Debug)]
pub struct ComponentData {
pub vtable: &'static ComponentVTable,
pub local_id: usize,
pub type_name: CString,
pub type_id: usize,
pub id: usize,
pub enabled: bool,
unk2: [u8; 3],
pub tags: BitSet<8>,
unk3: StdVec<usize>,
unk4: usize,
}
impl Default for ComponentData {
fn default() -> Self {
Self {
vtable: &ComponentVTable {},
local_id: 0,
type_name: CString(ptr::null()),
type_id: 0,
id: 0,
enabled: false,
unk2: [0; 3],
tags: Default::default(),
unk3: StdVec::null(),
unk4: 0,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentVTable {
//TODO should be a union
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentBufferVTable {
//TODO should be a union
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct ComponentTypeManager {
pub next_id: usize,
pub component_buffer_indices: StdMap<StdString, usize>,
}
impl ComponentTypeManager {
pub fn get<C: Component>(&self, entity_manager: &EntityManager) -> &ComponentBuffer {
let index = self
.component_buffer_indices
.get(C::STD_NAME)
.copied()
.unwrap();
let mgr = entity_manager.component_buffers.get(index).unwrap();
unsafe { mgr.as_ref() }.unwrap()
}
pub fn get_mut<C: Component>(
&mut self,
entity_manager: &mut EntityManager,
) -> &mut ComponentBuffer {
let index = self
.component_buffer_indices
.get(C::STD_NAME)
.copied()
.unwrap();
let mgr = entity_manager.component_buffers.get(index).unwrap();
unsafe { mgr.as_mut() }.unwrap()
}
}
#[test]
fn test_com_create() {
let mut em = EntityManager::default();
let cm = &mut ComponentTypeManager::default();
let id = &mut 0;
{
let mut com_buffer = ComponentBuffer::default();
let com = &mut com_buffer as *mut _;
em.component_buffers.push(com);
cm.component_buffer_indices
.insert(StdString::from_str("WalletComponent"), 0);
}
let ent = em.create();
{
em.create_component::<crate::noita::types::WalletComponent>(ent, id, cm);
em.create_component::<crate::noita::types::WalletComponent>(ent, id, cm);
em.create_component::<crate::noita::types::WalletComponent>(ent, id, cm);
em.create_component::<crate::noita::types::WalletComponent>(ent, id, cm);
}
let mut coms = em.iter_components::<crate::noita::types::WalletComponent>(ent.entry, cm);
assert_eq!(coms.next().unwrap().base.local_id, 0);
assert_eq!(coms.next().unwrap().base.local_id, 1);
assert_eq!(coms.next().unwrap().base.local_id, 2);
assert_eq!(coms.next().unwrap().base.local_id, 3);
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentBuffer {
pub vtable: &'static ComponentBufferVTable,
pub end: usize,
unk: [isize; 2],
pub entity_entry: StdVec<usize>,
pub entities: StdVec<*mut Entity>,
pub prev: StdVec<usize>,
pub next: StdVec<usize>,
pub component_list: StdVec<*mut ComponentData>,
}
impl Default for ComponentBuffer {
fn default() -> Self {
Self {
vtable: &ComponentBufferVTable {},
end: (-1isize).cast_unsigned(),
unk: [0, 0],
entity_entry: Default::default(),
entities: Default::default(),
prev: Default::default(),
next: Default::default(),
component_list: Default::default(),
}
}
}
impl ComponentBuffer {
pub fn create<C: Component>(
&mut self,
entity: &mut Entity,
id: usize,
type_id: usize,
) -> &'static mut C {
let com = C::default(ComponentData {
vtable: C::VTABLE,
local_id: self.component_list.len(),
type_name: C::C_NAME,
type_id,
id,
enabled: false,
unk2: [0; 3],
tags: Default::default(),
unk3: StdVec::null(),
unk4: 0,
});
let com = heap::place_new(com);
let index = self.component_list.len();
self.component_list.push((com as *mut C).cast());
if self.entities.len() > index {
self.entities[index] = entity;
} else {
while self.entities.len() < index {
self.entities.push(ptr::null_mut())
}
self.entities.push(entity);
}
while self.entity_entry.len() <= entity.entry {
self.entity_entry.push(self.end)
}
let mut off;
let mut last = self.end;
if let Some(e) = self.entity_entry.get(entity.entry).copied()
&& e != self.end
{
off = e;
while let Some(next) = self.next.get(off).copied() {
last = off;
if next == self.end {
break;
}
off = next;
}
while self.next.len() <= index {
self.next.push(self.end)
}
self.next[off] = index;
while self.prev.len() <= index {
self.prev.push(self.end)
}
self.prev[index] = last;
} else {
off = index;
self.entity_entry[entity.entry] = off;
while self.next.len() <= index {
self.next.push(self.end)
}
self.next[off] = self.end;
}
unsafe { &mut *com }
}
pub fn iter_components(&self, entry: usize) -> ComponentIter {
if let Some(off) = self.entity_entry.get(entry) {
ComponentIter {
component_list: self.component_list.copy(),
off: *off,
next: self.next.copy(),
prev: self.prev.copy(),
end: self.end,
}
} else {
ComponentIter {
component_list: StdVec::null(),
off: 0,
next: StdVec::null(),
prev: StdVec::null(),
end: 0,
}
}
}
pub fn iter_components_mut(&mut self, entry: usize) -> ComponentIterMut {
if let Some(off) = self.entity_entry.get(entry) {
ComponentIterMut {
component_list: self.component_list.copy(),
off: *off,
next: self.next.copy(),
prev: self.prev.copy(),
end: self.end,
}
} else {
ComponentIterMut {
component_list: StdVec::null(),
off: 0,
next: StdVec::null(),
prev: StdVec::null(),
end: 0,
}
}
}
pub fn iter_every_component(&self) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.component_list
.as_ref()
.iter()
.filter_map(|c| unsafe { c.as_ref() })
}
pub fn iter_every_component_mut(
&mut self,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.component_list
.as_mut()
.iter_mut()
.filter_map(|c| unsafe { c.as_mut() })
}
pub fn iter_components_with_tag(
&self,
tag_manager: &TagManager<u8>,
entry: usize,
tag: &StdString,
) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.iter_components(entry)
.filter(|c| c.tags.has_tag(tag_manager, tag))
}
pub fn iter_components_with_tag_mut(
&mut self,
tag_manager: &TagManager<u8>,
entry: usize,
tag: &StdString,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.iter_components_mut(entry)
.filter(|c| c.tags.has_tag(tag_manager, tag))
}
pub fn iter_enabled_components(
&self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.iter_components(entry).filter(|c| c.enabled)
}
pub fn iter_disabled_components(
&self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.iter_components(entry).filter(|c| !c.enabled)
}
pub fn iter_enabled_components_mut(
&mut self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.iter_components_mut(entry).filter(|c| c.enabled)
}
pub fn iter_disabled_components_mut(
&mut self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.iter_components_mut(entry).filter(|c| !c.enabled)
}
pub fn get_first(&self, entry: usize) -> Option<&'static ComponentData> {
self.iter_components(entry).next()
}
pub fn get_first_mut(&mut self, entry: usize) -> Option<&'static mut ComponentData> {
self.iter_components_mut(entry).next()
}
pub fn get_first_enabled(&self, entry: usize) -> Option<&'static ComponentData> {
self.iter_enabled_components(entry).next()
}
pub fn get_first_disabled(&self, entry: usize) -> Option<&'static ComponentData> {
self.iter_disabled_components(entry).next()
}
pub fn get_first_enabled_mut(&mut self, entry: usize) -> Option<&'static mut ComponentData> {
self.iter_enabled_components_mut(entry).next()
}
pub fn get_first_disabled_mut(&mut self, entry: usize) -> Option<&'static mut ComponentData> {
self.iter_disabled_components_mut(entry).next()
}
}
#[derive(Debug)]
pub struct ComponentIter {
component_list: StdVec<*mut ComponentData>,
off: usize,
end: usize,
next: StdVec<usize>,
prev: StdVec<usize>,
}
impl Iterator for ComponentIter {
type Item = &'static ComponentData;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
if self.off == self.end {
return None;
}
let com = self.component_list.get(self.off)?.as_ref();
self.off = *self.next.get(self.off)?;
com
}
}
}
impl DoubleEndedIterator for ComponentIter {
fn next_back(&mut self) -> Option<Self::Item> {
unsafe {
if self.off == self.end {
return None;
}
let com = self.component_list.get(self.off)?.as_ref();
self.off = *self.prev.get(self.off)?;
com
}
}
}
#[derive(Debug)]
pub struct ComponentIterMut {
component_list: StdVec<*mut ComponentData>,
off: usize,
end: usize,
next: StdVec<usize>,
prev: StdVec<usize>,
}
impl Iterator for ComponentIterMut {
type Item = &'static mut ComponentData;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
if self.off == self.end {
return None;
}
let com = self.component_list.get(self.off)?.as_mut();
self.off = *self.next.get(self.off)?;
com
}
}
}
impl DoubleEndedIterator for ComponentIterMut {
fn next_back(&mut self) -> Option<Self::Item> {
unsafe {
if self.off == self.end {
return None;
}
let com = self.component_list.get(self.off)?.as_mut();
self.off = *self.prev.get(self.off)?;
com
}
}
}
impl BitSet<8> {
pub fn get(&self, n: u8) -> bool {
let out_index = n / 32;
let in_index = n % 32;
self.0[out_index as usize] & (1 << in_index) != 0
}
pub fn set(&mut self, n: u8, value: bool) {
let out_index = n / 32;
let in_index = n % 32;
if value {
self.0[out_index as usize] |= 1 << in_index
} else {
self.0[out_index as usize] &= !(1 << in_index)
}
}
pub fn count(&self) -> usize {
let mut n = 0;
for s in self.0 {
n += s.count_ones()
}
n as usize
}
pub fn has_tag(&self, tag_manager: &TagManager<u8>, tag: &StdString) -> bool {
if let Some(n) = tag_manager.tag_indices.get(tag) {
self.get(*n)
} else {
false
}
}
pub fn add_tag(&mut self, tag_manager: &mut TagManager<u8>, tag: &StdString) {
if let Some(n) = tag_manager.tag_indices.get(tag) {
self.set(*n, true)
}
//TODO
}
pub fn remove_tag(&mut self, tag_manager: &TagManager<u8>, tag: &StdString) {
if let Some(n) = tag_manager.tag_indices.get(tag) {
self.set(*n, false)
}
}
pub fn get_tags(
&self,
tag_manager: &TagManager<u8>,
) -> impl Iterator<Item = &'static StdString> {
tag_manager
.tag_indices
.iter()
.filter_map(|(a, b)| if self.get(*b) { Some(a) } else { None })
}
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentSystemManager {
pub update_order: StdVec<&'static ComponentSystem>,
pub component_updaters: StdVec<&'static ComponentUpdater>,
pub component_vtables: StdMap<StdString, ComponentLuaVTable>,
pub unk: [*const usize; 8],
pub unk2: StdVec<*const usize>,
pub unk3: [*const usize; 7],
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentLuaVTable {
unk: [*const usize; 16],
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentUpdater {
pub vtable: &'static ComponentUpdaterVTable,
pub name: StdString,
pub unk: [*const usize; 8],
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentUpdaterVTable {}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentSystem {
pub vtable: &'static ComponentSystemVTable,
pub unk: [*const usize; 2],
pub name: StdString,
}
#[repr(C)]
#[derive(Debug)]
pub struct ComponentSystemVTable {}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,824 @@
use crate::heap;
use crate::noita::types::component::{ComponentBuffer, ComponentData};
use crate::noita::types::{
Component, ComponentTypeManager, Inventory2Component, StdMap, StdString, StdVec, Vec2,
};
use std::{mem, slice};
impl EntityManager {
pub fn create(&mut self) -> &'static mut Entity {
self.max_entity_id += 1;
let ent = Entity {
id: self.max_entity_id,
entry: 0,
filename_index: 0,
kill_flag: false,
padding: [0; 3],
unknown1: 0,
name: StdString::default(),
unknown2: 0,
tags: BitSet::default(),
transform: Transform::default(),
children: std::ptr::null_mut(),
parent: std::ptr::null_mut(),
};
let ent = heap::place_new_ref(ent);
if let Some(entry) = self.free_ids.pop() {
ent.entry = entry;
self.entities[entry] = ent;
} else {
ent.entry = self.entities.len();
self.entities.push(ent);
}
ent
}
pub fn set_all_components(&mut self, ent: &mut Entity, enabled: bool) {
self.iter_component_buffers_mut().for_each(|c| {
c.iter_components_mut(ent.entry)
.for_each(|c| c.enabled = enabled)
})
}
pub fn set_components<C: Component>(
&mut self,
component_type_manager: &mut ComponentTypeManager,
ent: &mut Entity,
enabled: bool,
) {
component_type_manager
.get_mut::<C>(self)
.iter_components_mut(ent.entry)
.for_each(|c| c.enabled = enabled)
}
pub fn set_all_components_with_tag(
&mut self,
tag_manager: &TagManager<u8>,
ent: &mut Entity,
tag: &StdString,
enabled: bool,
) {
self.iter_component_buffers_mut().for_each(|c| {
c.iter_components_mut(ent.entry)
.filter(|c| c.tags.has_tag(tag_manager, tag))
.for_each(|c| c.enabled = enabled)
})
}
pub fn set_components_with_tag<C: Component>(
&mut self,
component_type_manager: &mut ComponentTypeManager,
tag_manager: &TagManager<u8>,
ent: &mut Entity,
tag: &StdString,
enabled: bool,
) {
component_type_manager
.get_mut::<C>(self)
.iter_components_mut(ent.entry)
.filter(|c| c.tags.has_tag(tag_manager, tag))
.for_each(|c| c.enabled = enabled)
}
pub fn get_entities_with_tag(
&self,
tag: &StdString,
tag_manager: &TagManager<u16>,
) -> impl DoubleEndedIterator<Item = &'static Entity> {
let n = *tag_manager.tag_indices.get(tag).unwrap();
self.entity_buckets
.get(n as usize)
.unwrap()
.as_ref()
.iter()
.filter_map(|e| unsafe { e.as_ref() })
}
pub fn get_entities_with_tag_mut(
&mut self,
tag_manager: &TagManager<u16>,
tag: &StdString,
) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
let n = *tag_manager.tag_indices.get(tag).unwrap();
self.entity_buckets
.get_mut(n as usize)
.unwrap()
.as_mut()
.iter_mut()
.filter_map(|e| unsafe { e.as_mut() })
}
pub fn get_entity_with_name(&self, name: StdString) -> Option<&'static Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|a| unsafe { a.as_ref() })
.find(|e| e.name == name)
}
pub fn get_entity_with_name_mut(&mut self, name: StdString) -> Option<&'static mut Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|a| unsafe { a.as_mut() })
.find(|e| e.name == name)
}
pub fn get_entity(&self, id: usize) -> Option<&'static Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|c| unsafe { c.as_ref() })
.find(|ent| ent.id == id)
}
pub fn get_entity_mut(&mut self, id: usize) -> Option<&'static mut Entity> {
self.entities
.as_mut()
.iter_mut()
.filter_map(|c| unsafe { c.as_mut() })
.find(|ent| ent.id == id)
}
pub fn iter_entities_with_tag(
&self,
tag_manager: &TagManager<u16>,
tag: &StdString,
) -> impl DoubleEndedIterator<Item = &'static Entity> {
unsafe {
if let Some(n) = tag_manager.tag_indices.get(tag).copied()
&& let Some(v) = self.entity_buckets.get(n as usize)
{
v.as_ref()
} else {
&[]
}
.iter()
.filter_map(|e| e.as_ref())
}
}
pub fn iter_entities_with_tag_mut(
&mut self,
tag_manager: &TagManager<u16>,
tag: &StdString,
) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
unsafe {
if let Some(n) = tag_manager.tag_indices.get(tag).copied()
&& let Some(v) = self.entity_buckets.get_mut(n as usize)
{
v.as_mut()
} else {
&mut []
}
.iter_mut()
.filter_map(|e| e.as_mut())
}
}
pub fn iter_entities(&self) -> impl DoubleEndedIterator<Item = &'static Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|c| unsafe { c.as_ref() })
}
pub fn iter_entities_mut(&mut self) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
self.entities
.as_mut()
.iter_mut()
.filter_map(|c| unsafe { c.as_mut() })
}
pub fn iter_component_buffers(
&self,
) -> impl DoubleEndedIterator<Item = &'static ComponentBuffer> {
self.component_buffers
.as_ref()
.iter()
.filter_map(|c| unsafe { c.as_ref() })
}
pub fn iter_component_buffers_mut(
&mut self,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentBuffer> {
self.component_buffers
.as_mut()
.iter_mut()
.filter_map(|c| unsafe { c.as_mut() })
}
pub fn iter_components<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static C> {
let index = component_type_manager
.component_buffer_indices
.get(C::STD_NAME)
.copied()
.unwrap();
let mgr = self.component_buffers.get(index).unwrap();
unsafe { mgr.as_ref() }
.unwrap()
.iter_components(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_components_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static mut C> {
component_type_manager
.get_mut::<C>(self)
.iter_components_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_enabled_components<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static C> {
component_type_manager
.get::<C>(self)
.iter_enabled_components(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_enabled_components_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static mut C> {
component_type_manager
.get_mut::<C>(self)
.iter_enabled_components_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_disabled_components<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static C> {
component_type_manager
.get::<C>(self)
.iter_disabled_components(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_disabled_components_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> impl DoubleEndedIterator<Item = &'static mut C> {
component_type_manager
.get_mut::<C>(self)
.iter_disabled_components_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
#[allow(clippy::mut_from_ref)]
pub fn create_component<C: Component + 'static>(
&self,
entity: &mut Entity,
max_component: &mut usize,
component_type_manager: &mut ComponentTypeManager,
) -> &mut C {
let index = component_type_manager
.component_buffer_indices
.get(C::STD_NAME)
.copied()
.unwrap();
let mgr = self.component_buffers.get(index).unwrap();
let com = unsafe { mgr.as_mut() }
.unwrap()
.create::<C>(entity, *max_component, index);
*max_component += 1;
com
}
pub fn get_component_buffer<'a, C: Component + 'static>(
&self,
component_type_manager: &'a ComponentTypeManager,
) -> &'a ComponentBuffer {
//TODO this needs to deal with when it does not exist
component_type_manager.get::<C>(self)
}
pub fn get_component_buffer_mut<'a, C: Component + 'static>(
&mut self,
component_type_manager: &'a mut ComponentTypeManager,
) -> &'a mut ComponentBuffer {
//TODO this needs to deal with when it does not exist
component_type_manager.get_mut::<C>(self)
}
pub fn get_first_component<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> Option<&'static C> {
component_type_manager
.get::<C>(self)
.get_first(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn get_first_component_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> Option<&'static mut C> {
component_type_manager
.get_mut::<C>(self)
.get_first_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn get_first_enabled_component<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> Option<&'static C> {
component_type_manager
.get::<C>(self)
.get_first_enabled(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn get_first_enabled_component_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> Option<&'static mut C> {
component_type_manager
.get_mut::<C>(self)
.get_first_enabled_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn get_first_disabled_component<C: Component + 'static>(
&self,
entry: usize,
component_type_manager: &ComponentTypeManager,
) -> Option<&'static C> {
component_type_manager
.get::<C>(self)
.get_first_disabled(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn get_first_disabled_component_mut<C: Component + 'static>(
&mut self,
entry: usize,
component_type_manager: &mut ComponentTypeManager,
) -> Option<&'static mut C> {
component_type_manager
.get_mut::<C>(self)
.get_first_disabled_mut(entry)
.map(|c| unsafe { mem::transmute(c) })
}
pub fn iter_every_component(&self) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.iter_component_buffers()
.flat_map(move |c| c.iter_every_component())
}
pub fn iter_every_component_mut(
&mut self,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.iter_component_buffers_mut()
.flat_map(move |c| c.iter_every_component_mut())
}
pub fn iter_all_components(
&self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static ComponentData> {
self.iter_component_buffers()
.flat_map(move |c| c.iter_components(entry))
}
pub fn iter_all_components_mut(
&mut self,
entry: usize,
) -> impl DoubleEndedIterator<Item = &'static mut ComponentData> {
self.iter_component_buffers_mut()
.flat_map(move |c| c.iter_components_mut(entry))
}
pub fn iter_in_radius(
&self,
pos: Vec2,
radius: f32,
) -> impl DoubleEndedIterator<Item = &'static Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|e| unsafe { e.as_ref() })
.filter(move |e| pos.abs2(&e.transform.pos) < radius * radius)
}
pub fn iter_in_radius_with_tag(
&self,
pos: Vec2,
radius: f32,
tag: &StdString,
tag_manager: &TagManager<u16>,
) -> impl DoubleEndedIterator<Item = &'static Entity> {
if let Some(tag) = tag_manager.tag_indices.get(tag).copied()
&& let Some(ents) = self.entity_buckets.get(tag as usize)
{
ents.as_ref()
} else {
&[]
}
.iter()
.filter_map(|e| unsafe { e.as_ref() })
.filter(move |e| pos.abs2(&e.transform.pos) < radius * radius)
}
pub fn iter_in_radius_mut(
&mut self,
pos: Vec2,
radius: f32,
) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
self.entities
.as_mut()
.iter_mut()
.filter_map(|e| unsafe { e.as_mut() })
.filter(move |e| pos.abs2(&e.transform.pos) < radius * radius)
}
pub fn iter_in_radius_with_tag_mut(
&mut self,
pos: Vec2,
radius: f32,
tag: &StdString,
tag_manager: &TagManager<u16>,
) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
if let Some(tag) = tag_manager.tag_indices.get(tag).copied()
&& let Some(ents) = self.entity_buckets.get_mut(tag as usize)
{
ents.as_mut()
} else {
&mut []
}
.iter_mut()
.filter_map(|e| unsafe { e.as_mut() })
.filter(move |e| pos.abs2(&e.transform.pos) < radius * radius)
}
pub fn get_with_name(&self, name: StdString) -> Option<&'static Entity> {
self.entities.as_ref().iter().find_map(|e| {
unsafe { e.as_ref() }.and_then(|e| if e.name == name { Some(e) } else { None })
})
}
pub fn get_closest(&self, pos: Vec2) -> Option<&'static Entity> {
self.entities
.as_ref()
.iter()
.filter_map(|e| unsafe { e.as_ref().map(|e| (pos.abs2(&e.transform.pos), e)) })
.min_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, e)| e)
}
pub fn get_closest_with_tag(
&self,
pos: Vec2,
tag: &StdString,
tag_manager: &TagManager<u16>,
) -> Option<&'static Entity> {
tag_manager.tag_indices.get(tag).copied().and_then(|tag| {
self.entity_buckets.get(tag as usize).and_then(|b| {
b.as_ref()
.iter()
.filter_map(|e| unsafe { e.as_ref().map(|e| (pos.abs2(&e.transform.pos), e)) })
.min_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, e)| e)
})
})
}
pub fn get_with_name_mut(&mut self, name: StdString) -> Option<&'static mut Entity> {
self.entities.as_mut().iter_mut().find_map(|e| {
unsafe { e.as_mut() }.and_then(|e| if e.name == name { Some(e) } else { None })
})
}
pub fn get_closest_mut(&mut self, pos: Vec2) -> Option<&'static mut Entity> {
self.entities
.as_mut()
.iter_mut()
.filter_map(|e| unsafe { e.as_mut().map(|e| (pos.abs2(&e.transform.pos), e)) })
.min_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, e)| e)
}
pub fn get_closest_with_tag_mut(
&mut self,
pos: Vec2,
tag: &StdString,
tag_manager: &TagManager<u16>,
) -> Option<&'static mut Entity> {
tag_manager.tag_indices.get(tag).copied().and_then(|tag| {
self.entity_buckets.get_mut(tag as usize).and_then(|b| {
b.as_mut()
.iter_mut()
.filter_map(|e| unsafe { e.as_mut().map(|e| (pos.abs2(&e.transform.pos), e)) })
.min_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, e)| e)
})
})
}
}
#[repr(C)]
#[derive(Debug)]
pub struct BitSet<const N: usize>(pub [isize; N]);
impl BitSet<16> {
pub fn get(&self, n: u16) -> bool {
let out_index = n / 32;
let in_index = n % 32;
self.0[out_index as usize] & (1 << in_index) != 0
}
pub fn set(&mut self, n: u16, value: bool) {
let out_index = n / 32;
let in_index = n % 32;
if value {
self.0[out_index as usize] |= 1 << in_index
} else {
self.0[out_index as usize] &= !(1 << in_index)
}
}
pub fn count(&self) -> usize {
let mut n = 0;
for s in self.0 {
n += s.count_ones()
}
n as usize
}
pub fn has_tag(&'static self, tag_manager: &TagManager<u16>, tag: &StdString) -> bool {
if let Some(n) = tag_manager.tag_indices.get(tag) {
self.get(*n)
} else {
false
}
}
pub fn get_tags(
&'static self,
tag_manager: &TagManager<u16>,
) -> impl Iterator<Item = &'static StdString> {
tag_manager
.tag_indices
.iter()
.filter_map(|(a, b)| if self.get(*b) { Some(a) } else { None })
}
}
impl<const N: usize> Default for BitSet<N> {
fn default() -> Self {
Self([0; N])
}
}
#[repr(C)]
#[derive(Debug)]
pub struct Entity {
pub id: usize,
pub entry: usize,
pub filename_index: usize,
pub kill_flag: bool,
padding: [u8; 3],
unknown1: isize,
pub name: StdString,
unknown2: isize,
pub tags: BitSet<16>,
pub transform: Transform,
pub children: *mut StdVec<*mut Entity>,
pub parent: *mut Entity,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct Transform {
pub pos: Vec2,
pub angle: Vec2,
pub rot90: Vec2,
pub scale: Vec2,
}
impl Entity {
pub fn kill(&mut self) {
self.kill_flag = true;
self.iter_children_mut().for_each(|e| e.kill());
}
pub fn kill_safe(&mut self, inventory: &mut Inventory) {
if inventory.wand_pickup == self {
inventory.wand_pickup = std::ptr::null_mut();
inventory.pickup_state = 0;
}
self.kill();
}
pub fn iter_children(&self) -> impl DoubleEndedIterator<Item = &'static Entity> {
unsafe {
if let Some(child) = self.children.as_ref() {
let len = child.end.offset_from(child.start);
slice::from_raw_parts(child.start, len as usize)
} else {
&[]
}
.iter()
.filter_map(|e| e.as_ref())
}
}
pub fn iter_children_mut(&mut self) -> impl DoubleEndedIterator<Item = &'static mut Entity> {
unsafe {
if let Some(child) = self.children.as_ref() {
let len = child.end.offset_from(child.start);
slice::from_raw_parts(child.start, len as usize)
} else {
&[]
}
.iter()
.filter_map(|e| e.as_mut())
}
}
pub fn iter_descendants(&'static self) -> impl Iterator<Item = &'static Entity> {
DescendantIter {
entitys: self.iter_children().rev().collect(),
}
}
pub fn iter_descendants_mut(&'static mut self) -> impl Iterator<Item = &'static mut Entity> {
DescendantIterMut {
entitys: self.iter_children_mut().rev().collect(),
}
}
pub fn parent(&self) -> Option<&'static Entity> {
unsafe { self.parent.as_ref() }
}
pub fn parent_mut(&mut self) -> Option<&'static mut Entity> {
unsafe { self.parent.as_mut() }
}
pub fn iter_ancestors(&'static self) -> impl Iterator<Item = &'static Entity> {
AncestorIter {
current: Some(self),
}
}
pub fn iter_ancestors_mut(&'static mut self) -> impl Iterator<Item = &'static mut Entity> {
AncestorIterMut {
current: Some(self),
}
}
pub fn root(&'static self) -> &'static Entity {
if let Some(ent) = self.iter_ancestors().last() {
ent
} else {
self
}
}
pub fn root_mut(&'static mut self) -> &'static mut Entity {
if self.parent.is_null() {
self
} else {
self.iter_ancestors_mut().last().unwrap()
}
}
pub fn add_tag(
&'static mut self,
tag_manager: &mut TagManager<u16>,
entity_manager: &mut EntityManager,
tag: &StdString,
) {
if let Some(n) = tag_manager.tag_indices.get(tag).copied()
&& !self.tags.get(n)
{
entity_manager
.entity_buckets
.get_mut(n as usize)
.unwrap()
.push(self);
self.tags.set(n, true)
}
//TODO add tag if does not exist
}
pub fn remove_tag(
&'static mut self,
tag_manager: &TagManager<u16>,
entity_manager: &mut EntityManager,
tag: &StdString,
) {
if let Some(n) = tag_manager.tag_indices.get(tag).copied()
&& self.tags.get(n)
{
let v = entity_manager.entity_buckets.get_mut(n as usize).unwrap();
let Some(i) = v
.as_ref()
.iter()
.position(|c| unsafe { c.as_ref() }.map(|c| c.id) == Some(self.id))
else {
unreachable!()
};
v.remove(i);
self.tags.set(n, false)
}
}
}
#[derive(Debug)]
pub struct AncestorIter {
current: Option<&'static Entity>,
}
impl Iterator for AncestorIter {
type Item = &'static Entity;
fn next(&mut self) -> Option<Self::Item> {
if let Some(current) = self.current {
self.current = current.parent();
self.current
} else {
None
}
}
}
#[derive(Debug)]
pub struct AncestorIterMut {
current: Option<&'static mut Entity>,
}
impl Iterator for AncestorIterMut {
type Item = &'static mut Entity;
fn next(&mut self) -> Option<Self::Item> {
if let Some(current) = self.current.take() {
self.current = unsafe { current.parent.as_mut() };
unsafe { current.parent.as_mut() }
} else {
None
}
}
}
#[derive(Debug)]
pub struct DescendantIter {
entitys: Vec<&'static Entity>,
}
impl Iterator for DescendantIter {
type Item = &'static Entity;
fn next(&mut self) -> Option<Self::Item> {
if let Some(ent) = self.entitys.pop() {
self.entitys.extend(ent.iter_children().rev());
Some(ent)
} else {
None
}
}
}
#[derive(Debug)]
pub struct DescendantIterMut {
entitys: Vec<&'static mut Entity>,
}
impl Iterator for DescendantIterMut {
type Item = &'static mut Entity;
fn next(&mut self) -> Option<Self::Item> {
if let Some(ent) = self.entitys.pop() {
self.entitys.extend(ent.iter_children_mut().rev());
Some(ent)
} else {
None
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct EntityManager {
pub vtable: &'static EntityManagerVTable,
pub max_entity_id: usize,
pub free_ids: StdVec<usize>,
pub entities: StdVec<*mut Entity>,
pub entity_buckets: StdVec<StdVec<*mut Entity>>,
pub component_buffers: StdVec<*mut ComponentBuffer>,
pub unk: usize,
}
impl Default for EntityManager {
fn default() -> Self {
Self {
vtable: &EntityManagerVTable {},
max_entity_id: 0,
free_ids: Default::default(),
entities: Default::default(),
entity_buckets: Default::default(),
component_buffers: Default::default(),
unk: 0,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct EntityManagerVTable {
//TODO
}
#[repr(C)]
#[derive(Debug)]
pub struct TagManager<T: 'static> {
pub tags: StdVec<StdString>,
pub tag_indices: StdMap<StdString, T>,
pub max_tag_count: usize,
pub name: StdString,
}
#[repr(C)]
#[derive(Debug)]
pub struct SpriteStainSystem {}
#[repr(C)]
#[derive(Debug, Default)]
pub enum GameEffect {
#[default]
None = 0,
}
#[repr(C)]
#[derive(Debug)]
pub struct Inventory {
pub entity: *mut Entity,
pub inventory_quick: *mut Entity,
pub inventory_full: *mut Entity,
pub held_item_id: usize,
pub switch_item_id: isize,
pub inventory_component: *mut Inventory2Component,
unk7b1: bool,
pub item_placed: bool,
unk7b3: bool,
padding: u8,
pub item_in_pickup_range: bool,
padding2: u8,
padding3: u8,
padding4: u8,
pub is_in_inventory: bool,
unk9b2: bool,
pub is_dragging: bool,
padding5: u8,
unk10: StdVec<isize>,
pub pickup_state: usize,
pub wand_pickup: *mut Entity,
pub animation_state: usize,
unk15: StdVec<[isize; 18]>,
}

View file

@ -0,0 +1,129 @@
use crate::lua::LuaState;
use crate::noita::types::{StdMap, StdString, StdVec, Vec2};
#[derive(Debug)]
pub struct GlobalStatsVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct GlobalStats {
pub vftable: &'static GlobalStatsVTable,
pub stats_version: usize,
pub debug_tracker: usize,
pub debug: bool,
padding1: [u8; 3],
pub debug_reset_counter: usize,
pub fix_stats_flag: bool,
pub session_dead: bool,
padding2: [u8; 2],
pub key_value_stats: StdMap<StdString, usize>,
pub session: GameStats,
pub highest: GameStats,
pub global: GameStats,
pub prev_best: GameStats,
}
#[derive(Debug)]
pub struct GameStatsVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct GameStats {
pub vftable: &'static GameStatsVTable,
pub dead: bool,
padding1: [u8; 3],
pub death_count: usize,
pub streaks: usize,
pub world_seed: usize,
pub killed_by: StdString,
pub killed_by_extra: StdString,
pub death_pos: Vec2,
field_0x4c: usize,
pub playtime: f64,
pub playtime_str: StdString,
pub places_visited: usize,
pub enemies_killed: usize,
pub heart_containers: usize,
field_0x7c: usize,
pub hp: i64,
pub gold: i64,
pub gold_all: i64,
pub gold_infinite: bool,
padding2: [u8; 3],
pub items: usize,
pub projectiles_shot: usize,
pub kicks: usize,
pub damage_taken: f64,
pub healed: f64,
pub teleports: usize,
pub wands_edited: usize,
pub biomes_visited_with_wands: usize,
field_0xc4: usize,
}
#[derive(Debug)]
pub struct TranslationManagerVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct TranslationManager {
pub vftable: &'static TranslationManagerVTable,
pub unknown_strings: StdVec<StdString>,
pub languages: StdVec<Language>,
pub key_to_index: StdMap<StdString, usize>,
pub extra_lang_files: StdVec<StdString>,
pub current_lang_idx: usize,
pub unknown: usize,
pub unknown_float: f32,
pub unknown_primitive_vec: StdVec<usize>,
pub unknown_map: StdMap<StdString, StdString>,
}
#[derive(Debug)]
#[repr(C)]
pub struct Language {
pub id: StdString,
pub name: StdString,
pub font_default: StdString,
pub font_inventory_title: StdString,
pub font_important_message_title: StdString,
pub font_world_space_message: StdString,
pub fonts_utf8: bool,
pub fonts_pixel_font: bool,
padding1: [u8; 2],
pub fonts_dpi: f32,
pub ui_wand_info_offset1: f32,
pub ui_wand_info_offset2: f32,
pub ui_action_info_offset2: f32,
pub ui_configurecontrols_offset2: f32,
pub strings: StdVec<StdString>,
}
#[derive(Debug)]
#[repr(C)]
pub struct ModListEntry {
pub name: StdString,
pub steam_id: usize,
unk1: [u8; 4],
pub enabled: bool,
unk1_bool: bool,
unk2_bool: bool,
unk2: u8,
unk3: [u8; 4],
}
#[derive(Debug)]
#[repr(C)]
pub struct Mods {
pub names: StdVec<ModListEntry>,
pub list: StdVec<Mod>,
}
#[derive(Debug)]
#[repr(C)]
pub struct ModVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct Mod {
unk: [usize; 14],
pub lua_data: &'static ModLua,
pub vtable: &'static ModVTable,
unk2: [usize; 8],
}
#[derive(Debug)]
#[repr(C)]
pub struct ModLua {
unk: [usize; 14],
pub lua_state: *const LuaState,
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,215 @@
use crate::noita::types::{StdString, StdVec, Vec2};
use std::ffi::c_void;
#[derive(Debug)]
pub struct PlatformVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct Platform {
pub vftable: &'static PlatformVTable,
pub application: *const c_void,
pub app_config: &'static WizardAppConfig,
pub internal_width: f32,
pub internal_height: f32,
pub input_disabled: bool,
padding1: [u8; 3],
pub graphics: *const c_void,
pub fixed_time_step: bool,
padding2: [u8; 3],
pub frame_count: isize,
pub frame_rate: isize,
pub last_frame_execution_time: f64,
pub average_frame_execution_time: f64,
pub one_frame_should_last: f64,
pub time_elapsed_tracker: f64,
pub width: isize,
pub height: isize,
pub event_recorder: *const c_void,
pub mouse: *const c_void,
pub keyboard: *const c_void,
pub touch: *const c_void,
pub joysticks: StdVec<*const c_void>,
pub sound_player: *const c_void,
pub file_system: *const c_void,
pub running: bool,
padding3: [u8; 3],
pub mouse_pos: Vec2,
pub sleeping_mode: isize,
pub print_framerate: bool,
padding4: [u8; 3],
pub working_dir: StdString,
pub random_i: isize,
pub random_seed: isize,
pub joysticks_enabled: bool,
padding5: [u8; 3],
}
#[derive(Debug)]
pub struct AppConfigVTable {}
#[derive(Debug)]
#[repr(C)]
pub struct AppConfig {
pub vftable: &'static AppConfigVTable,
pub internal_size_w: u32,
pub internal_size_h: u32,
pub framerate: u32,
pub iphone_is_landscape: bool,
pub sounds: bool,
pub event_recorder_flush_every_frame: bool,
pub record_events: bool,
pub do_a_playback: bool,
padding1: [u8; 3],
pub playback_file: StdString,
pub report_fps: bool,
pub joysticks_enabled: bool,
padding2: [u8; 2],
pub joystick_rumble_intensity: f32,
pub graphics_settings: GraphicsSettings,
pub set_random_seed_cb: *const c_void,
}
#[derive(Debug)]
#[repr(C)]
pub struct WizardAppConfig {
pub parent: AppConfig,
pub has_been_started_before: bool,
pub audio_fmod: bool,
padding1: [u8; 2],
pub audio_music_volume: f32,
pub audio_effects_volume: f32,
pub rendering_low_quality: bool,
pub rendering_low_resolution: bool,
pub rendering_pixel_art_antialiasing: bool,
padding2: u8,
pub rendering_brightness_delta: f32,
pub rendering_contrast_delta: f32,
pub rendering_gamma_delta: f32,
pub rendering_teleport_flash_brightness: f32,
pub rendering_cosmetic_particle_count_coeff: f32,
pub backbuffer_width: isize,
pub backbuffer_height: isize,
pub application_rendered_cursor: bool,
padding3: [u8; 3],
pub screenshake_intensity: f32,
pub ui_inventory_icons_always_clickable: bool,
pub ui_allow_shooting_while_inventory_open: bool,
pub ui_report_damage: bool,
pub ui_show_world_hover_info_next_to_mouse: bool,
pub replay_recorder_enabled: bool,
padding4: [u8; 3],
pub replay_recorder_max_budget_mb: u32,
pub replay_recorder_max_resolution_x: u32,
pub replay_recorder_max_resolution_y: u32,
pub language: StdString,
pub check_for_updates: bool,
padding5: [u8; 3],
pub last_started_game_version_hash: StdString,
pub config_format_version: u32,
pub is_default_config: bool,
padding6: [u8; 3],
pub keyboard_controls: ControlsConfig,
pub gamepad_controls: ControlsConfig,
pub gamepad_mode: isize,
pub rendering_filmgrain: bool,
pub online_features: bool,
padding7: [u8; 2],
pub steam_cloud_size_warning_limit_mb: f32,
pub _unknown_bool: bool,
pub mouse_capture_inside_window: bool,
pub ui_snappy_hover_boxes: bool,
pub application_pause_when_unfocused: bool,
pub gamepad_analog_flying: bool,
padding8: [u8; 3],
pub mods_active: StdString,
pub mods_active_privileged: StdString,
pub mods_sandbox_enabled: bool,
pub mods_sandbox_warning_done: bool,
pub mods_disclaimer_accepted: bool,
pub streaming_integration_autoconnect: bool,
pub streaming_integration_channel_name: StdString,
pub streaming_integration_events_per_vote: u32,
pub _unknown_streaming_number: f32,
pub streaming_integration_time_seconds_voting: f32,
pub streaming_integration_time_seconds_between_votings: f32,
pub streaming_integration_play_new_vote_sound: bool,
pub streaming_integration_viewernames_ghosts: bool,
pub streaming_integration_hide_votes_during_voting: bool,
pub streaming_integration_ui_pos_left: bool,
pub single_threaded_loading: bool,
padding9: [u8; 3],
pub _unknown_string: StdString,
pub debug_dont_load_other_config: bool,
padding10: [u8; 3],
}
#[derive(Debug)]
#[repr(C)]
pub struct ControlsConfigKey {
pub primary: isize,
pub secondary: isize,
pub primary_name: StdString,
pub secondary_name: StdString,
}
#[derive(Debug)]
#[repr(C)]
pub struct ControlsConfig {
pub key_up: ControlsConfigKey,
pub key_down: ControlsConfigKey,
pub key_left: ControlsConfigKey,
pub key_right: ControlsConfigKey,
pub key_use_wand: ControlsConfigKey,
pub key_spray_flask: ControlsConfigKey,
pub key_throw: ControlsConfigKey,
pub key_kick: ControlsConfigKey,
pub key_inventory: ControlsConfigKey,
pub key_interact: ControlsConfigKey,
pub key_drop_item: ControlsConfigKey,
pub key_drink_potion: ControlsConfigKey,
pub key_item_next: ControlsConfigKey,
pub key_item_prev: ControlsConfigKey,
pub key_item_slot1: ControlsConfigKey,
pub key_item_slot2: ControlsConfigKey,
pub key_item_slot3: ControlsConfigKey,
pub key_item_slot4: ControlsConfigKey,
pub key_item_slot5: ControlsConfigKey,
pub key_item_slot6: ControlsConfigKey,
pub key_item_slot7: ControlsConfigKey,
pub key_item_slot8: ControlsConfigKey,
pub key_item_slot9: ControlsConfigKey,
pub key_item_slot10: ControlsConfigKey,
pub key_takescreenshot: ControlsConfigKey,
pub key_replayedit_open: ControlsConfigKey,
pub aim_stick: ControlsConfigKey,
pub key_ui_confirm: ControlsConfigKey,
pub key_ui_drag: ControlsConfigKey,
pub key_ui_quick_drag: ControlsConfigKey,
pub gamepad_analog_sticks_threshold: f32,
pub gamepad_analog_buttons_threshold: f32,
}
#[repr(C)]
#[derive(Debug)]
pub enum VsyncMode {
Off,
On,
Adaptive,
}
#[repr(C)]
#[derive(Debug)]
pub enum FullscreenMode {
Windowed,
Stretched,
Full,
}
#[derive(Debug)]
#[repr(C)]
pub struct GraphicsSettings {
pub window_w: usize,
pub window_h: usize,
pub fullscreen: FullscreenMode,
pub caption: StdString,
pub icon_bmp: StdString,
pub textures_resize_to_power_of_two: bool,
pub textures_fix_alpha_channel: bool,
padding1: [u8; 2],
pub vsync: VsyncMode,
pub current_display: usize,
pub external_graphics_context: *const c_void,
}

View file

@ -0,0 +1,798 @@
use crate::heap;
use crate::noita::types::objects::{ConfigExplosion, ConfigGridCosmeticParticle};
use crate::noita::types::{StdMap, StdString, StdVec, ThiscallFn, Vec2, Vec2i};
use shared::world_sync::{Pixel, PixelFlags};
use std::ffi::c_void;
use std::fmt::{Debug, Formatter};
use std::slice;
#[repr(usize)]
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum CellType {
#[default]
None = 0,
Liquid = 1,
Gas = 2,
Solid = 3,
Fire = 4,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct CellGraphics {
pub texture_file: StdString,
pub color: Color,
pub fire_colors_index: u32,
pub randomize_colors: bool,
pub normal_mapped: bool,
pub is_grass: bool,
pub is_grass_hashed: bool,
pub pixel_info: *const c_void,
unknown: [isize; 6],
}
#[repr(C)]
#[derive(Debug)]
pub struct StatusEffect {
pub id: isize,
pub duration: f32,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct CellData {
pub name: StdString,
pub ui_name: StdString,
pub material_type: isize,
pub id_2: isize,
pub cell_type: CellType,
pub platform_type: isize,
pub wang_color: Color,
pub gfx_glow: isize,
pub gfx_glow_color: Color,
pub graphics: CellGraphics,
pub cell_holes_in_texture: bool,
pub stainable: bool,
pub burnable: bool,
pub on_fire: bool,
pub fire_hp: isize,
pub autoignition_temperature: isize,
pub minus_100_autoignition_temp: isize,
pub temperature_of_fire: isize,
pub generates_smoke: isize,
pub generates_flames: isize,
pub requires_oxygen: bool,
padding1: [u8; 3],
pub on_fire_convert_to_material: StdString,
pub on_fire_convert_to_material_id: isize,
pub on_fire_flame_material: StdString,
pub on_fire_flame_material_id: isize,
pub on_fire_smoke_material: StdString,
pub on_fire_smoke_material_id: isize,
pub explosion_config: *const ConfigExplosion,
pub durability: isize,
pub crackability: isize,
pub electrical_conductivity: bool,
pub slippery: bool,
padding2: [u8; 2],
pub stickyness: f32,
pub cold_freezes_to_material: StdString,
pub warmth_melts_to_material: StdString,
pub warmth_melts_to_material_id: isize,
pub cold_freezes_to_material_id: isize,
pub cold_freezes_chance_rev: i16,
pub warmth_melts_chance_rev: i16,
pub cold_freezes_to_dont_do_reverse_reaction: bool,
padding3: [u8; 3],
pub lifetime: isize,
pub hp: isize,
pub density: f32,
pub liquid_sand: bool,
pub liquid_slime: bool,
pub liquid_static: bool,
pub liquid_stains_self: bool,
pub liquid_sticks_to_ceiling: isize,
pub liquid_gravity: f32,
pub liquid_viscosity: isize,
pub liquid_stains: isize,
pub liquid_stains_custom_color: Color,
pub liquid_sprite_stain_shaken_drop_chance: f32,
pub liquid_sprite_stain_ignited_drop_chance: f32,
pub liquid_sprite_stains_check_offset: i8,
padding4: [u8; 3],
pub liquid_sprite_stains_status_threshold: f32,
pub liquid_damping: f32,
pub liquid_flow_speed: f32,
pub liquid_sand_never_box2d: bool,
padding5: [u8; 3],
pub gas_speed: i8,
pub gas_upwards_speed: i8,
pub gas_horizontal_speed: i8,
pub gas_downwards_speed: i8,
pub solid_friction: f32,
pub solid_restitution: f32,
pub solid_gravity_scale: f32,
pub solid_static_type: isize,
pub solid_on_collision_splash_power: f32,
pub solid_on_collision_explode: bool,
pub solid_on_sleep_convert: bool,
pub solid_on_collision_convert: bool,
pub solid_on_break_explode: bool,
pub solid_go_through_sand: bool,
pub solid_collide_with_self: bool,
padding6: [u8; 2],
pub solid_on_collision_material: StdString,
pub solid_on_collision_material_id: isize,
pub solid_break_to_type: StdString,
pub solid_break_to_type_id: isize,
pub convert_to_box2d_material: StdString,
pub convert_to_box2d_material_id: isize,
pub vegetation_full_lifetime_growth: isize,
pub vegetation_sprite: StdString,
pub vegetation_random_flip_x_scale: bool,
padding7: [u8; 3],
pub max_reaction_probability: u32,
pub max_fast_reaction_probability: u32,
unknown11: isize,
pub wang_noise_percent: f32,
pub wang_curvature: f32,
pub wang_noise_type: isize,
pub tags: StdVec<StdString>,
pub danger_fire: bool,
pub danger_radioactive: bool,
pub danger_poison: bool,
pub danger_water: bool,
pub stain_effects: StdVec<StdString>,
pub ingestion_effects: StdVec<StdString>,
pub always_ignites_damagemodel: bool,
pub ignore_self_reaction_warning: bool,
padding8: [u8; 2],
pub audio_physics_material_event_idx: isize,
pub audio_physics_material_wall_idx: isize,
pub audio_physics_material_solid_idx: isize,
pub audio_size_multiplier: f32,
pub audio_is_soft: bool,
padding9: [u8; 3],
pub audio_material_audio_type: isize,
pub audio_material_breakaudio_type: isize,
pub show_in_creative_mode: bool,
pub is_just_particle_fx: bool,
padding10: [u8; 2],
pub grid_cosmetic_particle_config: *const ConfigGridCosmeticParticle,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct CellVTables(pub [CellVTable; 5]);
impl CellVTables {
pub fn none(&self) -> &'static NoneCellVTable {
unsafe { self.0[0].none }
}
pub fn liquid(&self) -> &'static LiquidCellVTable {
unsafe { self.0[1].liquid }
}
pub fn gas(&self) -> &'static GasCellVTable {
unsafe { self.0[2].gas }
}
pub fn solid(&self) -> &'static SolidCellVTable {
unsafe { self.0[3].solid }
}
pub fn fire(&self) -> &'static FireCellVTable {
unsafe { self.0[4].fire }
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union CellVTable {
//ptr is 0xff2040
pub none: &'static NoneCellVTable,
//ptr is 0x100bb90
pub liquid: &'static LiquidCellVTable,
//ptr is 0x1007bcc
pub gas: &'static GasCellVTable,
//ptr is 0xff8a6c
pub solid: &'static SolidCellVTable,
//ptr is 0x10096e0
pub fire: &'static FireCellVTable,
}
impl Debug for CellVTable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self as *const CellVTable)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct SolidCellVTable {
unknown0: *const ThiscallFn,
unknown1: *const ThiscallFn,
unknown2: *const ThiscallFn,
unknown3: *const ThiscallFn,
unknown4: *const ThiscallFn,
unknown5: *const ThiscallFn,
unknown6: *const ThiscallFn,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct NoneCellVTable {
pub unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GasCellVTable {
unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FireCellVTable {
unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct LiquidCellVTable {
pub destroy: *const ThiscallFn,
pub get_cell_type: *const ThiscallFn,
unknown01: *const ThiscallFn,
unknown02: *const ThiscallFn,
unknown03: *const ThiscallFn,
pub get_color: *const ThiscallFn,
unknown04: *const ThiscallFn,
pub set_color: *const ThiscallFn,
unknown05: *const ThiscallFn,
unknown06: *const ThiscallFn,
unknown07: *const ThiscallFn,
unknown08: *const ThiscallFn,
pub get_material: *const ThiscallFn,
unknown09: *const ThiscallFn,
unknown10: *const ThiscallFn,
unknown11: *const ThiscallFn,
unknown12: *const ThiscallFn,
unknown13: *const ThiscallFn,
unknown14: *const ThiscallFn,
unknown15: *const ThiscallFn,
pub get_position: *const ThiscallFn,
unknown16: *const ThiscallFn,
unknown17: *const ThiscallFn,
unknown18: *const ThiscallFn,
unknown19: *const ThiscallFn,
unknown20: *const ThiscallFn,
unknown21: *const ThiscallFn,
unknown22: *const ThiscallFn,
unknown23: *const ThiscallFn,
pub is_burning: *const ThiscallFn,
unknown24: *const ThiscallFn,
unknown25: *const ThiscallFn,
unknown26: *const ThiscallFn,
pub stop_burning: *const ThiscallFn,
unknown27: *const ThiscallFn,
unknown28: *const ThiscallFn,
unknown29: *const ThiscallFn,
unknown30: *const ThiscallFn,
unknown31: *const ThiscallFn,
pub remove: *const ThiscallFn,
unknown32: *const ThiscallFn,
}
#[repr(C)]
#[derive(Clone, Debug, Copy)]
pub struct Cell {
pub vtable: CellVTable,
pub hp: isize,
unknown1: [isize; 2],
pub is_burning: bool,
pub temperature_of_fire: u8,
unknown2: [u8; 2],
pub material: &'static CellData,
}
unsafe impl Sync for Cell {}
unsafe impl Send for Cell {}
#[derive(Default, Debug)]
pub enum FullCell {
Cell(Cell),
LiquidCell(LiquidCell),
GasCell(GasCell),
FireCell(FireCell),
#[default]
None,
}
impl From<&Cell> for FullCell {
fn from(value: &Cell) -> Self {
match value.material.cell_type {
CellType::Liquid => FullCell::LiquidCell(*value.get_liquid()),
CellType::Fire => FullCell::FireCell(*value.get_fire()),
CellType::Gas => FullCell::GasCell(*value.get_gas()),
CellType::None | CellType::Solid => FullCell::Cell(*value),
}
}
}
impl Cell {
pub fn get_liquid(&self) -> &LiquidCell {
unsafe { std::mem::transmute::<&Cell, &LiquidCell>(self) }
}
pub fn get_fire(&self) -> &FireCell {
unsafe { std::mem::transmute::<&Cell, &FireCell>(self) }
}
pub fn get_gas(&self) -> &GasCell {
unsafe { std::mem::transmute::<&Cell, &GasCell>(self) }
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FireCell {
pub cell: Cell,
pub x: isize,
pub y: isize,
pub lifetime: isize,
unknown: isize,
}
impl FireCell {
///# Safety
pub unsafe fn create(
mat: &'static CellData,
vtable: &'static FireCellVTable,
world: *mut GridWorld,
) -> Self {
let lifetime = if let Some(world) = unsafe { world.as_mut() } {
world.rng *= 0x343fd;
world.rng += 0x269ec3;
(world.rng >> 0x10 & 0x7fff) % 0x15
} else {
-1
};
let mut cell = Cell::create(mat, CellVTable { fire: vtable });
cell.is_burning = true;
Self {
cell,
x: 0,
y: 0,
lifetime,
unknown: 1,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GasCell {
pub cell: Cell,
unknown5: isize,
unknown6: isize,
pub x: isize,
pub y: isize,
unknown1: u8,
unknown2: u8,
unknown3: u8,
unknown4: u8,
pub color: Color,
unknown7: isize,
unknown8: isize,
lifetime: isize,
}
impl GasCell {
///# Safety
pub unsafe fn create(
mat: &'static CellData,
vtable: &'static GasCellVTable,
world: *mut GridWorld,
) -> Self {
let (bool, lifetime) = if let Some(world) = unsafe { world.as_mut() } {
let life = ((mat.lifetime as f32 * 0.3) as u64).max(1);
world.rng *= 0x343fd;
world.rng += 0x269ec3;
(
(world.rng >> 0x10 & 0x7fff) % 0x65 < 0x32,
(((world.rng >> 0x10 & 0x7fff) as u64 % (life * 2 + 1)) - life) as isize,
)
} else {
(false, -1)
};
let mut cell = Cell::create(mat, CellVTable { gas: vtable });
cell.is_burning = true;
Self {
cell,
unknown5: if bool { 1 } else { 0xff },
unknown6: 0,
x: 0,
y: 0,
unknown1: 0,
unknown2: 0,
unknown3: 0,
unknown4: 0,
unknown7: 0,
unknown8: 0,
color: mat.graphics.color,
lifetime,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct LiquidCell {
pub cell: Cell,
pub x: isize,
pub y: isize,
unknown1: u8,
unknown2: u8,
pub is_static: bool,
unknown3: u8,
unknown4: isize,
unknown5: isize,
unknown6: isize,
pub color: Color,
pub original_color: Color,
lifetime: isize,
unknown8: isize,
}
impl LiquidCell {
/// # Safety
pub unsafe fn create(
mat: &'static CellData,
vtable: &'static LiquidCellVTable,
world: *mut GridWorld,
) -> Self {
let lifetime = if mat.lifetime > 0
&& let Some(world) = (unsafe { world.as_mut() })
{
let life = ((mat.lifetime as f32 * 0.3) as u64).max(1);
world.rng *= 0x343fd;
world.rng += 0x269ec3;
(((world.rng >> 0x10 & 0x7fff) as u64 % (life * 2 + 1)) - life) as isize
} else {
-1
};
Self {
cell: Cell::create(mat, CellVTable { liquid: vtable }),
x: 0,
y: 0,
unknown1: 3,
unknown2: 0,
is_static: mat.liquid_static,
unknown3: 0,
unknown4: 0,
unknown5: 0,
unknown6: 0,
color: mat.graphics.color,
original_color: mat.graphics.color,
lifetime,
unknown8: 0,
}
}
}
impl Cell {
pub fn create(material: &'static CellData, vtable: CellVTable) -> Self {
Self {
vtable,
hp: material.hp,
unknown1: [-1000, 0],
is_burning: material.on_fire,
temperature_of_fire: material.temperature_of_fire as u8,
unknown2: [0, 0],
material,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct GameWorld {
pub cam: AABB,
unknown1: [isize; 13],
pub grid_world: &'static mut GridWorld,
//likely more data
}
#[repr(C)]
pub struct CellFactory {
unknown1: isize,
pub material_names: StdVec<StdString>,
pub material_ids: StdMap<StdString, usize>,
pub cell_data: StdVec<CellData>,
pub material_count: usize,
unknown2: isize,
pub reaction_lookup: ReactionLookupTable,
pub fast_reaction_lookup: ReactionLookupTable,
pub req_reactions: StdVec<CellReactionBuf>,
pub materials_by_tag: StdMap<StdString, StdVec<&'static CellData>>,
unknown3: StdVec<*mut StdVec<*mut c_void>>,
pub fire_cell_data: &'static CellData,
unknown4: [usize; 4],
pub fire_material_id: usize,
}
impl Debug for CellFactory {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CellFactory")
.field(&"too large to debug")
.finish()
}
}
#[repr(C)]
#[derive(Debug)]
pub struct ReactionLookupTable {
pub width: usize,
pub height: usize,
pub len: usize,
unknown: [usize; 5],
pub storage: *mut CellReactionBuf,
unk_len: usize,
unknown3: usize,
}
impl AsRef<[CellReactionBuf]> for ReactionLookupTable {
fn as_ref(&self) -> &'static [CellReactionBuf] {
unsafe { slice::from_raw_parts(self.storage, self.len) }
}
}
impl ReactionLookupTable {
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'static [CellReaction]> {
self.as_ref()
.iter()
.map(|b| unsafe { slice::from_raw_parts(b.base, b.len) })
}
}
#[repr(C)]
#[derive(Debug)]
pub struct CellReactionBuf {
pub base: *mut CellReaction,
pub len: usize,
pub cap: usize,
}
#[repr(C)]
#[derive(Debug)]
pub struct CellReaction {
pub fast_reaction: bool,
padding: [u8; 3],
pub probability_times_100: usize,
pub input_cell1: isize,
pub input_cell2: isize,
pub output_cell1: isize,
pub output_cell2: isize,
pub has_input_cell3: bool,
padding2: [u8; 3],
pub input_cell3: isize,
pub output_cell3: isize,
pub cosmetic_particle: isize,
pub req_lifetime: isize,
pub blob_radius1: u8,
pub blob_radius2: u8,
pub blob_restrict_to_input_material1: bool,
pub blob_restrict_to_input_material2: bool,
pub destroy_horizontally_lonely_pixels: bool,
pub convert_all: bool,
padding3: [u8; 2],
pub entity_file_idx: usize,
pub direction: ReactionDir,
pub explosion_config: *const ConfigExplosion,
pub audio_fx_volume_1: f32,
}
#[derive(Debug)]
#[repr(isize)]
pub enum ReactionDir {
None = -1,
Top,
Bottom,
Left,
Right,
}
#[repr(C)]
#[derive(Debug)]
pub struct Textures {
//TODO find some data maybe
}
#[repr(C)]
#[derive(Debug)]
pub struct GameGlobal {
pub frame_num: usize,
pub frame_num_start: usize,
unknown1: isize,
pub m_game_world: &'static mut GameWorld,
pub m_grid_world: &'static mut GridWorld,
pub m_textures: &'static mut Textures,
pub m_cell_factory: &'static mut CellFactory,
unknown2: [isize; 11],
pub pause_state: isize,
unk: [isize; 5],
pub inventory_open: usize,
unk4: [isize; 79],
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Chunk {
#[inline]
pub fn get(&self, x: isize, y: isize) -> Option<&Cell> {
let index = (y << 9) | x;
unsafe { self.data[index.cast_unsigned()].as_ref() }
}
#[inline]
pub fn get_mut(&mut self, x: isize, y: isize) -> Option<&mut Cell> {
unsafe { self.get_mut_raw(x, y).as_mut() }
}
#[inline]
pub fn get_mut_raw(&mut self, x: isize, y: isize) -> &mut *mut Cell {
let index = (y << 9) | x;
&mut self.data[index.cast_unsigned()]
}
#[inline]
pub fn get_pixel(&self, x: isize, y: isize) -> Pixel {
if let Some(cell) = self.get(x, y) {
if cell.material.cell_type == CellType::Liquid {
Pixel::new(
cell.material.material_type as u16,
if cell.get_liquid().is_static == cell.material.liquid_static {
PixelFlags::Normal
} else {
PixelFlags::Abnormal
},
)
} else {
Pixel::new(cell.material.material_type as u16, PixelFlags::Normal)
}
} else {
Pixel::new(0, PixelFlags::Normal)
}
}
}
#[repr(C)]
pub struct ChunkMap {
pub len: usize,
pub unknown: isize,
pub chunk_array: &'static mut [*mut Chunk; 512 * 512],
pub chunk_count: usize,
pub min_chunk: Vec2i,
pub max_chunk: Vec2i,
pub min_pixel: Vec2i,
pub max_pixel: Vec2i,
}
impl Debug for ChunkMap {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkMap")
.field("len", &self.len)
.field("unknown", &self.unknown)
/*.field(
"chunk_array",
&self
.chunk_array
.iter()
.enumerate()
.filter_map(|(i, a)| unsafe {
a.as_ref().map(|a| (i % 512 - 256, i / 512 - 256, a))
})
.collect::<Vec<_>>(),
)*/
.field("chunk_count", &self.chunk_count)
.field("min_chunk", &self.min_chunk)
.field("max_chunk", &self.max_chunk)
.field("min_pixel", &self.min_pixel)
.field("max_pixel", &self.max_pixel)
.finish()
}
}
#[repr(C)]
pub struct Chunk {
pub data: &'static mut [*mut Cell; 512 * 512],
}
impl Debug for Chunk {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Chunk")
.field(
&self
.data
.iter()
.enumerate()
.filter_map(|(i, a)| {
unsafe { a.as_ref() }.map(|a| (i % 512, i / 512, a.material.material_type))
})
.collect::<Vec<_>>(),
)
.finish()
}
}
unsafe impl Sync for Chunk {}
unsafe impl Send for Chunk {}
impl ChunkMap {
#[inline]
pub fn get(&self, x: isize, y: isize) -> Option<&Chunk> {
let index = (((y - 256) & 511) << 9) | ((x - 256) & 511);
unsafe { self.chunk_array[index.cast_unsigned()].as_ref() }
}
#[inline]
pub fn get_mut(&mut self, x: isize, y: isize) -> Option<&mut Chunk> {
let index = (((y - 256) & 511) << 9) | ((x - 256) & 511);
unsafe { self.chunk_array[index.cast_unsigned()].as_mut() }
}
#[inline]
pub fn insert(&mut self, x: isize, y: isize, chunk: Chunk) {
let index = (((y - 256) & 511) << 9) | ((x - 256) & 511);
self.chunk_array[index.cast_unsigned()] = heap::place_new(chunk)
}
}
#[repr(C)]
#[derive(Debug)]
pub struct GridWorldVTable {
//ptr is 0x10013bc
pub unknown: [*const ThiscallFn; 3],
pub get_chunk_map: *const ThiscallFn,
pub unknownmagic: *const ThiscallFn,
pub unknown2: [*const ThiscallFn; 29],
}
#[repr(C)]
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Default)]
pub struct AABB {
pub top_left: Vec2,
pub bottom_right: Vec2,
}
#[repr(C)]
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Default)]
pub struct IAABB {
pub top_left: Vec2i,
pub bottom_right: Vec2i,
}
#[repr(C)]
#[derive(Debug)]
//ptr is 0x17f83e30, seems not constant
pub struct GridWorldThreadedVTable {
//TODO find some data maybe
}
#[repr(C)]
#[derive(Debug)]
pub struct GridWorldThreaded {
pub grid_world_threaded_vtable: &'static GridWorldThreadedVTable,
pub unknown: [isize; 287],
pub update_region: AABB,
}
#[repr(C)]
#[derive(Debug)]
pub struct GridWorld {
pub vtable: &'static GridWorldVTable,
pub rng: isize,
pub unk: [isize; 292],
pub cam_pos: Vec2i,
pub cam_dimen: Vec2i,
pub unknown: [isize; 6],
pub unk_cam: IAABB,
pub unk2_cam: IAABB,
pub unkown3: isize,
pub cam: IAABB,
pub unkown2: isize,
pub unk_counter: isize,
pub world_update_count: isize,
pub chunk_map: ChunkMap,
pub unknown2: [isize; 40],
pub m_thread_impl: &'static mut GridWorldThreaded,
}
#[repr(C)]
#[derive(Debug)]
pub struct B2Object {}

View file

@ -0,0 +1,82 @@
use crate::noita::types;
use crate::noita::types::StdVec;
use eyre::ContextCompat;
#[derive(Debug)]
pub struct ParticleWorldState {
pub world_ptr: *mut types::GridWorld,
pub material_list: StdVec<types::CellData>,
pub cell_vtables: types::CellVTables,
}
unsafe impl Sync for ParticleWorldState {}
unsafe impl Send for ParticleWorldState {}
impl ParticleWorldState {
pub fn get_shift<const CHUNK_SIZE: usize>(&self, x: isize, y: isize) -> (isize, isize) {
let shift_x = (x * CHUNK_SIZE as isize).rem_euclid(512);
let shift_y = (y * CHUNK_SIZE as isize).rem_euclid(512);
(shift_x, shift_y)
}
pub fn exists<const SCALE: isize>(&self, cx: isize, cy: isize) -> bool {
let Some(world) = (unsafe { self.world_ptr.as_mut() }) else {
return false;
};
world.chunk_map.get(cx >> SCALE, cy >> SCALE).is_some()
}
///# Safety
#[allow(clippy::type_complexity)]
pub unsafe fn clone_chunks(&mut self) -> Vec<((isize, isize), Vec<types::FullCell>)> {
let Some(world) = (unsafe { self.world_ptr.as_mut() }) else {
return Vec::new();
};
world
.chunk_map
.chunk_array
.iter()
.enumerate()
.filter_map(|(i, c)| {
unsafe { c.as_ref() }.map(|c| {
let x = i as isize % 512 - 256;
let y = i as isize / 512 - 256;
(
(x, y),
c.data
.iter()
.map(|p| {
unsafe { p.as_ref() }
.map(types::FullCell::from)
.unwrap_or_default()
})
.collect(),
)
})
})
.collect::<Vec<((isize, isize), Vec<types::FullCell>)>>()
}
///# Safety
pub unsafe fn debug_mouse_pos(&mut self) -> eyre::Result<()> {
let (x, y) = crate::raw::debug_get_mouse_world()?;
let (x, y) = (x.floor(), y.floor());
let (x, y) = (x as isize, y as isize);
if let Some(pixel_array) = unsafe { self.world_ptr.as_mut() }
.wrap_err("no world")?
.chunk_map
.get_mut(x.div_euclid(512), y.div_euclid(512))
{
if let Some(cell) = pixel_array.get(x.rem_euclid(512), y.rem_euclid(512)) {
let full = types::FullCell::from(cell);
crate::print!("{full:?}");
} else {
crate::print!("mat nil");
}
}
Ok(())
}
pub fn new() -> eyre::Result<Self> {
let (cell_vtables, global_ptr) = crate::noita::init_data::get_functions()?;
let global = unsafe { global_ptr.as_mut() }.wrap_err("no global?")?;
Ok(ParticleWorldState {
world_ptr: global.m_grid_world,
material_list: global.m_cell_factory.cell_data.copy(),
cell_vtables,
})
}
}

View file

@ -0,0 +1,39 @@
dofile_once("data/scripts/lib/utilities.lua")
--function collision_trigger(target_id)
local entity_id = GetUpdatedEntityID()
local x, y = EntityGetTransform(entity_id)
local target_id = EntityGetClosestWithTag(x, y, "enemy")
if not target_id then
return
end
if not IsPlayer(target_id) and EntityGetIsAlive(target_id) then
--print("ghost added: " .. x .. ", " .. y)
local children = EntityGetAllChildren(entity_id)
if #children == 0 then
return
end
local ghost_id = children[1]
-- reduce health of target for balance
component_readwrite(
EntityGetFirstComponent(target_id, "DamageModelComponent"),
{ hp = 0, max_hp = 0 },
function(comp)
comp.max_hp = math.max(comp.max_hp * 0.75, comp.max_hp - 3)
comp.hp = comp.max_hp
end
)
-- enable ghost
for _, comp in pairs(EntityGetAllComponents(ghost_id)) do
EntitySetComponentIsEnabled(ghost_id, comp, true)
end
-- transfer ghost & remove spawner
EntityRemoveFromParent(ghost_id)
EntityAddChild(target_id, ghost_id)
EntityKill(entity_id)
end

View file

@ -0,0 +1,40 @@
dofile_once("data/scripts/lib/utilities.lua")
local entity_id = GetUpdatedEntityID()
local pos_x, pos_y = EntityGetTransform(entity_id)
pos_x = pos_x - 5
local stones = {
"data/entities/props/physics_stone_01.xml",
"data/entities/props/physics_stone_02.xml",
"data/entities/props/physics_stone_03.xml",
"data/entities/props/physics_stone_03.xml",
"data/entities/props/physics_stone_04.xml",
"data/entities/props/physics_stone_04.xml",
}
local props = {
"data/entities/props/physics_box_explosive.xml",
"data/entities/props/physics_barrel_oil.xml",
"data/entities/props/physics_seamine.xml",
"data/entities/props/physics/minecart.xml",
}
local count = ProceduralRandomi(pos_x, pos_y, 2, 7)
if not GameHasFlagRun("ew_flag_this_is_host") then
return
end
for i = 1, count do
local obj
local r = ProceduralRandomf(i + pos_x, pos_y + 4)
if r > 0.9 then
obj = props[ProceduralRandomi(pos_x - 4, pos_y + i, 1, #props)]
else
obj = stones[ProceduralRandomi(pos_x - 4, pos_y + i, 1, #stones)]
end
EntityLoad(obj, pos_x + r * 8, pos_y)
pos_y = pos_y - 5
end

View file

@ -28,10 +28,10 @@ module.phys_sync_allowed = {
["data/entities/props/physics_box_harmless.xml"] = true,
--["data/entities/props/physics_tubelamp.xml"] = true,
["data/entities/props/suspended_container.xml"] = true,
["data/entities/props/suspended_seamine.xml"] = true,
["data/entities/props/suspended_tank_acid.xml"] = true,
["data/entities/props/suspended_tank_radioactive.xml"] = true,
--["data/entities/props/suspended_container.xml"] = true,
--["data/entities/props/suspended_seamine.xml"] = true,
--["data/entities/props/suspended_tank_acid.xml"] = true,
--["data/entities/props/suspended_tank_radioactive.xml"] = true,
["data/entities/props/physics_wheel_small.xml"] = true,
["data/entities/props/physics_wheel_stand_01.xml"] = true,
@ -114,11 +114,11 @@ module.phys_sync_allowed = {
["data/entities/props/physics/trap_electricity.xml"] = true,
["data/entities/props/physics/trap_ignite_enabled.xml"] = true,
["data/entities/props/physics/trap_ignite.xml"] = true,
["data/entities/props/physics/trap_laser_enabled_left.xml"] = true,
["data/entities/props/physics/trap_laser_enabled.xml"] = true,
["data/entities/props/physics/trap_laser_toggling_left.xml"] = true,
["data/entities/props/physics/trap_laser_toggling.xml"] = true,
["data/entities/props/physics/trap_laser.xml"] = true,
--["data/entities/props/physics/trap_laser_enabled_left.xml"] = true,
--["data/entities/props/physics/trap_laser_enabled.xml"] = true,
--["data/entities/props/physics/trap_laser_toggling_left.xml"] = true,
--["data/entities/props/physics/trap_laser_toggling.xml"] = true,
--["data/entities/props/physics/trap_laser.xml"] = true,
-- animals
["data/entities/buildings/essence_eater.xml"] = true,

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