Compare commits

...

292 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
117 changed files with 30585 additions and 7634 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
@ -50,7 +53,7 @@ jobs:
- name: Build Linux proxy release
run: cargo build --release
working-directory: ./noita-proxy
- name: Create archives
run: python scripts/ci_make_archives.py linux
@ -80,10 +83,10 @@ jobs:
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
@ -109,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
@ -123,4 +126,4 @@ jobs:
with:
#draft: true
artifacts: "*.zip"
bodyFile: last_release_notes.md
bodyFile: last_release_notes.md

8
.gitignore vendored
View file

@ -5,4 +5,10 @@ save_state
/quant.ew/files/system/player/tmp/
/quant.ew/ewext.dll
/quant.ew/ewext0.dll
/quant.ew/ewext1.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
@ -63,4 +69,4 @@ clean:
cd ewext && cargo clean
make_release_assets:
python scripts/make_release_assets.py
python scripts/make_release_assets.py

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".
@ -71,14 +93,14 @@ There are 11 global perks:
[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
@ -90,4 +112,4 @@ Special thanks to:
- Contributors.
- @EvaisaDev for allowing to use code from Noita Arena mod.
- @dextercd for NoitaPatcher.
- Creators of other libraries used in this project.
- Creators of other libraries used in this project.

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.2"
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.2"
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,748 +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);
}
}
}
}
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,13 +1,12 @@
## Noita Entangled Worlds v1.5.4
## Noita Entangled Worlds v1.6.2
Changed config and save_state location to system-specific locations (%APPDATA%\Roaming\quant\entangledworlds on windows, ~/.config/entangledworlds and ~/.local/share/entangledworlds on Linux)
Old locations (right next to the executable) will be used if proxy.ron already exists here.
- remove the ton of debug prints i forgot about to prob help performance
Should fix issues related to saving when location next to executable isn't writeable.
## Accepted pull requests
- translated more strings for russian by @goluboch in #378
No pull requests have been accepted in this release.
## Installation

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

2965
noita-proxy/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,63 +4,65 @@ resolver = "2"
[package]
name = "noita-proxy"
description = "Noita Entangled Worlds companion app."
version = "1.5.4"
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
@ -70,4 +72,4 @@ lto = true
strip = true
[profile.release-lto]
inherits = "release"
inherits = "release"

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

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

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

@ -47,10 +47,9 @@ impl SavePaths {
}
fn next_to_exe_path() -> PathBuf {
let base_path = std::env::current_exe()
std::env::current_exe()
.map(|p| p.parent().unwrap().to_path_buf())
.unwrap_or(".".into());
base_path
.unwrap_or(".".into())
}
fn settings_next_to_exe_path() -> PathBuf {

View file

@ -20,7 +20,7 @@ impl SelfRestarter {
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.command.arg(format!("{lobby_mode:?}"));
self
}

View file

@ -128,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();
@ -140,7 +140,7 @@ impl SelfUpdateManager {
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();
@ -149,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}"));
}
}
}
@ -172,12 +172,22 @@ fn proxy_bin_name() -> &'static str {
if cfg!(target_os = "windows") {
"noita_proxy.exe"
} else if cfg!(target_os = "macos") {
"noita_proxy.arm64"
"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)
@ -187,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,3 +1,4 @@
use arboard::Clipboard;
use bitcode::{Decode, Encode};
use bookkeeping::{
noita_launcher::{LaunchTokenResult, NoitaLauncher},
@ -6,14 +7,12 @@ use bookkeeping::{
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;
@ -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>,
@ -1087,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,
@ -1123,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();
}
{
@ -1201,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 {
@ -1241,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(
@ -1320,6 +1327,7 @@ pub struct App {
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> {
@ -1452,6 +1460,7 @@ impl App {
noitalog: Vec::new(),
proxylog: String::new(),
save_paths,
clipboard: Clipboard::new().ok(),
};
if let Some(connect_to) = me.args.auto_connect_to {
@ -1523,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)
@ -1651,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());
@ -1670,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()
@ -1687,7 +1696,7 @@ impl App {
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(info_rect.shrink(group_shrink)),
..Default::default()
@ -1701,7 +1710,7 @@ impl App {
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(right_b_panel.shrink(group_shrink)),
..Default::default()
@ -1715,7 +1724,7 @@ impl App {
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(settings_rect.shrink(group_shrink)),
..Default::default()
@ -1744,7 +1753,7 @@ impl App {
});
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(steam_connect_rect.shrink(group_shrink)),
..Default::default()
@ -1757,7 +1766,7 @@ impl App {
});
},
);
ui.allocate_new_ui(
ui.scope_builder(
UiBuilder {
max_rect: Some(ip_connect_rect.shrink(group_shrink)),
..Default::default()
@ -1813,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}")),
);
}
},
@ -1828,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,
@ -1911,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() {
@ -1936,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}"));
}
}
}
@ -1959,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);
}
});
}
@ -2216,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() {
@ -2228,7 +2240,6 @@ impl App {
self.noitalog.push(s);
}
}
}
match self.connected_menu {
ConnectedMenu::Normal => {
if netman.peer.is_steam() {
@ -2238,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 {
@ -2314,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);
}
}
@ -2397,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)
@ -2420,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])
@ -2612,7 +2627,7 @@ impl eframe::App for App {
.connect_to(*target_lobby)
.restart()
});
self.notify_error(format!("Failed to self-restart: {}", err));
self.notify_error(format!("Failed to self-restart: {err}"));
}
if button_back {
self.state = AppState::Connect;
@ -2852,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))
@ -2869,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 {
@ -2895,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

@ -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();
@ -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]
@ -3410,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,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

@ -1,40 +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
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

@ -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,
@ -288,4 +288,4 @@ for line in string.gmatch(ModTextFileGetContent("mods/quant.ew/files/resource/in
module.interned_filename_to_index[line] = #module.interned_index_to_filename
end
return module
return module

View file

@ -28,11 +28,18 @@ ctx.init = function()
ctx.host_frame_num = 0
ctx.is_texting = false
ctx.stop_cam = false
ctx.timings = ""
end
local function is_measure_perf_enabled()
-- return ctx.proxy_opt.debug
return false
return ModSettingGet("quant.ew.log_performance")
end
function ctx.finish()
if is_measure_perf_enabled() and string.len(ctx.timings) > 1 then
print(string.sub(ctx.timings, 1, -2) .. "}")
ctx.timings = "{"
end
end
function ctx.add_hook(hook_name, system_name, fn)
@ -45,10 +52,8 @@ function ctx.add_hook(hook_name, system_name, fn)
local start_time = GameGetRealWorldTimeSinceStarted()
util.tpcall(entry.fn, ...)
local end_time = GameGetRealWorldTimeSinceStarted()
local delta = (end_time - start_time) * 1000
if delta > 0.02 then
print("Hook " .. hook_name .. " took " .. delta .. " ms to run for " .. entry.system_name)
end
local delta = (end_time - start_time) * 1000000
ctx.timings = ctx.timings .. hook_name .. "/" .. entry.system_name .. ":" .. delta .. ","
end
end,
})

View file

@ -187,9 +187,7 @@ end
local DEST_PROXY = 1
local DEST_BROADCAST = 2
local DEST_PROXY_BIN = 3
local DEST_FLAGS = 0
local MOD_RELIABLE = 4 -- 0b101
function net.send_internal(msg, dest, reliable)
@ -228,10 +226,6 @@ function net.proxy_send(key, value)
net.send_internal(key .. " " .. value, DEST_PROXY)
end
function net.proxy_bin_send(key, value)
ewext.netmanager_send(string.char(DEST_PROXY_BIN, key) .. value)
end
function net.proxy_notify_game_over()
net.proxy_send("game_over", 1)
end

View file

@ -73,6 +73,7 @@ local function set_lukki(entity, peer_id)
end
if new ~= nil then
ComponentSetValue(sprite, "image_file", new)
EntityRefreshSprite(entity, sprite)
end
end
end

View file

