···11+{
22+ description = "Noita Utility Box - a collection of memory-reading utilities for the game Noita";
33+44+ # I don't understand like a quarter of this flake, haven't slept for the last 48 hours lol
55+ # Adapted from https://gitlab.com/mud-rs/milk/-/blob/56f03874c577261f2c520461aebddd47c649ea30/flake.nix
66+ # But suprisingly, it worked quickly, not complaining
77+88+ inputs = {
99+ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
1010+ flake-utils.url = "github:numtide/flake-utils";
1111+ naersk = {
1212+ url = "github:nix-community/naersk";
1313+ inputs.nixpkgs.follows = "nixpkgs";
1414+ };
1515+ fenix = {
1616+ url = "github:nix-community/fenix";
1717+ inputs.nixpkgs.follows = "nixpkgs";
1818+ };
1919+ };
2020+2121+ nixConfig = {
2222+ extra-substituters = [ "https://necauqua.cachix.org" ];
2323+ extra-trusted-public-keys = [ "necauqua.cachix.org-1:XG5McOG0XwQ9kayUuEiEn0cPoLAMvc2TVs3fXqv/7Uc=" ];
2424+ };
2525+2626+ outputs = { self, nixpkgs, naersk, flake-utils, fenix }:
2727+ let
2828+ cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
2929+ name = cargoToml.package.name;
3030+ version = cargoToml.package.version;
3131+ in
3232+ flake-utils.lib.eachDefaultSystem
3333+ (system:
3434+ let
3535+ pkgs = import nixpkgs {
3636+ inherit system;
3737+ overlays = [ fenix.overlays.default ];
3838+ };
3939+ lib = pkgs.lib;
4040+ toolchain = with pkgs.fenix;
4141+ combine [
4242+ (complete.withComponents [
4343+ "cargo"
4444+ "clippy"
4545+ "rust-src"
4646+ "rustc"
4747+ ])
4848+ targets.x86_64-unknown-linux-musl.latest.rust-std
4949+ targets.x86_64-pc-windows-gnu.latest.rust-std
5050+ ];
5151+ # Make naersk aware of the tool chain which is to be used.
5252+ naersk-lib = naersk.lib.${system}.override {
5353+ cargo = toolchain;
5454+ rustc = toolchain;
5555+ };
5656+ buildPackage = target: { nativeBuildInputs ? [ ], ... }@args:
5757+ naersk-lib.buildPackage (
5858+ {
5959+ inherit name version;
6060+ src = ./.;
6161+ doCheck = false; # a test or two that I left in there are *not* unit tests lol
6262+ strictDeps = true;
6363+ }
6464+ // (lib.optionalAttrs (target != system) {
6565+ CARGO_BUILD_TARGET = target;
6666+ })
6767+ // args
6868+ // {
6969+ nativeBuildInputs = [ pkgs.fenix.complete.rustfmt-preview ] ++ nativeBuildInputs;
7070+ }
7171+ );
7272+ in
7373+ rec {
7474+ packages = {
7575+ default = buildPackage system {
7676+ # todo make sure this is less cringe
7777+ nativeBuildInputs = [ pkgs.makeWrapper ];
7878+ postInstall = ''
7979+ wrapProgram $out/bin/${name} \
8080+ --prefix LD_LIBRARY_PATH : ${with pkgs; lib.makeLibraryPath [
8181+ vulkan-loader
8282+ libxkbcommon
8383+ wayland
8484+8585+ # not sure those are exactly what's needed on X11
8686+ xorg.libX11
8787+ xorg.libXcursor
8888+ xorg.libXi
8989+ xorg.libXrandr
9090+ ]}
9191+ '';
9292+ };
9393+ x86_64-unknown-linux-musl = buildPackage "x86_64-unknown-linux-musl" {
9494+ nativeBuildInputs = with pkgs; [ pkgsStatic.stdenv.cc ];
9595+ CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS = "-C target-feature=+crt-static";
9696+ };
9797+ x86_64-pc-windows-gnu = buildPackage "x86_64-pc-windows-gnu" {
9898+9999+ # we can run tests with wine ig, cool
100100+ # this needs some fixing tho
101101+ # doCheck = system == "x86_64-linux";
102102+ # nativeBuildInputs = lib.optional doCheck pkgs.wineWowPackages.stable;
103103+ # CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER = pkgs.writeScript "wine-wrapper" ''
104104+ # # Without this, wine will error out when attempting to create the
105105+ # # prefix in the build's homeless shelter.
106106+ # export WINEPREFIX="$(mktemp -d)"
107107+ # exec wine64 $@
108108+ # '';
109109+110110+ depsBuildBuild = with pkgs.pkgsCross.mingwW64; [
111111+ stdenv.cc
112112+ windows.pthreads
113113+ ];
114114+ };
115115+ };
116116+117117+ apps.default = {
118118+ type = "app";
119119+ program = "${packages.default}/bin/${name}";
120120+ };
121121+122122+ devShells.default = pkgs.mkShell {
123123+ inputsFrom = builtins.attrValues packages;
124124+ nativeBuildInputs = with pkgs; [
125125+ rust-analyzer-nightly
126126+ cargo-udeps
127127+ cargo-nextest
128128+129129+ wineWowPackages.staging
130130+ ];
131131+132132+ RUST_BACKTRACE = "full";
133133+ RUST_LOG = "info,wgpu_core=warn,wgpu_hal=warn,zbus=warn,noita_utility_box=trace";
134134+ };
135135+ }
136136+ ) // {
137137+ # huh?. we doing hydra now?
138138+ hydraJobs = {
139139+ inherit (self.packages) x86_64-linux;
140140+ };
141141+ };
142142+}
143143+
+23-4
readme.md
···11-## noita-counter
11+## noita-utility-box
2233-This is a little cheatengine-style memory reader that reads a few statistics
33+This is a cheatengine-style memory reader that reads useful data
44directly from a running instance of [Noita](https://noitagame.com).
5566-This is useful if you want **no** mods to be installed yet want to have an
77-automatic counter for winstreaking, for example.
66+This is useful if you want **no** mods to be installed yet want to do dome
77+advanced stuff.
88+99+### Tools
1010+#### Orb Radar
1111+It has an orb radar tool which fully automatically finds 34th orb GTG locations
1212+for the running game and tracks player coords in realtime.
1313+1414+#### Live Stats
1515+Automatically gets current death/win/streak/best-streak counts, formats them
1616+and sets an OBS text input (through obs-websocket which you can enable in OBS
1717+menus)
1818+1919+#### .. more coming
2020+There are plans for more stuff to come, some low handing fruits like hitless
2121+checker, less low ones like modless streamer wands, I want to do a git backup
2222+manager, a mod update manager, etc etc
2323+2424+## License
2525+It's MIT, please have a copy of the LICENSE file in your derivatives so that my
2626+name is there lol
···11+use std::{borrow::Cow, ffi::CStr};
22+33+use iced_x86::{Code, Register};
44+use memchr::memmem;
55+66+use crate::memory::exe_image::ExeImage;
77+88+use super::NoitaGlobals;
99+1010+/// Assuming Lua API functions are set up like this..
1111+/// ```c
1212+/// lua_pushcclosure(L,function_pointer,0);
1313+/// lua_setfield(L,LUA_GLOBALSINDEX,"UniqueString");
1414+/// ```
1515+/// ..we look for the `PUSH imm32` of the unique string given as `name`, and
1616+/// then we look if there is a `PUSH imm32` at 8 bytes before that
1717+/// (`CALL EDI => lua_pushcclosure` and `PUSH EBX` being 3 bytes, and
1818+/// 5 bytes for the `PUSH imm32` image), and return it's argument.
1919+///
2020+/// Note that this completely breaks (already) with noita_dev.exe lol
2121+fn find_lua_api_fn(image: &ExeImage, name: &CStr) -> Option<u32> {
2222+ match image.text()[image.find_push_str_pos(name)? - 8..] {
2323+ [0x68, a, b, c, d, ..] => {
2424+ let addr = u32::from_le_bytes([a, b, c, d]);
2525+ tracing::debug!("Found Lua API function {name:?} at 0x{addr:x}");
2626+ Some(addr)
2727+ }
2828+ _ => {
2929+ tracing::warn!("Did not find Lua API function {name:?}");
3030+ None
3131+ }
3232+ }
3333+}
3434+3535+/// We look for the `SetRandomSeed` Lua API function and then we look for
3636+/// the `mov eax, [addr]` and `add eax, [addr]` instructions, which
3737+/// correspond to WORLD_SEED + NEW_GAME_PLUS_COUNT being passed as a second
3838+/// parameter of a (SetRandomSeedImpl) function call.
3939+fn find_seed_pointers(image: &ExeImage) -> Option<(u32, u32)> {
4040+ let mut state = None;
4141+ for instr in image.decode_fn(find_lua_api_fn(image, c"SetRandomSeed")?) {
4242+ state = match state {
4343+ None if instr.code() == Code::Mov_EAX_moffs32 => Some(instr.memory_displacement32()),
4444+ // allow the `add esp, 0x10` thing in between
4545+ Some(addr) if instr.code() == Code::Add_rm32_imm8 => Some(addr),
4646+ Some(addr)
4747+ if instr.code() == Code::Add_r32_rm32 && instr.op0_register() == Register::EAX =>
4848+ {
4949+ return Some((addr, instr.memory_displacement32()));
5050+ }
5151+ _ => None,
5252+ };
5353+ }
5454+ None
5555+}
5656+5757+/// We look for the `GamePrint` Lua API function and then we look at the third
5858+/// `CALL rel32` instruction from the end, which is a call to `GetGameGlobal`
5959+/// (as I call it).
6060+///
6161+/// Then we look for the `MOV moffs32, EAX` instruction which is the assignment
6262+/// to the pointer of the GameGlobal structure.
6363+fn find_game_global_pointer(image: &ExeImage) -> Option<u32> {
6464+ let third_from_last_call_rel = image
6565+ .decode_fn(find_lua_api_fn(image, c"GamePrint")?)
6666+ .filter(|instr| instr.code() == Code::Call_rel32_32)
6767+ .collect::<Vec<_>>()
6868+ .into_iter()
6969+ .rev()
7070+ .nth(2)?;
7171+7272+ image
7373+ .decode_fn(third_from_last_call_rel.near_branch32())
7474+ .find(|instr| {
7575+ instr.code() == Code::Mov_moffs32_EAX && instr.segment_prefix() == Register::None
7676+ })
7777+ .map(|instr| instr.memory_displacement32())
7878+}
7979+8080+/// We look for the `AddFlagPersistent` Lua API function and then we look
8181+/// for second-to-last `CALL rel32`, the last being some C++ exception
8282+/// thing, and the second-to-last being a call to `AddFlagPersistentImpl`,
8383+/// as I call it.
8484+///
8585+/// Then inside of that we look for `MOV ECX imm32` which is specifically
8686+/// after `CALL rel32` which is after `MOV EDX, "progress_ending1"`.
8787+/// The call being to a string equality check and our MOV being an
8888+/// argument to a following call which is the global KEY_VALUE_STATS map
8989+/// pointer.
9090+fn find_stats_map_pointer(image: &ExeImage) -> Option<u32> {
9191+ let mut before_last_call_rel = None;
9292+ let mut last_call_rel = None;
9393+ for instr in image.decode_fn(find_lua_api_fn(image, c"AddFlagPersistent")?) {
9494+ if instr.code() == Code::Call_rel32_32 {
9595+ before_last_call_rel = last_call_rel;
9696+ last_call_rel = Some(instr.near_branch32());
9797+ }
9898+ }
9999+100100+ let end1_addr = image.find_string(c"progress_ending1")?;
101101+102102+ enum State {
103103+ Init,
104104+ FoundProgressEnding1,
105105+ FoundStreqCall,
106106+ }
107107+ let mut state = State::Init;
108108+109109+ for instr in image.decode_fn(before_last_call_rel?) {
110110+ match state {
111111+ State::Init
112112+ if instr.code() == Code::Mov_r32_imm32
113113+ && instr.op0_register() == Register::EDX
114114+ && instr.immediate32() == end1_addr =>
115115+ {
116116+ state = State::FoundProgressEnding1;
117117+ }
118118+ State::FoundProgressEnding1 if instr.code() == Code::Call_rel32_32 => {
119119+ state = State::FoundStreqCall;
120120+ }
121121+ State::FoundStreqCall
122122+ if instr.code() == Code::Mov_r32_imm32 && instr.op0_register() == Register::ECX =>
123123+ {
124124+ return Some(instr.immediate32());
125125+ }
126126+ _ => {}
127127+ };
128128+ }
129129+ None
130130+}
131131+132132+/// We look for the `EntityGetParent` Lua API function and there we look
133133+/// for `MOV ECX, [addr]` which immediately follows a Lua call - that MOV
134134+/// happens to be setting up an argument to a following relative call that
135135+/// is the pointer to the entity manager global.
136136+fn find_entity_manager_pointer(image: &ExeImage) -> Option<u32> {
137137+ let mut state = false;
138138+139139+ for instr in image.decode_fn(find_lua_api_fn(image, c"EntityGetParent")?) {
140140+ state = match state {
141141+ false if instr.code() == Code::Call_rm32 => true,
142142+ true if instr.code() == Code::Mov_r32_rm32 && instr.op0_register() == Register::ECX => {
143143+ return Some(instr.memory_displacement32());
144144+ }
145145+ _ => false,
146146+ };
147147+ }
148148+ None
149149+}
150150+151151+/// Look for the `EntityHasTag` Lua API function and then look for the
152152+/// second to last `CALL rel32` again, which is a call that accepts the
153153+/// entity tag manager global in ECX
154154+fn find_entity_tag_manager_pointer(image: &ExeImage) -> Option<u32> {
155155+ let mut before_last_call_rel = None;
156156+ let mut last_call_rel = None;
157157+158158+ let instrs = image
159159+ .decode_fn(find_lua_api_fn(image, c"EntityHasTag")?)
160160+ .enumerate()
161161+ .map(|(i, instr)| {
162162+ if instr.code() == Code::Call_rel32_32 {
163163+ before_last_call_rel = last_call_rel;
164164+ last_call_rel = Some(i);
165165+ }
166166+ instr
167167+ })
168168+ .collect::<Vec<_>>();
169169+170170+ instrs[..before_last_call_rel?]
171171+ .iter()
172172+ .rev()
173173+ .find(|instr| instr.code() == Code::Mov_r32_rm32 && instr.op0_register() == Register::ECX)
174174+ .map(|instr| instr.memory_displacement32())
175175+}
176176+177177+/// Look for the `EntityGetComponent` Lua API function and then look for
178178+/// a `CALL rel32` instruction that immediately follows a `PUSH EAX`,
179179+/// it's a call to `GetComponentTypeManager` (as I call it).
180180+///
181181+/// Then we look for the `MOV EAX, imm32` instruction which the return
182182+/// of the component type manager global pointer.
183183+fn find_component_type_manager_pointer(image: &ExeImage) -> Option<u32> {
184184+ let mut state = false;
185185+ let mut found = None;
186186+187187+ for instr in image.decode_fn(find_lua_api_fn(image, c"EntityGetComponent")?) {
188188+ state = match state {
189189+ false if instr.code() == Code::Push_r32 && instr.op0_register() == Register::EAX => {
190190+ true
191191+ }
192192+ true if instr.code() == Code::Call_rel32_32 => {
193193+ found = Some(instr.near_branch32());
194194+ break;
195195+ }
196196+ _ => false,
197197+ };
198198+ }
199199+200200+ image
201201+ .decode_fn(found?)
202202+ .find(|instr| instr.code() == Code::Mov_r32_imm32)
203203+ .map(|instr| instr.immediate32())
204204+}
205205+206206+/// It's actually almost same as the PE timestamp I've been using, but
207207+/// they might have some more human-readable stuff here.
208208+pub fn find_noita_build(image: &ExeImage) -> Option<Cow<str>> {
209209+ let pos = memmem::find(image.rdata(), b"Noita - Build ")?;
210210+211211+ // + 8 to skip the "Noita - " part
212212+ let prefix = image.rdata()[pos + 8..].split(|b| *b == 0).next()?;
213213+ Some(String::from_utf8_lossy(prefix))
214214+}
215215+216216+pub fn run(image: &ExeImage) -> NoitaGlobals {
217217+ let mut g = NoitaGlobals::default();
218218+219219+ let seed = find_seed_pointers(image);
220220+ g.world_seed = seed.map(|(seed, _)| seed).map(|p| p.into());
221221+ g.ng_count = seed.map(|(_, ng)| ng).map(|p| p.into());
222222+ g.global_stats = find_stats_map_pointer(image).map(|p| (p - 0x18).into());
223223+ g.game_global = find_game_global_pointer(image).map(|p| p.into());
224224+ g.entity_manager = find_entity_manager_pointer(image).map(|p| p.into());
225225+ g.entity_tag_manager = find_entity_tag_manager_pointer(image).map(|p| p.into());
226226+ g.component_type_manager = find_component_type_manager_pointer(image).map(|p| p.into());
227227+228228+ g
229229+}
230230+231231+#[cfg(test)]
232232+mod tests {
233233+ use crate::memory::exe_image::PeHeader;
234234+235235+ use super::*;
236236+237237+ use std::time::Instant;
238238+239239+ use sysinfo::ProcessesToUpdate;
240240+ use tracing::level_filters::LevelFilter;
241241+ use tracing_subscriber::EnvFilter;
242242+243243+ #[test]
244244+ fn test() -> anyhow::Result<()> {
245245+ tracing_subscriber::fmt()
246246+ .with_env_filter(
247247+ EnvFilter::builder()
248248+ .with_default_directive(LevelFilter::DEBUG.into())
249249+ .from_env()?,
250250+ )
251251+ .init();
252252+253253+ let mut system = sysinfo::System::new();
254254+ system.refresh_processes(ProcessesToUpdate::All);
255255+256256+ let Some(noita_pid) = system
257257+ .processes_by_exact_name("noita.exe".as_ref())
258258+ .find(|p| p.thread_kind().is_none())
259259+ else {
260260+ eprintln!("Noita process not found");
261261+ return Ok(());
262262+ };
263263+264264+ let proc = noita_pid.pid().as_u32().try_into()?;
265265+ let header = PeHeader::read(&proc)?;
266266+ if header.timestamp() != 0x66ba59d6 {
267267+ eprintln!("Timestamp mismatch: 0x{:x}", header.timestamp());
268268+ return Ok(());
269269+ }
270270+271271+ let instant = Instant::now();
272272+ let image = header.read_image(&proc)?;
273273+ println!("Image read in {:?}", instant.elapsed());
274274+275275+ let instant = Instant::now();
276276+ let globals = run(&image);
277277+ println!("Pointers found in {:?}", instant.elapsed());
278278+279279+ println!("{globals:#?}");
280280+281281+ // destructure so we know to update this when growing the list lol
282282+ let NoitaGlobals {
283283+ world_seed,
284284+ ng_count,
285285+ global_stats,
286286+ game_global,
287287+ entity_manager,
288288+ entity_tag_manager,
289289+ component_type_manager,
290290+ } = NoitaGlobals::debug();
291291+292292+ assert_eq!(globals.world_seed, world_seed);
293293+ assert_eq!(globals.ng_count, ng_count);
294294+ assert_eq!(globals.global_stats, global_stats);
295295+ assert_eq!(globals.game_global, game_global);
296296+ assert_eq!(globals.entity_manager, entity_manager);
297297+ assert_eq!(globals.entity_tag_manager, entity_tag_manager);
298298+ assert_eq!(globals.component_type_manager, component_type_manager);
299299+300300+ Ok(())
301301+ }
302302+}
+345
src/noita/mod.rs
···11+use std::{collections::HashMap, io, marker::PhantomData};
22+33+use derive_more::{derive::Display, Debug};
44+use types::{
55+ components::ComponentName, ComponentBuffer, ComponentTypeManager, Entity, EntityManager,
66+ GameGlobal, GlobalStats, TagManager,
77+};
88+99+use crate::memory::{MemoryStorage, Pod, ProcessRef, Ptr};
1010+1111+pub mod discovery;
1212+pub mod rng;
1313+pub mod types;
1414+1515+#[derive(Debug, Clone)]
1616+pub struct Noita {
1717+ proc: ProcessRef,
1818+ g: NoitaGlobals,
1919+ entity_tag_cache: HashMap<String, u8>,
2020+ no_player_not_polied: bool,
2121+2222+ materials: Vec<String>,
2323+ material_ui_names: Vec<String>,
2424+}
2525+2626+#[derive(Debug, Default, Clone)]
2727+pub struct NoitaGlobals {
2828+ pub world_seed: Option<Ptr<u32>>,
2929+ pub ng_count: Option<Ptr<u32>>,
3030+ pub global_stats: Option<Ptr<GlobalStats>>,
3131+ pub game_global: Option<Ptr<Ptr<GameGlobal>>>,
3232+ pub entity_manager: Option<Ptr<Ptr<EntityManager>>>,
3333+ pub entity_tag_manager: Option<Ptr<Ptr<TagManager>>>,
3434+ pub component_type_manager: Option<Ptr<ComponentTypeManager>>,
3535+}
3636+3737+impl NoitaGlobals {
3838+ pub fn debug() -> Self {
3939+ Self {
4040+ world_seed: Some(Ptr::of(0x1202fe4)),
4141+ ng_count: Some(Ptr::of(0x1203004)),
4242+ global_stats: Some(Ptr::of(0x1206920)),
4343+ game_global: Some(Ptr::of(0x0122172c)),
4444+ entity_manager: Some(Ptr::of(0x1202b78)),
4545+ entity_tag_manager: Some(Ptr::of(0x1204fbc)),
4646+ component_type_manager: Some(Ptr::of(0x01221c08)),
4747+ }
4848+ }
4949+}
5050+5151+macro_rules! not_found {
5252+ ($($args:tt)*) => {
5353+ || ::std::io::Error::new(::std::io::ErrorKind::NotFound, format!($($args)*))
5454+ };
5555+}
5656+5757+macro_rules! read_ptr {
5858+ ($self:ident.$ident:ident) => {
5959+ $self
6060+ .g
6161+ .$ident
6262+ .ok_or_else(not_found!(concat!("No ", stringify!($ident), " pointer")))?
6363+ .read(&$self.proc)?
6464+ };
6565+}
6666+6767+pub trait TagRef {
6868+ fn get_tag_index(&self, noita: &mut Noita) -> io::Result<Option<u8>>;
6969+}
7070+7171+impl TagRef for str {
7272+ fn get_tag_index(&self, noita: &mut Noita) -> io::Result<Option<u8>> {
7373+ noita.get_entity_tag_index(self)
7474+ }
7575+}
7676+7777+impl TagRef for u8 {
7878+ fn get_tag_index(&self, _: &mut Noita) -> io::Result<Option<u8>> {
7979+ Ok(Some(*self))
8080+ }
8181+}
8282+8383+impl TagRef for Option<u8> {
8484+ fn get_tag_index(&self, _: &mut Noita) -> io::Result<Option<u8>> {
8585+ Ok(*self)
8686+ }
8787+}
8888+8989+impl Noita {
9090+ pub fn new(proc: ProcessRef, g: NoitaGlobals) -> Self {
9191+ Self {
9292+ proc,
9393+ g,
9494+ entity_tag_cache: HashMap::new(),
9595+ no_player_not_polied: false,
9696+ materials: Vec::new(),
9797+ material_ui_names: Vec::new(),
9898+ }
9999+ }
100100+101101+ pub const fn proc(&self) -> &ProcessRef {
102102+ &self.proc
103103+ }
104104+105105+ pub fn read_seed(&self) -> io::Result<Option<Seed>> {
106106+ let world_seed = read_ptr!(self.world_seed).read(&self.proc)?;
107107+ if world_seed == 0 {
108108+ return Ok(None);
109109+ }
110110+ Ok(Some(Seed {
111111+ world_seed,
112112+ ng_count: read_ptr!(self.ng_count).read(&self.proc)?,
113113+ }))
114114+ }
115115+116116+ pub fn read_stats(&self) -> io::Result<GlobalStats> {
117117+ Ok(read_ptr!(self.global_stats))
118118+ }
119119+120120+ pub fn get_player(&mut self) -> io::Result<Option<(Entity, bool)>> {
121121+ let Some(player_unit_idx) = self.get_entity_tag_index("player_unit")? else {
122122+ // no player_unit means definitely no player
123123+ return Ok(None);
124124+ };
125125+126126+ if let Some(player) = self.get_first_tagged_entity(player_unit_idx)? {
127127+ self.no_player_not_polied = false;
128128+ return Ok(Some((player, false)));
129129+ }
130130+131131+ // avoid repeatedly trying to look up the polymorphed_player tag if it wasn't created yet
132132+ if self.no_player_not_polied {
133133+ return Ok(None);
134134+ }
135135+136136+ let Some(polymorphed_player_idx) = self.get_entity_tag_index("polymorphed_player")? else {
137137+ // no polymorphed_player means player was never polymorphed,
138138+ // and without a player it means there's no player lol
139139+ self.no_player_not_polied = true;
140140+ return Ok(None);
141141+ };
142142+ Ok(self
143143+ .get_first_tagged_entity(polymorphed_player_idx)?
144144+ .map(|p| (p, true)))
145145+ }
146146+147147+ pub fn get_first_tagged_entity(&mut self, tag: impl TagRef) -> io::Result<Option<Entity>> {
148148+ let entity_manager = read_ptr!(self.entity_manager).read(&self.proc)?;
149149+150150+ let Some(tag_idx) = tag.get_tag_index(self)? else {
151151+ return Ok(None);
152152+ };
153153+ let Some(bucket) = entity_manager.entity_buckets.get(tag_idx as u32) else {
154154+ return Ok(None);
155155+ };
156156+ let Some(entity) = bucket.read(&self.proc)?.get(0) else {
157157+ return Ok(None);
158158+ };
159159+ let entity = entity.read(&self.proc)?;
160160+ if entity.is_null() {
161161+ return Ok(None);
162162+ }
163163+ Ok(Some(entity.read(&self.proc)?))
164164+ }
165165+166166+ /// Can store the index and check entity bitset directly to avoid hashmap
167167+ /// lookups
168168+ pub fn get_entity_tag_index(&mut self, tag: &str) -> io::Result<Option<u8>> {
169169+ if let Some(idx) = self.entity_tag_cache.get(tag) {
170170+ return Ok(Some(*idx));
171171+ }
172172+173173+ let idx = read_ptr!(self.entity_tag_manager)
174174+ .read(&self.proc)?
175175+ .tag_indices
176176+ .get(&self.proc, tag)?;
177177+178178+ if let Some(index) = idx {
179179+ self.entity_tag_cache.insert(tag.to_string(), index);
180180+181181+ tracing::debug!("Found {tag} index: {index}");
182182+ } else {
183183+ // this can spam when the tag was never touched yet and thus doesn't exist
184184+ tracing::trace!("Did not find {tag} index");
185185+ }
186186+187187+ Ok(idx)
188188+ }
189189+190190+ pub fn has_tag(&mut self, entity: &Entity, tag: impl TagRef) -> io::Result<bool> {
191191+ Ok(entity.tags[tag.get_tag_index(self)?])
192192+ }
193193+194194+ pub fn materials(&mut self) -> io::Result<&[String]> {
195195+ if !self.materials.is_empty() {
196196+ return Ok(&self.materials);
197197+ }
198198+199199+ let material_ptrs = read_ptr!(self.game_global)
200200+ .read(&self.proc)?
201201+ .cell_factory
202202+ .read(&self.proc)?
203203+ .materials
204204+ .read(&self.proc)?;
205205+206206+ let mut materials = Vec::with_capacity(material_ptrs.len());
207207+ for ptr in material_ptrs {
208208+ materials.push(ptr.read(&self.proc)?);
209209+ }
210210+ self.materials = materials;
211211+ Ok(&self.materials)
212212+ }
213213+214214+ pub fn get_material_name(&mut self, index: u32) -> io::Result<Option<String>> {
215215+ Ok(self.materials()?.get(index as usize).cloned())
216216+ }
217217+218218+ pub fn get_material_ui_name(&mut self, index: u32) -> io::Result<Option<String>> {
219219+ if !self.material_ui_names.is_empty() {
220220+ return Ok(self.material_ui_names.get(index as usize).cloned());
221221+ }
222222+223223+ let cell_factory = read_ptr!(self.game_global)
224224+ .read(&self.proc)?
225225+ .cell_factory
226226+ .read(&self.proc)?;
227227+ let material_descs = cell_factory
228228+ .material_descs_maybe
229229+ .truncated(cell_factory.number_of_materials)
230230+ .read(&self.proc)?;
231231+232232+ let mut material_ui_names = Vec::with_capacity(material_descs.len());
233233+ for desc in material_descs {
234234+ material_ui_names.push(desc.ui_name.read(&self.proc)?);
235235+ }
236236+ self.material_ui_names = material_ui_names;
237237+ Ok(self.material_ui_names.get(index as usize).cloned())
238238+ }
239239+240240+ pub fn component_store<T: ComponentName>(&self) -> io::Result<ComponentStore<T>> {
241241+ let index = read_ptr!(self.component_type_manager)
242242+ .component_indices
243243+ .get(&self.proc, T::NAME)?
244244+ .ok_or_else(not_found!("Component type index not found"))?;
245245+246246+ let buffer = read_ptr!(self.entity_manager)
247247+ .read(&self.proc)?
248248+ .component_buffers
249249+ .get(index)
250250+ .ok_or_else(not_found!(
251251+ "Component buffer not found for index {index} ({})",
252252+ T::NAME
253253+ ))?
254254+ .read(&self.proc)?;
255255+256256+ Ok(ComponentStore {
257257+ proc: self.proc.clone(),
258258+ buffer,
259259+ _marker: PhantomData,
260260+ })
261261+ }
262262+}
263263+264264+#[derive(Display, Debug, Clone, Copy)]
265265+#[display("{world_seed}+{ng_count}")]
266266+pub struct Seed {
267267+ pub world_seed: u32,
268268+ pub ng_count: u32,
269269+}
270270+271271+impl Seed {
272272+ pub fn sum(&self) -> u32 {
273273+ self.world_seed.wrapping_add(self.ng_count)
274274+ }
275275+}
276276+277277+#[derive(Debug)]
278278+pub struct ComponentStore<T> {
279279+ proc: ProcessRef,
280280+ buffer: Ptr<ComponentBuffer>,
281281+ _marker: PhantomData<T>,
282282+}
283283+284284+impl<T> ComponentStore<T>
285285+where
286286+ T: ComponentName + Pod,
287287+{
288288+ pub fn get(&self, entity: &Entity) -> io::Result<Option<T>> {
289289+ let buffer = self.buffer.read(&self.proc)?;
290290+291291+ let idx = buffer
292292+ .indices
293293+ .get(entity.comp_idx)
294294+ .map(|i| i.read(&self.proc))
295295+ .transpose()?
296296+ .unwrap_or(buffer.default_index);
297297+298298+ let Some(ptr) = buffer.storage.get(idx.read(&self.proc)?) else {
299299+ return Ok(None);
300300+ };
301301+302302+ let ptr = ptr.read(&self.proc)?;
303303+ // not sure it could be null, but just in case
304304+ if ptr.is_null() {
305305+ return Ok(None);
306306+ }
307307+ Ok(Some(ptr.read(&self.proc)?))
308308+ }
309309+}
310310+311311+#[cfg(test)]
312312+#[test]
313313+fn test() -> anyhow::Result<()> {
314314+ use sysinfo::ProcessesToUpdate;
315315+ use tracing::level_filters::LevelFilter;
316316+ use tracing_subscriber::EnvFilter;
317317+318318+ tracing_subscriber::fmt()
319319+ .with_env_filter(
320320+ EnvFilter::builder()
321321+ .with_default_directive(LevelFilter::DEBUG.into())
322322+ .from_env()?,
323323+ )
324324+ .init();
325325+326326+ let mut system = sysinfo::System::new();
327327+ system.refresh_processes(ProcessesToUpdate::All);
328328+329329+ let Some(noita_pid) = system
330330+ .processes_by_exact_name("noita.exe".as_ref())
331331+ .find(|p| p.thread_kind().is_none())
332332+ else {
333333+ eprintln!("Noita process not found");
334334+ return Ok(());
335335+ };
336336+337337+ let proc = noita_pid.pid().as_u32().try_into()?;
338338+ let noita = Noita::new(proc, NoitaGlobals::debug());
339339+340340+ let stats = noita.read_stats()?;
341341+342342+ println!("{:#?}", stats);
343343+344344+ Ok(())
345345+}
+92
src/noita/rng.rs
···11+#[derive(Debug, Clone)]
22+pub struct NoitaRng(i64);
33+44+impl NoitaRng {
55+ /// The random function itself is super standard, the secret sauce was
66+ /// getting the state from the world seed and position
77+ pub fn random(&mut self) -> f64 {
88+ let hi = self.0 / 127773;
99+ let lo = self.0 - hi * 127773;
1010+ self.0 = lo * 16807 - hi * 2836;
1111+ if self.0 <= 0 {
1212+ self.0 += 0x7fffffff;
1313+ }
1414+ self.0 as f64 * 4.656612875e-10
1515+ }
1616+1717+ pub fn from_pos(seed_plus_ng: u32, x: f64, y: f64) -> Self {
1818+ let xo = x + ((seed_plus_ng ^ 0x93262e6f) & 0xfff) as f64;
1919+ let yo = y + (((seed_plus_ng ^ 0x93262e6f) >> 12) & 0xfff) as f64;
2020+2121+ let xi = to_int_kinda(xo * 134217727.0);
2222+ let yi = to_int_kinda(if yo.abs() >= 102400.0 || xo.abs() <= 1.0 {
2323+ yo * 134217727.0
2424+ } else {
2525+ yo * (yo * 3483.328 + xi as f64)
2626+ });
2727+2828+ let mixed = mix(xi as i32, yi as i32, seed_plus_ng);
2929+3030+ let mut state = (mixed as f64) / 4294967295.0 * 2147483639.0 + 1.0;
3131+ if state >= 2147483647.0 {
3232+ state *= 0.5;
3333+ }
3434+3535+ let mut rng = Self(state as i64);
3636+ rng.random();
3737+3838+ for _ in 0..(seed_plus_ng & 3) {
3939+ rng.random();
4040+ }
4141+ rng
4242+ }
4343+}
4444+4545+// wrapping_sub soup
4646+fn mix(a: i32, b: i32, c: u32) -> u32 {
4747+ let mut x = (a.wrapping_sub(b) as u32).wrapping_sub(c) ^ c >> 13;
4848+ let mut y = (b as u32).wrapping_sub(x).wrapping_sub(c) ^ x << 8;
4949+ let mut z = c.wrapping_sub(x).wrapping_sub(y) ^ y >> 13;
5050+ x = x.wrapping_sub(y).wrapping_sub(z) ^ z >> 12;
5151+ y = y.wrapping_sub(x).wrapping_sub(z) ^ x << 16;
5252+ z = z.wrapping_sub(x).wrapping_sub(y) ^ y >> 5;
5353+ x = x.wrapping_sub(y).wrapping_sub(z) ^ z >> 3;
5454+ y = y.wrapping_sub(x).wrapping_sub(z) ^ x << 10;
5555+ z.wrapping_sub(x).wrapping_sub(y) ^ y >> 15
5656+}
5757+5858+// pretty sure this was some bog standard double->int conversion function of stl or something
5959+fn to_int_kinda(input: f64) -> u64 {
6060+ // let is_normal_finite = ((bits >> 32) & 0x7fff_ffff) < 0x7ff0_0000;
6161+ let is_normal_finite = input.is_finite();
6262+ let valid_range = (-9.223372036854776e18..9.223372036854776e18).contains(&input);
6363+6464+ // could remove this check ig, I've never seen that warning
6565+ if !is_normal_finite || !valid_range {
6666+ tracing::warn!("invalid float received");
6767+ return (-0.0f64).to_bits();
6868+ }
6969+7070+ let abs_bits = input.to_bits() & !(1 << 63);
7171+ let in_abs = f64::from_bits(abs_bits);
7272+ if in_abs == 0.0 {
7373+ return 0;
7474+ }
7575+7676+ let exponent = abs_bits >> 52;
7777+ let norm_mantissa = (abs_bits & ((1 << 52) - 1)) | (1 << 52);
7878+7979+ let shift = 0x433 - exponent as i32;
8080+ let mut result = if shift > 0 {
8181+ norm_mantissa >> (shift & 63)
8282+ } else {
8383+ norm_mantissa << (-shift & 63)
8484+ };
8585+8686+ // restore the sign lol
8787+ if input != in_abs {
8888+ result = result.wrapping_neg();
8989+ }
9090+9191+ result & 0xffff_ffff
9292+}