@ -15,9 +15,9 @@ function on_open(entity_item)
rand_x = tonumber(ComponentGetValue(position_comp, "pos_x"))
rand_y = tonumber(ComponentGetValue(position_comp, "pos_y"))
end
local is_mine = 0
local is_mine = false
if ComponentGetValue2(gid, "value_bool") then
is_mine = 1
is_mine = true
end
gid = ComponentGetValue2(gid, "value_string")
CrossCall("ew_chest_opened", x, y, rand_x, rand_y, EntityGetFilename(entity_item), gid, is_mine)

View file

@ -1,914 +0,0 @@
local item_sync = dofile_once("mods/quant.ew/files/system/item_sync.lua")
local effect_sync = dofile_once("mods/quant.ew/files/system/game_effect_sync/game_effect_sync.lua")
local stain_sync = dofile_once("mods/quant.ew/files/system/effect_data_sync/effect_data_sync.lua")
local ffi = require("ffi")
local rpc = net.new_rpc_namespace()
local EnemyData = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
})
-- Variant of EnemyData for when we don't have any motion (or no VelocityComponent).
local EnemyDataNoMotion = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y" },
})
local EnemyDataWorm = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy", "tx", "ty" },
})
local EnemyDataKolmi = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
bool = { "enabled" },
vecfloat = { "legs" },
})
local EnemyDataMom = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
vecbool = { "orbs" },
})
local EnemyDataFish = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
u8 = { "r" },
})
local HpData = util.make_type({
u32 = { "enemy_id" },
f32 = { "hp", "max_hp" },
})
local should_wait = {}
local first = true
local FULL_TURN = math.pi * 2
local frame = 0
local enemy_sync = {}
local unsynced_enemys = {}
local dead_entities = {}
--this basically never happens, doesn't seem that useful anymore. Perhaps should be removed to conserve memory.
--local confirmed_kills = {}
local spawned_by_us = {}
-- HACK
local times_spawned_last_minute = {}
local DISTANCE_LIMIT = 128 * 6
for filename, _ in pairs(constants.phys_sync_allowed) do
util.add_tag_to(filename, "prop_physics")
-- Idk it just causes the minecart to not appear at all.
-- util.replace_text_in(filename, 'kill_entity_after_initialized="1"', 'kill_entity_after_initialized="0"')
end
util.add_cross_call("ew_es_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
local damage = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
table.insert(dead_entities, { enemy_id, responsible, ComponentGetValue2(damage, "wait_for_kill_flag_on_death") })
end)
local function world_exists_for(entity)
local x, y = EntityGetFirstHitboxCenter(entity)
local w, h = 5, 5 -- TODO
w = w * 0.5
h = h * 0.5
return DoesWorldExistAt(x - w, y - h, x + w, y + h)
end
local function table_extend(to, from)
for _, e in ipairs(from) do
to[#to + 1] = e
end
end
local function table_extend_filtered(to, from, filter)
for _, e in ipairs(from) do
if filter(e) then
to[#to + 1] = e
end
end
end
local function get_sync_entities(return_all)
local entities = EntityGetWithTag("enemy") or {}
table_extend(entities, EntityGetWithTag("ew_enemy_sync_extra"))
table_extend(entities, EntityGetWithTag("plague_rat"))
table_extend(entities, EntityGetWithTag("seed_f"))
table_extend(entities, EntityGetWithTag("seed_e"))
table_extend(entities, EntityGetWithTag("seed_d"))
table_extend(entities, EntityGetWithTag("seed_c"))
table_extend(entities, EntityGetWithTag("perk_fungus_tiny"))
table_extend(entities, EntityGetWithTag("helpless_animal"))
table_extend_filtered(entities, EntityGetWithTag("hittable"), function(ent)
local name = EntityGetName(ent)
local file = EntityGetFilename(ent)
return name == "$item_essence_stone"
or name == "$animal_fish_giga"
or file == "data/entities/buildings/spittrap_left.xml"
or file == "data/entities/buildings/spittrap_right.xml"
or file == "data/entities/buildings/thundertrap_left.xml"
or file == "data/entities/buildings/thundertrap_right.xml"
or file == "data/entities/buildings/arrowtrap_left.xml"
or file == "data/entities/buildings/arrowtrap_right.xml"
or file == "data/entities/buildings/firetrap_left.xml"
or file == "data/entities/buildings/firetrap_right.xml"
--data/entities/buildings/statue_trap_left.xml
--data/entities/buildings/statue_trap_right.xml
end)
table_extend_filtered(entities, EntityGetWithTag("prop_physics"), function(ent)
local f = EntityGetFilename(ent)
if f ~= nil then
return constants.phys_sync_allowed[f]
end
return true
end)
local entities2 = {}
if return_all then
table_extend_filtered(entities2, entities, function(ent)
return not EntityHasTag(ent, "ew_no_enemy_sync")
end)
else
table_extend_filtered(entities2, entities, function(ent)
local x, y = EntityGetTransform(ent)
local has_anyone = EntityHasTag(ent, "worm")
or EntityGetFirstComponent(ent, "BossHealthBarComponent") ~= nil
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "ew_peer") ~= 0
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "polymorphed_player") ~= 0
return has_anyone and not EntityHasTag(ent, "ew_no_enemy_sync")
end)
end
return entities2
end
local was_held = {}
function enemy_sync.host_upload_entities()
local entities = get_sync_entities()
local enemy_data_list = {}
for i, enemy_id in ipairs(entities) do
if not world_exists_for(enemy_id) then
goto continue
end
local filename = EntityGetFilename(enemy_id)
filename = constants.interned_filename_to_index[filename] or filename
local x, y, rot = EntityGetTransform(enemy_id)
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
local vx, vy = 0, 0
if character_data ~= nil then
vx, vy = ComponentGetValue2(character_data, "mVelocity")
else
local velocity = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
if velocity ~= nil then
vx, vy = ComponentGetValue2(velocity, "mVelocity")
end
end
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
if ai_component ~= 0 and ai_component ~= nil then
ComponentSetValue2(ai_component, "max_distance_to_cam_to_start_hunting", math.pow(2, 29))
end
local phys_info = util.get_phys_info(enemy_id, true)
if phys_info == nil then
goto continue
end
local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
if has_hp then
util.ensure_component_present(enemy_id, "LuaComponent", "ew_death_notify", {
script_death = "mods/quant.ew/files/resource/cbs/death_notify.lua",
})
end
-- TODO: figure out how to sync those.
-- local laser_sight_data = nil
-- local laser_sight = EntityGetFirstComponentIncludingDisabled(enemy_id, "SpriteComponent", "laser_sight")
-- if laser_sight ~= nil and laser_sight ~= 0 then
-- -- local x, y, r =
-- end
local death_triggers = {}
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_death")
if script ~= nil and script ~= "" then
table.insert(death_triggers, constants.interned_filename_to_index[script] or script)
end
end
local en_data
local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
if EntityHasTag(enemy_id, "boss_centipede") then
local legs = {}
for _, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
local lx, ly = ComponentGetValue2(limb, "end_position")
table.insert(legs, lx)
table.insert(legs, ly)
end
en_data = EnemyDataKolmi({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
enabled = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent", "disabled_at_start") ~= nil,
legs = legs,
})
elseif EntityHasTag(enemy_id, "boss_wizard") then
local orbs = { false, false, false, false, false, false, false, false }
for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
local n = ComponentGetValue2(var, "value_int")
orbs[n] = true
end
end
en_data = EnemyDataMom({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
orbs = orbs,
})
elseif worm ~= nil then
local tx, ty = ComponentGetValue2(worm, "mTargetVec")
en_data = EnemyDataWorm({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
tx = tx,
ty = ty,
})
elseif math.abs(vx) < 0.01 and math.abs(vy) < 0.01 then
en_data = EnemyDataNoMotion({
enemy_id = enemy_id,
x = x,
y = y,
})
elseif EntityGetFirstComponentIncludingDisabled(enemy_id, "AdvancedFishAIComponent") ~= nil then
en_data = EnemyDataFish({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
r = math.floor((rot % FULL_TURN) / FULL_TURN * 255),
})
else
en_data = EnemyData({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
})
end
local wand
local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
if inv ~= nil then
local item = ComponentGetValue2(inv, "mActualActiveItem")
if item ~= nil and EntityGetIsAlive(item) then
if not EntityHasTag(item, "ew_global_item") then
item_sync.make_item_global(item)
else
wand = item_sync.get_global_item_id(item)
if wand == nil then
EntityRemoveTag(item, "ew_global_item")
goto continue
end
if not item_sync.is_my_item(wand) then
item_sync.take_authority(wand)
end
was_held[wand] = true
end
end
end
local effect_data = effect_sync.get_sync_data(enemy_id, true)
local has_laser
local animations = {}
for _, sprite in ipairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
local animation
if sprite ~= nil then
animation = ComponentGetValue2(sprite, "rect_animation")
end
table.insert(animations, animation)
if ComponentHasTag(sprite, "laser_sight") then
has_laser = true
end
end
local laser
if has_laser and EntityGetName(enemy_id) ~= "$animal_turret" then
local ai = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
if ai ~= nil then
local target = ComponentGetValue2(ai, "mGreatestPrey")
local peer = player_fns.get_player_data_by_local_entity_id(target)
if peer ~= nil then
laser = peer.peer_id
end
end
end
local dont_cull = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent") ~= nil
or worm ~= nil
or EntityHasTag(enemy_id, "seed_f")
or EntityHasTag(enemy_id, "seed_e")
or EntityHasTag(enemy_id, "seed_d")
or EntityHasTag(enemy_id, "seed_c")
or EntityGetFilename(enemy_id) == "data/entities/buildings/essence_eater.xml"
local stains = stain_sync.get_stains(enemy_id)
table.insert(
enemy_data_list,
{ filename, en_data, phys_info, wand, effect_data, animations, dont_cull, death_triggers, stains, laser }
)
::continue::
end
rpc.handle_enemy_data(enemy_data_list, first)
first = false
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
end
local function host_upload_health()
local entities = get_sync_entities()
local enemy_health_list = {}
for i, enemy_id in ipairs(entities) do
if not world_exists_for(enemy_id) then
goto continue
end
local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
if has_hp then
table.insert(
enemy_health_list,
HpData({
enemy_id = enemy_id,
hp = hp,
max_hp = max_hp,
})
)
end
::continue::
end
if #enemy_health_list > 0 then
rpc.handle_enemy_health(enemy_health_list)
end
end
function enemy_sync.client_cleanup()
local entities = get_sync_entities(true)
for _, enemy_id in ipairs(entities) do
if not EntityHasTag(enemy_id, "ew_replicated") then
EntityKill(enemy_id)
elseif not spawned_by_us[enemy_id] then
EntityKill(enemy_id)
end
end
for remote_id, enemy_data in pairs(ctx.entity_by_remote_id) do
if frame > enemy_data.frame then
EntityKill(enemy_data.id)
ctx.entity_by_remote_id[remote_id] = nil
end
end
end
function enemy_sync.on_world_update_host()
local rt = math.floor(tonumber(ModSettingGet("quant.ew.enemy_sync") or 2) or 2 + 0.5)
local n = 0
if rt == 3 then
n = 2
elseif rt == 2 then
n = 1
end
if rt == 1 or GameGetFrameNum() % rt == n then
enemy_sync.host_upload_entities()
end
if GameGetFrameNum() % 10 == 5 then
host_upload_health()
end
for wand, _ in pairs(was_held) do
if EntityGetRootEntity(wand) == wand then
was_held[wand] = nil
if item_sync.is_my_item(item_sync.get_global_item_id(wand)) then
item_sync.make_item_global(wand)
end
end
end
end
function enemy_sync.on_world_update_client()
if GameGetFrameNum() % 12 == 1 then
enemy_sync.client_cleanup()
end
if GameGetFrameNum() % (60 * 60) == 1 then
times_spawned_last_minute = {}
end
end
local kolmi_spawn
local function sync_enemy(enemy_info_raw, force_no_cull, host_fps)
local filename = enemy_info_raw[1]
filename = constants.interned_index_to_filename[filename] or filename
local en_data = enemy_info_raw[2]
local dont_cull = enemy_info_raw[7]
local death_triggers = enemy_info_raw[8]
local stains = enemy_info_raw[9]
local has_laser = enemy_info_raw[10]
local remote_enemy_id = en_data.enemy_id
local x, y = en_data.x, en_data.y
if not force_no_cull and not dont_cull then
local my_x, my_y = EntityGetTransform(ctx.my_player.entity)
if my_x == nil then
goto continue
end
local c_x, c_y = GameGetCameraPos()
local dx, dy = my_x - x, my_y - y
local cdx, cdy = c_x - x, c_y - y
if
dx * dx + dy * dy > DISTANCE_LIMIT * DISTANCE_LIMIT
and cdx * cdx + cdy * cdy > DISTANCE_LIMIT * DISTANCE_LIMIT
then
if ctx.entity_by_remote_id[remote_enemy_id] ~= nil then
EntityKill(ctx.entity_by_remote_id[remote_enemy_id].id)
ctx.entity_by_remote_id[remote_enemy_id] = nil
end
unsynced_enemys[remote_enemy_id] = enemy_info_raw
goto continue
else
unsynced_enemys[remote_enemy_id] = nil
end
else
unsynced_enemys[remote_enemy_id] = nil
end
local vx = 0
local vy = 0
if ffi.typeof(en_data) ~= EnemyDataNoMotion then
vx, vy = en_data.vx, en_data.vy
end
local phys_infos = enemy_info_raw[3]
local gid = enemy_info_raw[4]
local effects = enemy_info_raw[5]
local animation = enemy_info_raw[6]
local has_died = filename == nil
local frame_now = GameGetFrameNum()
--[[if confirmed_kills[remote_enemy_id] then
goto continue
end]]
if
ctx.entity_by_remote_id[remote_enemy_id] ~= nil
and not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
then
ctx.entity_by_remote_id[remote_enemy_id] = nil
end
if ctx.entity_by_remote_id[remote_enemy_id] == nil then
if filename == nil or filename == "" or not ModDoesFileExist(filename) then
goto continue
end
times_spawned_last_minute[remote_enemy_id] = (times_spawned_last_minute[remote_enemy_id] or 0) + 1
if times_spawned_last_minute[remote_enemy_id] > 5 then
if times_spawned_last_minute[remote_enemy_id] == 6 then
print("Entity has been spawned again more than 5 times in last minute, skipping " .. filename)
end
goto continue
end
local enemy_id
enemy_id = EntityLoad(filename, x, y)
if enemy_id == nil then
print("entity is nil " .. tostring(filename))
goto continue
end
spawned_by_us[enemy_id] = true
EntityAddTag(enemy_id, "ew_replicated")
EntityAddTag(enemy_id, "polymorphable_NOT")
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_damage_received")
if
(
script ~= nil
and (
script == "data/scripts/animals/leader_damage.lua"
or script == "data/scripts/animals/giantshooter_death.lua"
or script == "data/scripts/animals/blob_damage.lua"
)
)
or ComponentGetValue2(com, "script_source_file")
== "data/scripts/props/suspended_container_physics_objects.lua"
then
EntityRemoveComponent(enemy_id, com)
end
end
EntityAddComponent2(enemy_id, "LuaComponent", {
_tags = "ew_immortal",
script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua",
})
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
end
for _, name in ipairs({
"AnimalAIComponent",
"PhysicsAIComponent",
"CameraBoundComponent",
"AdvancedFishAIComponent",
"AIAttackComponent",
}) do
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, name)
if ai_component ~= 0 then
EntityRemoveComponent(enemy_id, ai_component)
end
end
ctx.entity_by_remote_id[remote_enemy_id] = { id = enemy_id, frame = frame_now }
for _, phys_component in ipairs(EntityGetComponent(enemy_id, "PhysicsBody2Component") or {}) do
if phys_component ~= nil and phys_component ~= 0 then
ComponentSetValue2(phys_component, "destroy_body_if_entity_destroyed", true)
end
end
-- Make sure stuff doesn't decide to explode on clients by itself.
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_damage_percent", 0)
ComponentSetValue2(expl_component, "physics_body_modified_death_probability", 0)
ComponentSetValue2(expl_component, "explode_on_death_percent", 0)
end
local pick_up = EntityGetFirstComponentIncludingDisabled(enemy_id, "ItemPickUpperComponent")
if pick_up ~= nil then
EntityRemoveComponent(enemy_id, pick_up)
end
for _, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent", "character") or {}) do
ComponentRemoveTag(sprite, "character")
end
local ghost = EntityGetFirstComponentIncludingDisabled(enemy_id, "GhostComponent")
if ghost ~= nil then
ComponentSetValue2(ghost, "die_if_no_home", false)
end
if not EntityHasTag(enemy_id, "effectable_prop") then
util.make_ephemerial(enemy_id)
end
end
local enemy_data_new = ctx.entity_by_remote_id[remote_enemy_id]
enemy_data_new.frame = frame_now
local enemy_id = enemy_data_new.id
if not has_died then
local laser = EntityGetFirstComponentIncludingDisabled(enemy_id, "LaserEmitterComponent", "ew_laser")
if has_laser then
if laser == nil then
laser = EntityAddComponent2(enemy_id, "LaserEmitterComponent", { _tags = "ew_laser" })
ComponentObjectSetValue2(laser, "laser", "max_cell_durability_to_destroy", 0)
ComponentObjectSetValue2(laser, "laser", "damage_to_cells", 0)
ComponentObjectSetValue2(laser, "laser", "max_length", 1024)
ComponentObjectSetValue2(laser, "laser", "beam_radius", 0)
ComponentObjectSetValue2(laser, "laser", "beam_particle_chance", 80)
ComponentObjectSetValue2(laser, "laser", "beam_particle_fade", 0)
ComponentObjectSetValue2(laser, "laser", "hit_particle_chance", 0)
ComponentObjectSetValue2(laser, "laser", "audio_enabled", false)
ComponentObjectSetValue2(laser, "laser", "damage_to_entities", 0)
ComponentObjectSetValue2(laser, "laser", "beam_particle_type", 225)
end
local target = ctx.players[has_laser].entity
local lx, ly = EntityGetTransform(target)
if lx ~= nil then
local did_hit, _, _ = RaytracePlatforms(x, y, lx, ly)
ComponentSetValue2(laser, "is_emitting", not did_hit)
if not did_hit then
local dy = ly - y
local dx = lx - x
local theta = math.atan2(dy, dx)
ComponentSetValue2(laser, "laser_angle_add_rad", theta)
ComponentObjectSetValue2(laser, "laser", "max_length", math.sqrt(dx * dx + dy * dy))
end
end
elseif laser ~= nil then
ComponentSetValue2(laser, "is_emitting", false)
end
if not util.set_phys_info(enemy_id, phys_infos, host_fps) or enemy_id == kolmi_spawn then
local m = host_fps / ctx.my_player.fps
vx, vy = vx * m, vy * m
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
if character_data ~= nil then
ComponentSetValue2(character_data, "mVelocity", vx, vy)
else
local velocity_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
if velocity_data ~= nil then
ComponentSetValue2(velocity_data, "mVelocity", vx, vy)
end
end
if ffi.typeof(en_data) == EnemyDataFish then
EntitySetTransform(enemy_id, x, y, en_data.r / 255 * FULL_TURN)
else
EntitySetTransform(enemy_id, x, y)
end
end
local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
if worm ~= nil and ffi.typeof(en_data) == EnemyDataWorm then
local tx, ty = en_data.tx, en_data.ty
ComponentSetValue2(worm, "mTargetVec", tx, ty)
end
if ffi.typeof(en_data) == EnemyDataKolmi and en_data.enabled then
if kolmi_spawn ~= enemy_id then
for _, c in ipairs(EntityGetComponentIncludingDisabled(enemy_id, "LuaComponent") or {}) do
EntityRemoveComponent(enemy_id, c)
end
kolmi_spawn = enemy_id
end
EntitySetComponentsWithTagEnabled(enemy_id, "enabled_at_start", false)
EntitySetComponentsWithTagEnabled(enemy_id, "disabled_at_start", true)
for i, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
ComponentSetValue2(limb, "end_position", en_data.legs[2 * i - 2], en_data.legs[2 * i - 1])
end
end
local indexed = {}
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_death")
local has = false
for _, inx in ipairs(death_triggers) do
local script2 = constants.interned_index_to_filename[inx] or inx
if script == script2 then
has = true
indexed[script] = true
end
end
if not has then
ComponentSetValue2(com, "script_death", "")
end
end
for _, inx in ipairs(death_triggers) do
local script = constants.interned_index_to_filename[inx] or inx
if indexed[script] == nil then
EntityAddComponent(enemy_id, "LuaComponent", {
script_death = script,
execute_every_n_frame = "-1",
})
end
end
if ffi.typeof(en_data) == EnemyDataMom then
local orbs = en_data.orbs
for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
local damage_component = EntityGetFirstComponentIncludingDisabled(child, "DamageModelComponent")
if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
local n = ComponentGetValue2(var, "value_int")
if orbs[n - 1] then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
else
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
EntityKill(child)
end
end
end
end
effect_sync.apply_effects(effects, enemy_id, true)
if stains ~= nil then
stain_sync.sync_stains(stains, enemy_id)
end
end
local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
local item
if inv ~= nil then
item = ComponentGetValue2(inv, "mActualActiveItem")
end
if gid ~= nil and (item == nil or item == 0 or not EntityGetIsAlive(item)) then
local wand = item_sync.find_by_gid(gid)
if wand ~= nil and EntityGetIsAlive(wand) then
EntityAddTag(wand, "ew_client_item")
local inventory
for _, child in pairs(EntityGetAllChildren(enemy_id) or {}) do
if EntityGetName(child) == "inventory_quick" then
inventory = child
end
end
if inventory == nil then
inventory = EntityCreateNew("inventory_quick")
EntityAddChild(enemy_id, inventory)
end
if EntityGetParent(wand) ~= inventory then
if EntityGetParent(wand) ~= 0 then
EntityRemoveFromParent(wand)
end
EntityAddChild(inventory, wand)
end
np.SetActiveHeldEntity(enemy_id, wand, false, false)
elseif should_wait[gid] == nil or should_wait[gid] < GameGetFrameNum() then
item_sync.rpc.request_send_again(gid)
should_wait[gid] = GameGetFrameNum() + 15
end
end
for i, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
if animation[i] ~= nil then
ComponentSetValue2(sprite, "rect_animation", animation[i])
ComponentSetValue2(sprite, "next_rect_animation", animation[i])
end
end
::continue::
end
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
--[[if confirmed_kills[remote_id] then
GamePrint("Remote id has been killed already..?")
goto continue
end
confirmed_kills[remote_id] = true]]
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
if unsynced_enemys[remote_id] ~= nil then
sync_enemy(unsynced_enemys[remote_id], true, 60)
end
local enemy_data = ctx.entity_by_remote_id[remote_id]
if enemy_data ~= nil and EntityGetIsAlive(enemy_data.id) then
local enemy_id = enemy_data.id
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
ComponentSetValue2(damage_component, "ui_report_damage", false)
ComponentSetValue2(damage_component, "hp", 2 ^ -38)
end
-- Enable explosion back
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg + 0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
if not remote_data[3] then
EntityKill(enemy_id)
else
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
ComponentSetValue2(damage_component, "kill_now", true)
end
ctx.entity_by_remote_id[remote_id] = nil
end
::continue::
end
end
function rpc.handle_enemy_data(enemy_data, is_first)
if is_first then
for _, n in pairs(ctx.entity_by_remote_id) do
EntityKill(n.id)
end
ctx.entity_by_remote_id = {}
end
frame = GameGetFrameNum()
for _, enemy_info_raw in ipairs(enemy_data) do
sync_enemy(enemy_info_raw, false, ctx.rpc_player_data.fps)
end
end
function rpc.handle_enemy_health(enemy_health_data)
for _, en_data in ipairs(enemy_health_data) do
local remote_enemy_id = en_data.enemy_id
local hp = en_data.hp
local max_hp = en_data.max_hp
if
ctx.entity_by_remote_id[remote_enemy_id] == nil
or not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
then
goto continue
end
local enemy_data = ctx.entity_by_remote_id[remote_enemy_id]
local enemy_id = enemy_data.id
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp - hp
if dmg > 0 then
-- Make sure the enemy doesn't die from the next EntityInflictDamage.
if EntityGetName(enemy_id) ~= "$animal_boss_sky" then
util.set_ent_health(enemy_id, { dmg * 2, dmg * 2 })
else
util.set_ent_health(enemy_id, { hp + dmg, max_hp })
end
-- Deal damage, so that game displays damage numbers.
EntityInflictDamage(enemy_id, dmg, "DAMAGE_CURSE", "", "NONE", 0, 0, GameGetWorldStateEntity())
end
util.set_ent_health(enemy_id, { hp, max_hp })
::continue::
end
end
function enemy_sync.on_projectile_fired(
shooter_id,
projectile_id,
initial_rng,
position_x,
position_y,
target_x,
target_y,
send_message,
unknown1,
multicast_index,
unknown3
)
local not_a_player = not EntityHasTag(shooter_id, "ew_no_enemy_sync")
and not EntityHasTag(shooter_id, "player_unit")
and not EntityHasTag(shooter_id, "ew_client")
if not_a_player and ctx.is_host then
local projectileComponent = EntityGetFirstComponentIncludingDisabled(projectile_id, "ProjectileComponent")
if projectileComponent ~= nil then
local entity_that_shot = ComponentGetValue2(projectileComponent, "mEntityThatShot")
if entity_that_shot == 0 then
rpc.replicate_projectile(
util.serialize_entity(projectile_id),
position_x,
position_y,
target_x,
target_y,
shooter_id,
initial_rng
)
end
end
end
end
rpc.opts_reliable()
function rpc.replicate_projectile(seri_ent, position_x, position_y, target_x, target_y, remote_source_ent, rng)
if rng ~= nil then
np.SetProjectileSpreadRNG(rng)
end
if ctx.entity_by_remote_id[remote_source_ent] == nil then
return
end
local source_ent = ctx.entity_by_remote_id[remote_source_ent].id
local ent = util.deserialize_entity(seri_ent)
GameShootProjectile(source_ent, position_x, position_y, target_x, target_y, ent)
end
return enemy_sync

View file

@ -7,6 +7,10 @@ local initial_world_state_entity
local module = {}
local log = false
local cache = false
-- Used in ewext
EwextSerialize = util.serialize_entity
EwextDeserialize = util.deserialize_entity
@ -21,14 +25,12 @@ end
function module.on_world_initialized()
initial_world_state_entity = GameGetWorldStateEntity()
ewext.on_world_initialized()
local grid_world = world_ffi.get_grid_world()
local chunk_map = grid_world.vtable.get_chunk_map(grid_world)
grid_world = tonumber(ffi.cast("intptr_t", grid_world))
chunk_map = tonumber(ffi.cast("intptr_t", chunk_map))
local material_list = tonumber(ffi.cast("intptr_t", world_ffi.get_material_ptr(0)))
ewext.init_particle_world_state(grid_world, chunk_map, material_list)
ewext.module_on_world_init()
ewext.set_world_num(ctx.proxy_opt.world_num)
log = ModSettingGet("quant.ew.log_performance") or false
ewext.set_log(log)
cache = ModSettingGet("quant.ew.cache") or false
ewext.set_cache(cache)
end
local function oh_another_world_state(entity)
@ -80,6 +82,16 @@ function module.on_draw_debug_window(imgui)
end
function module.on_world_update()
local temp = ModSettingGet("quant.ew.log_performance") or false
if temp ~= log then
log = temp
ewext.set_log(log)
end
local temp = ModSettingGet("quant.ew.cache") or false
if temp ~= cache then
cache = temp
ewext.set_cache(cache)
end
if GameGetWorldStateEntity() ~= initial_world_state_entity then
oh_another_world_state(GameGetWorldStateEntity())
initial_world_state_entity = GameGetWorldStateEntity()
@ -87,12 +99,8 @@ function module.on_world_update()
ewext.module_on_world_update()
end
function module.on_new_entity(ent)
if not ctx.is_host and ctx.proxy_opt.disable_kummitus and EntityGetName(ent) == "$animal_playerghost" then
EntityKill(ent)
else
ewext.module_on_new_entity(ent)
end
function module.on_new_entity(arr)
ewext.module_on_new_entity(arr, #arr)
end
function module.on_projectile_fired(

View file

@ -64,7 +64,12 @@ local first = true
local function hole(item)
local ce = EntityGetFirstComponent(item, "CellEaterComponent")
if ce == nil or ComponentGetValue2(ce, "only_stain") or ComponentGetValue2(ce, "limited_materials") or EntityGetFirstComponent(item, "PhysicsBodyComponent") ~= nil then
if
ce == nil
or ComponentGetValue2(ce, "only_stain")
or ComponentGetValue2(ce, "limited_materials")
or EntityGetFirstComponent(item, "PhysicsBodyComponent") ~= nil
then
return
end
local r = 0
@ -170,9 +175,11 @@ local exists
local new_ents = {}
function mod.on_new_entity(ent)
function mod.on_new_entity(arr)
if ctx.is_host then
table.insert(new_ents, ent)
for _, ent in ipairs(arr) do
table.insert(new_ents, ent)
end
end
end

View file

@ -52,7 +52,7 @@ local function run_spawn_fn(fn_name, x, y, ...)
else
local mx, my = GameGetCameraPos()
local item_cost_component = EntityGetFirstComponentIncludingDisabled(eid, "ItemCostComponent")
if math.abs(mx - x) > 1024 or math.abs(my - y) > 1024 then
if math.abs(mx - x) > 512 or math.abs(my - y) > 512 then
if ComponentGetValue2(item_cost_component, "stealable") then
EntityAddComponent2(eid, "VariableStorageComponent", { _tags = "ew_try_stealable" })
ComponentSetValue2(item_cost_component, "stealable", false)

View file

@ -1,865 +0,0 @@
-- Synchronizes item pickup and item drop
ModLuaFileAppend("data/scripts/items/utility_box.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random_super.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
dofile_once("data/scripts/lib/coroutines.lua")
local rpc = net.new_rpc_namespace()
local item_sync = {}
local pending_remove = {}
local pickup_handlers = {}
local dead_entities = {}
local frame = {}
local gid_last_frame_updated = {}
local wait_on_send = {}
local wait_for_gid = {}
function rpc.open_chest(gid)
if wait_for_gid[gid] == nil or wait_for_gid[gid] < 10000 then
wait_for_gid[gid] = GameGetFrameNum() + 36000
wait_on_send[gid] = GameGetFrameNum() + 36000
local ent = item_sync.find_by_gid(gid)
if ent ~= nil then
local file
local name = EntityGetFilename(ent)
if name == "data/entities/items/pickup/utility_box.xml" then
file = "data/scripts/items/utility_box.lua"
elseif name == "data/entities/items/pickup/chest_random_super.xml" then
file = "data/scripts/items/chest_random_super.lua"
elseif name == "data/entities/items/pickup/chest_random.xml" then
file = "data/scripts/items/chest_random.lua"
end
if file ~= nil then
EntityAddComponent2(ent, "LuaComponent", {
script_source_file = file,
execute_on_added = true,
call_init_function = true,
})
end
end
end
end
util.add_cross_call("ew_chest_opened", function(chest_id)
local gid = item_sync.get_global_item_id(chest_id)
if gid ~= nil then
wait_for_gid[gid] = GameGetFrameNum() + 36000
wait_on_send[gid] = GameGetFrameNum() + 36000
rpc.open_chest(gid)
end
end)
util.add_cross_call("ew_item_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
local gid = item_sync.get_global_item_id(enemy_id)
if gid ~= nil then
table.insert(dead_entities, { gid, responsible })
end
end)
function item_sync.ensure_notify_component(ent)
local notify = EntityGetFirstComponentIncludingDisabled(ent, "LuaComponent", "ew_notify_component")
if notify == nil then
EntityAddComponent2(ent, "LuaComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_notify_component,ew_remove_on_send",
script_throw_item = "mods/quant.ew/files/resource/cbs/item_notify.lua",
script_item_picked_up = "mods/quant.ew/files/resource/cbs/item_notify.lua",
})
end
end
local function mark_in_inventory(my_player)
local items = inventory_helper.get_all_inventory_items(my_player)
for _, ent in pairs(items) do
if not EntityHasTag(ent, "polymorphed_player") then
item_sync.ensure_notify_component(ent)
end
end
end
local function allocate_global_id()
local current = tonumber(GlobalsGetValue("ew_global_item_id", "1"))
GlobalsSetValue("ew_global_item_id", tostring(current + 1))
return ctx.my_id .. ":" .. current
end
-- Try to guess if the item is in world.
local function is_item_on_ground(item)
return EntityGetRootEntity(item) == item
end
function item_sync.get_global_item_id(item)
local gid = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid == nil then
return nil
end
local ret = ComponentGetValue2(gid, "value_string")
return ret
end
local function is_wand(ent)
if ent == nil or ent == 0 then
return false
end
local ability = EntityGetFirstComponentIncludingDisabled(ent, "AbilityComponent")
if ability == nil then
return false
end
return ComponentGetValue2(ability, "use_gun_script") == true
end
local function is_safe_to_remove()
return not ctx.is_wand_pickup
end
function item_sync.remove_item_with_id(gid)
local item_ent_id = item_sync.find_by_gid(gid)
if is_safe_to_remove() or not is_wand(item_ent_id) then
item_sync.remove_item_with_id_now(gid)
else
table.insert(pending_remove, gid)
EntitySetTransform(item_ent_id, 0, 0)
util.make_ephemerial(item_ent_id)
end
end
local find_by_gid_cache = {}
function item_sync.find_by_gid(gid)
if find_by_gid_cache[gid] ~= nil then
if
EntityGetIsAlive(find_by_gid_cache[gid])
and EntityHasTag(find_by_gid_cache[gid], "ew_global_item")
and is_item_on_ground(find_by_gid_cache[gid])
then
return find_by_gid_cache[gid]
else
find_by_gid_cache[gid] = nil
end
end
--print("find_by_gid: searching")
local candidate
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local i_gid = item_sync.get_global_item_id(item)
if i_gid ~= nil then
find_by_gid_cache[i_gid] = item
if i_gid == gid then
if is_item_on_ground(item) then
return item
else
candidate = item
end
end
end
end
return candidate
end
function item_sync.remove_item_with_id_now(gid)
local item = item_sync.find_by_gid(gid)
if item ~= nil then
find_by_gid_cache[gid] = nil
for _, audio in ipairs(EntityGetComponent(item, "AudioComponent") or {}) do
if string.sub(ComponentGetValue2(audio, "event_root"), 1, 10) == "collision/" then
EntitySetComponentIsEnabled(item, audio, false)
end
end
EntityKill(item)
return item
end
end
function item_sync.host_localize_item(gid, peer_id)
if ctx.item_prevent_localize[gid] then
print("Item localize for " .. gid .. " prevented")
return
end
ctx.item_prevent_localize[gid] = true
if table.contains(pending_remove, gid) then
print("Item localize prevented, already taken")
return
end
local item_ent_id = item_sync.find_by_gid(gid)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
if peer_id ~= ctx.my_id then
item_sync.remove_item_with_id(gid)
end
rpc.item_localize(peer_id, gid)
if peer_id == ctx.my_id then
item_sync.take_authority(gid)
else
rpc.hand_authority_over_to(peer_id, gid)
end
end
local function make_global(item, give_authority_to)
if not EntityGetIsAlive(item) then
print("Thrown item vanished before we could send it")
return
end
item_sync.ensure_notify_component(item)
local gid_component =
EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
local gid
if gid_component == nil then
gid = allocate_global_id()
if give_authority_to ~= nil then
gid = give_authority_to .. ":" .. gid
end
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_global_item_id",
value_string = gid,
})
else
gid = ComponentGetValue2(gid_component, "value_string")
end
--local vel = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
--if vel then
-- local vx, vy = ComponentGetValue2(vel, "mVelocity")
--end
local item_data = inventory_helper.serialize_single_item(item)
item_data.gid = gid
local _, _, has_hp = util.get_ent_health(item)
if has_hp then
util.ensure_component_present(item, "LuaComponent", "ew_item_death_notify", {
script_death = "mods/quant.ew/files/resource/cbs/item_death_notify.lua",
})
end
ctx.item_prevent_localize[gid] = false
rpc.item_globalize(item_data)
if wait_on_send[gid] ~= nil then
wait_on_send[gid] = GameGetFrameNum() + 30
end
end
function item_sync.make_item_global(item, instant, give_authority_to)
EntityAddTag(item, "ew_global_item")
if instant then
make_global(item, give_authority_to)
else
async(function()
wait(1) -- Wait 1 frame so that game sets proper velocity.
make_global(item, give_authority_to)
end)
end
end
local function remove_client_items_from_world()
if GameGetFrameNum() % 5 ~= 3 then
return
end
for _, item in ipairs(EntityGetWithTag("ew_client_item")) do
if is_item_on_ground(item) then
item_sync.remove_item_with_id(item_sync.get_global_item_id(item))
end
end
end
local function is_peers_item(gid, peer)
if gid == nil then
return false
end
return string.sub(gid, 1, 16) == peer
end
function item_sync.is_my_item(gid)
if gid == nil then
return false
end
return string.sub(gid, 1, 16) == ctx.my_id
end
function item_sync.take_authority(gid)
if gid ~= nil and not item_sync.is_my_item(gid) then
local new_id = allocate_global_id()
rpc.give_authority_to(gid, new_id)
end
end
rpc.opts_everywhere()
rpc.opts_reliable()
function rpc.give_authority_to(gid, new_id)
local item
local to_remove = {}
for _, ent in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local i_gid = item_sync.get_global_item_id(ent)
if i_gid == gid then
if item == nil then
item = ent
else
table.insert(to_remove, gid)
end
end
end
find_by_gid_cache[gid] = nil
if table.contains(pending_remove, gid) then
for i, id in ipairs(pending_remove) do
if id == gid then
table.remove(pending_remove, i)
break
end
end
table.insert(pending_remove, new_id)
end
for _, g in ipairs(to_remove) do
item_sync.remove_item_with_id(g)
end
if item ~= nil then
find_by_gid_cache[new_id] = item
local var = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
ComponentSetValue2(var, "value_string", new_id)
end
end
rpc.opts_reliable()
function rpc.hand_authority_over_to(peer_id, gid)
if peer_id == ctx.my_id then
if item_sync.find_by_gid(gid) ~= nil then
item_sync.take_authority(gid)
elseif wait_for_gid[gid] == nil then
rpc.request_send_again(gid)
wait_for_gid[gid] = GameGetFrameNum() + 300
end
end
end
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
local enemy_id = item_sync.find_by_gid(remote_id)
if enemy_id ~= nil and EntityGetIsAlive(enemy_id) then
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
ComponentSetValue2(damage_component, "ui_report_damage", false)
ComponentSetValue2(damage_component, "hp", 2 ^ -38)
end
-- Enable explosion back
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg + 0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
EntityKill(enemy_id)
end
::continue::
end
end
local DISTANCE_LIMIT = 128 * 4
local ignore = {}
local function send_item_positions(all)
local position_data = {}
local cx, cy = GameGetCameraPos()
local cap = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item")) do
local gid = item_sync.get_global_item_id(item)
-- Only send info about items created by us.
local tg = EntityHasTag(item, "ew_no_spawn")
if gid ~= nil and item_sync.is_my_item(gid) and (is_item_on_ground(item) or tg) then
local x, y = EntityGetTransform(item)
local dx, dy = x - cx, y - cy
if
not tg
and (ignore[gid] == nil or ignore[gid] < GameGetFrameNum())
and dx * dx + dy * dy > 4 * DISTANCE_LIMIT * DISTANCE_LIMIT
then
local ent = EntityGetClosestWithTag(x, y, "ew_peer")
local nx, ny
local ndx, ndy
if ent ~= 0 then
nx, ny = EntityGetTransform(ent)
ndx, ndy = x - nx, y - ny
end
if ent == 0 or ndx * ndx + ndy * ndy > DISTANCE_LIMIT * DISTANCE_LIMIT then
ent = EntityGetClosestWithTag(x, y, "polymorphed_player")
if ent ~= 0 then
nx, ny = EntityGetTransform(ent)
ndx, ndy = x - nx, y - ny
end
if ent == 0 or ndx * ndx + ndy * ndy > DISTANCE_LIMIT * DISTANCE_LIMIT then
ignore[gid] = GameGetFrameNum() + 60
goto continue
end
end
local data = player_fns.get_player_data_by_local_entity_id(ent)
if data ~= nil then
local peer = data.peer_id
rpc.hand_authority_over_to(peer, gid)
ignore[gid] = nil
else
ignore[gid] = GameGetFrameNum() + 60
end
else
local phys_info = util.get_phys_info(item, true)
if
tg
or (
(phys_info[1][1] ~= nil or phys_info[2][1] ~= nil or all)
and (
#EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "ew_peer") ~= 0
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "polymorphed_player") ~= 0
)
)
then
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
local cost = 0
if costcom ~= nil then
cost = ComponentGetValue2(costcom, "cost")
local vel = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if math.abs(cx - x) < DISTANCE_LIMIT and math.abs(cy - y) < DISTANCE_LIMIT then
if
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_stealable"
) ~= nil
then
ComponentSetValue2(costcom, "stealable", true)
ComponentSetValue2(vel, "gravity_y", 400)
elseif
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_float"
) ~= nil
then
ComponentSetValue2(vel, "gravity_y", 400)
end
else
if
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_stealable"
) ~= nil
then
ComponentSetValue2(costcom, "stealable", false)
ComponentSetValue2(vel, "gravity_y", 0)
elseif
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_float"
) ~= nil
then
ComponentSetValue2(vel, "gravity_y", 0)
end
end
end
position_data[gid] = { x, y, phys_info, cost }
if EntityHasTag(item, "egg_item") then
if
EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_egg") ~= nil
then
position_data[gid][5] = true
end
elseif tg then
local f = EntityGetFilename(item)
if cap[f] == nil then
cap[f] = tonumber(ModSettingGet("quant.ew.rocks") or 16) or 16
end
if cap[f] == 0 then
position_data[gid] = nil
goto continue
end
cap[f] = cap[f] - 1
position_data[gid][5] = false
local velocity = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if velocity ~= nil then
local vx, vy = ComponentGetValue2(velocity, "mVelocity")
position_data[gid][6] = { vx, vy }
end
end
end
end
end
::continue::
end
rpc.update_positions(position_data, all)
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
end
util.add_cross_call("ew_thrown", function(thrown_item)
if
thrown_item ~= nil
and (item_sync.get_global_item_id(thrown_item) == nil or item_sync.is_my_item(
item_sync.get_global_item_id(thrown_item)
))
and EntityGetFirstComponentIncludingDisabled(thrown_item, "VariableStorageComponent", "ew_egg") == nil
then
item_sync.make_item_global(thrown_item)
end
end)
util.add_cross_call("ew_picked", function(picked_item)
if picked_item ~= nil and EntityHasTag(picked_item, "ew_global_item") then
local gid = item_sync.get_global_item_id(picked_item)
if gid ~= nil then
if ctx.is_host then
item_sync.host_localize_item(gid, ctx.my_id)
else
rpc.item_localize_req(gid)
end
end
end
end)
function item_sync.on_world_update()
-- TODO check that we not removing item we are going to pick now, instead of checking if picker gui is open.
if is_safe_to_remove() then
if #pending_remove > 0 then
local gid = table.remove(pending_remove)
item_sync.remove_item_with_id_now(gid)
end
end
if GameGetFrameNum() % 120 == 35 then
for _, ent in ipairs(EntityGetWithTag("mimic_potion")) do
if not EntityHasTag(ent, "polymorphed_player") and is_item_on_ground(ent) then
if not EntityHasTag(ent, "ew_global_item") then
if ctx.is_host then
item_sync.make_item_global(ent)
else
EntityKill(ent)
end
end
end
end
for _, wand in ipairs(EntityGetWithTag("wand")) do
local com = EntityGetFirstComponentIncludingDisabled(wand, "ItemComponent")
if com ~= nil then
ComponentSetValue2(com, "item_pickup_radius", 256)
end
end
end
local rt = math.floor(tonumber(ModSettingGet("quant.ew.item_sync") or 4) or 4 + 0.5)
local n = 0
if rt == 5 then
n = 3
elseif rt == 3 then
n = 1
elseif rt == 4 then
n = 2
end
if GameGetFrameNum() % 60 == 3 then
send_item_positions(true)
elseif rt == 1 or GameGetFrameNum() % rt == n then
send_item_positions(false)
end
if GameGetFrameNum() % 30 == 23 then
for gid, num in pairs(wait_for_gid) do
if num < GameGetFrameNum() then
wait_for_gid[gid] = nil
end
end
end
if GameGetFrameNum() % 5 == 4 then
mark_in_inventory(ctx.my_player)
end
remove_client_items_from_world()
end
function item_sync.on_should_send_updates()
if not ctx.is_host then
return
end
local item_list = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
if is_item_on_ground(item) and not EntityHasTag(item, "mimic_potion") then
local item_data = inventory_helper.serialize_single_item(item)
local gid = item_sync.get_global_item_id(item)
if gid ~= nil then
item_data.gid = gid
table.insert(item_list, item_data)
end
end
end
rpc.initial_items(item_list)
end
function item_sync.on_draw_debug_window(imgui)
local mx, my = DEBUG_GetMouseWorld()
local ent = EntityGetClosestWithTag(mx, my, "ew_global_item")
if ent ~= nil and ent ~= 0 then
if imgui.CollapsingHeader("Item gid") then
local x, y = EntityGetTransform(ent)
GameCreateSpriteForXFrames("mods/quant.ew/files/resource/debug/marker.png", x, y, true, 0, 0, 1, true)
local gid = item_sync.get_global_item_id(ent)
imgui.Text("GID: " .. tostring(gid))
local prevented = ctx.item_prevent_localize[gid]
if prevented then
imgui.Text("Localize prevented")
else
imgui.Text("Localize allowed")
end
local on_ground, reason = is_item_on_ground(ent)
if on_ground then
imgui.Text("On ground: " .. reason)
else
imgui.Text("Not on ground: " .. reason)
end
end
end
end
local function add_stuff_to_globalized_item(item, gid)
EntityAddTag(item, "ew_global_item")
item_sync.ensure_notify_component(item)
local gid_c = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid_c == nil then
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "ew_global_item_id",
value_string = gid,
})
else
ComponentSetValue2(gid_c, "value_string", gid)
end
ctx.item_prevent_localize[gid] = false
end
rpc.opts_reliable()
function rpc.initial_items(item_list)
-- Only run once ever, as it tends to duplicate items otherwise
if GameHasFlagRun("ew_initial_items") then
return
end
GameAddFlagRun("ew_initial_items")
for _, item_data in ipairs(item_list) do
local item = item_sync.find_by_gid(item_data.gid)
if item == nil then
local item_new = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item_new, item_data.gid)
end
end
end
rpc.opts_reliable()
function rpc.item_globalize(item_data)
if wait_for_gid[item_data.gid] ~= nil then
if wait_for_gid[item_data.gid] > GameGetFrameNum() + 10000 then
return
end
wait_for_gid[item_data.gid] = GameGetFrameNum() + 30
end
local a = item_sync.find_by_gid(item_data.gid)
if is_safe_to_remove() or not is_wand(a) then
local k = item_sync.remove_item_with_id_now(item_data.gid)
local n = item_sync.find_by_gid(item_data.gid)
if n ~= nil and k ~= n then
return
end
else
local n = item_sync.find_by_gid(item_data.gid)
if n ~= nil then
return
end
end
local item = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item, item_data.gid)
for _, com in ipairs(EntityGetComponent(item, "VariableStorageComponent") or {}) do
if ComponentGetValue2(com, "name") == "throw_time" then
ComponentSetValue2(com, "value_int", GameGetFrameNum())
end
end
local damage_component = EntityGetFirstComponentIncludingDisabled(item, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
EntityAddComponent2(item, "LuaComponent", {
_tags = "ew_immortal",
script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua",
})
end
end
rpc.opts_reliable()
function rpc.item_localize(l_peer_id, item_id)
local item_ent_id = item_sync.find_by_gid(item_id)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
if l_peer_id ~= ctx.my_id then
item_sync.remove_item_with_id(item_id)
end
end
rpc.opts_reliable()
function rpc.item_localize_req(gid)
if not ctx.is_host then
return
end
item_sync.host_localize_item(gid, ctx.rpc_peer_id)
end
local function cleanup(peer)
for gid, num in pairs(gid_last_frame_updated[peer]) do
if frame[peer] > num then
local item = item_sync.find_by_gid(gid)
if is_item_on_ground(item) then
item_sync.remove_item_with_id(gid)
gid_last_frame_updated[peer][gid] = nil
end
end
end
local is_duplicate = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local gid = item_sync.get_global_item_id(item)
if gid ~= nil and is_peers_item(gid, peer) then
if is_duplicate[gid] then
item_sync.remove_item_with_id(gid)
else
is_duplicate[gid] = true
end
end
end
end
function rpc.kill_egg(gid)
item_sync.remove_item_with_id_now(gid)
end
function rpc.update_positions(position_data, all)
if frame[ctx.rpc_peer_id] == nil or all then
frame[ctx.rpc_peer_id] = GameGetFrameNum()
if gid_last_frame_updated[ctx.rpc_peer_id] == nil then
gid_last_frame_updated[ctx.rpc_peer_id] = {}
end
end
local cx, cy = GameGetCameraPos()
for gid, el in pairs(position_data) do
if table.contains(pending_remove, gid) then
goto continue
end
local x, y = el[1], el[2]
local name = EntityGetFilename(item)
local is_chest = name == "data/entities/items/pickup/utility_box.xml"
or name == "data/entities/items/pickup/chest_random_super.xml"
or name == "data/entities/items/pickup/chest_random.xml"
if is_chest or el[5] ~= nil or (math.abs(x - cx) < DISTANCE_LIMIT and math.abs(y - cy) < DISTANCE_LIMIT) then
if el[5] == nil then
gid_last_frame_updated[ctx.rpc_peer_id][gid] = frame[ctx.rpc_peer_id]
end
local phys_info = el[3]
local price = el[4]
local item = item_sync.find_by_gid(gid)
if item ~= nil then
if not util.set_phys_info(item, phys_info, ctx.rpc_player_data.fps) then
EntitySetTransform(item, x, y)
if el[6] ~= nil then
local vx, vy = el[6][1], el[6][2]
local velocity = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if velocity ~= nil then
ComponentSetValue2(velocity, "mVelocity", vx, vy)
end
end
end
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
if costcom ~= nil then
if price == 0 then
EntitySetComponentsWithTagEnabled(item, "shop_cost", false)
ComponentSetValue2(costcom, "cost", 0)
else
EntitySetComponentsWithTagEnabled(item, "shop_cost", true)
ComponentSetValue2(costcom, "cost", price)
end
end
elseif wait_for_gid[gid] == nil then
if el[5] == true then
rpc.kill_egg(gid)
elseif el[5] ~= false then
util.log("Requesting again " .. gid)
rpc.request_send_again(gid)
wait_for_gid[gid] = GameGetFrameNum() + 300
end
end
end
::continue::
end
if all then
cleanup(ctx.rpc_peer_id)
end
end
function rpc.request_send_again(gid)
if gid ~= nil and not item_sync.is_my_item(gid) then
return
end
local item = item_sync.find_by_gid(gid)
if item == nil then
util.log("Requested to send item again, but this item wasn't found: " .. gid)
return
end
if wait_on_send[gid] == nil or wait_on_send[gid] < GameGetFrameNum() then
wait_on_send[gid] = GameGetFrameNum() + 240
item_sync.make_item_global(item)
end
end
ctx.cap.item_sync = {
globalize = item_sync.make_item_global,
register_pickup_handler = function(handler)
table.insert(pickup_handlers, handler)
end,
}
item_sync.rpc = rpc
return item_sync

View file

@ -11,12 +11,15 @@ local function orbs_found_this_run()
return ComponentGetValue2(wsc, "orbs_found_thisrun")
end
local spawned_orbs = {}
local function actual_orbs_update(found_orbs)
local found_local = orbs_found_this_run()
for _, orb in ipairs(found_orbs) do
if table.contains(found_local, orb) then
if table.contains(found_local, orb) or table.contains(spawned_orbs, orb) then
goto continue
end
table.insert(spawned_orbs, orb)
local orb_ent = EntityCreateNew()
EntityAddTag(orb_ent, "ew_no_enemy_sync")
EntityAddComponent2(orb_ent, "ItemComponent", {
@ -37,6 +40,7 @@ local function actual_orbs_update(found_orbs)
last_orb_count = GameGetOrbCountThisRun()
end
rpc.opts_reliable()
function rpc.update_orbs(found_orbs)
if ctx.rpc_peer_id ~= ctx.host_id and not ctx.is_host then
return
@ -58,22 +62,24 @@ function rpc.update_orbs(found_orbs)
actual_orbs_update(found_orbs)
end
function module.on_new_entity(ent)
local comp = EntityGetFirstComponent(ent, "OrbComponent")
if comp ~= nil then
local found_local = orbs_found_this_run()
local orb = ComponentGetValue2(comp, "orb_id")
if table.contains(found_local, orb) then
function module.on_new_entity(arr)
for _, ent in ipairs(arr) do
local comp = EntityGetFirstComponent(ent, "OrbComponent")
if comp ~= nil then
local found_local = orbs_found_this_run()
local orb = ComponentGetValue2(comp, "orb_id")
if table.contains(found_local, orb) then
EntityKill(ent)
end
elseif EntityGetFilename(ent) == "data/entities/base_item.xml" then
EntityKill(ent)
end
elseif EntityGetFilename(ent) == "data/entities/base_item.xml" then
EntityKill(ent)
end
local com = EntityGetFirstComponentIncludingDisabled(ent, "AbilityComponent")
if com ~= nil and ComponentGetValue2(com, "use_gun_script") then
com = EntityGetFirstComponentIncludingDisabled(ent, "ItemComponent")
if com ~= nil then
ComponentSetValue2(com, "item_pickup_radius", 256)
local com = EntityGetFirstComponentIncludingDisabled(ent, "AbilityComponent")
if com ~= nil and ComponentGetValue2(com, "use_gun_script") then
com = EntityGetFirstComponentIncludingDisabled(ent, "ItemComponent")
if com ~= nil then
ComponentSetValue2(com, "item_pickup_radius", 256)
end
end
end
end

View file

@ -49,6 +49,10 @@ function cos.player_color(player_entity)
local x, y = EntityGetTransform(ctx.my_player.entity)
local cape2 = EntityLoad(player_cape_sprite_file, x, y)
EntityAddChild(player_entity, cape2)
EntityRefreshSprite(player_entity, player_sprite_component)
if player_sprite_component_lukki ~= nil then
EntityRefreshSprite(player_entity, player_sprite_component_lukki)
end
end
function cos.player_cosmetics(player_entity)

View file

@ -729,12 +729,14 @@ function pvp.on_world_update()
end
end
function pvp.on_new_entity(ent)
if
EntityGetFirstComponentIncludingDisabled(ent, "TeleportComponent") ~= nil
and string.sub(EntityGetFilename(ent), 1, 24) == "data/entities/buildings/"
then
EntityKill(ent)
function pvp.on_new_entity(arr)
for _, ent in ipairs(arr) do
if
EntityGetFirstComponentIncludingDisabled(ent, "TeleportComponent") ~= nil
and string.sub(EntityGetFilename(ent), 1, 24) == "data/entities/buildings/"
then
EntityKill(ent)
end
end
end

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