From 57d89fe58ecb55131e1bf70de9f43d6a2d7f623e Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Mon, 31 Oct 2022 23:06:51 +0100 Subject: [PATCH] Initial Commit --- .cargo/config.toml | 3 + .gitignore | 1 + Cargo.lock | 186 +++++++ Cargo.toml | 24 + asr-dotnet/.gitignore | 2 + asr-dotnet/Cargo.toml | 15 + asr-dotnet/asr-dotnet-derive/Cargo.toml | 16 + asr-dotnet/asr-dotnet-derive/src/lib.rs | 161 ++++++ asr-dotnet/src/lib.rs | 701 ++++++++++++++++++++++++ src/lib.rs | 262 +++++++++ 10 files changed, 1371 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 asr-dotnet/.gitignore create mode 100644 asr-dotnet/Cargo.toml create mode 100644 asr-dotnet/asr-dotnet-derive/Cargo.toml create mode 100644 asr-dotnet/asr-dotnet-derive/src/lib.rs create mode 100644 asr-dotnet/src/lib.rs create mode 100644 src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..7e54b9a --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +target = "wasm32-unknown-unknown" +rustflags = ["-C", "target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext,+simd128"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0135259 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,186 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "asr" +version = "0.1.0" +source = "git+https://github.com/CryZe/asr#f8759a04ca885b1f689b2a607be1369e6615dd63" +dependencies = [ + "bytemuck", + "time", +] + +[[package]] +name = "asr-dotnet" +version = "0.1.0" +dependencies = [ + "asr", + "asr-dotnet-derive", + "bstr", + "bytemuck", +] + +[[package]] +name = "asr-dotnet-derive" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bstr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" +dependencies = [ + "memchr", +] + +[[package]] +name = "bytemuck" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aec14f5d4e6e3f927cd0c81f72e5710d95ee9019fbeb4b3021193867491bfd8" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9e1f5fa78f69496407a27ae9ed989e3c3b072310286f5ef385525e4cbc24a9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lunistice-auto-splitter" +version = "0.1.0" +dependencies = [ + "arrayvec", + "asr-dotnet", + "bytemuck", + "itoa", + "spinning_top", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "spinning_top" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75adad84ee84b521fb2cca2d4fd0f1dab1d8d026bda3c5bea4ca63b5f9f9293c" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +dependencies = [ + "libc", + "num_threads", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bd4431d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "lunistice-auto-splitter" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +asr-dotnet = { path = "asr-dotnet", features = ["il2cpp"] } +arrayvec = { version = "0.7.2", default-features = false } +spinning_top = "0.2.3" +itoa = { version = "1.0.1", default-features = false } +bytemuck = { version = "1.9.1", features = ["derive", "min_const_generics"] } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = true +panic = "abort" +strip = true + +[profile.release.build-override] +opt-level = 0 diff --git a/asr-dotnet/.gitignore b/asr-dotnet/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/asr-dotnet/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/asr-dotnet/Cargo.toml b/asr-dotnet/Cargo.toml new file mode 100644 index 0000000..3782f23 --- /dev/null +++ b/asr-dotnet/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "asr-dotnet" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +asr = { git = "https://github.com/CryZe/asr" } +bytemuck = { version = "1.9.1", features = ["derive", "min_const_generics"] } +bstr = { version = "1.0.1", default-features = false } +asr-dotnet-derive = { path = "asr-dotnet-derive" } + +[features] +il2cpp = ["asr-dotnet-derive/il2cpp"] diff --git a/asr-dotnet/asr-dotnet-derive/Cargo.toml b/asr-dotnet/asr-dotnet-derive/Cargo.toml new file mode 100644 index 0000000..310bcb7 --- /dev/null +++ b/asr-dotnet/asr-dotnet-derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "asr-dotnet-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = "1.0.95" +quote = "1.0.18" + +[lib] +proc-macro = true + +[features] +il2cpp = [] diff --git a/asr-dotnet/asr-dotnet-derive/src/lib.rs b/asr-dotnet/asr-dotnet-derive/src/lib.rs new file mode 100644 index 0000000..c9fc096 --- /dev/null +++ b/asr-dotnet/asr-dotnet-derive/src/lib.rs @@ -0,0 +1,161 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Data, DeriveInput, Ident, Lit, Meta}; + +#[proc_macro_derive(MonoClassBinding, attributes(namespace))] +pub fn mono_class_binding(input: TokenStream) -> TokenStream { + let ast: DeriveInput = syn::parse(input).unwrap(); + + let struct_data = match ast.data { + Data::Struct(s) => s, + _ => panic!("Only structs are supported"), + }; + + let struct_name = ast.ident; + let stuct_name_string = struct_name.to_string(); + + let name_space_string = ast + .attrs + .iter() + .find_map(|x| { + let nv = match x.parse_meta().ok()? { + Meta::NameValue(nv) => nv, + _ => return None, + }; + if nv.path.get_ident()? != "namespace" { + return None; + } + match nv.lit { + Lit::Str(s) => Some(s.value()), + _ => None, + } + }) + .unwrap_or_default(); + let binding_name = Ident::new(&format!("{struct_name}Binding"), struct_name.span()); + + let mut field_names = Vec::new(); + let mut field_name_strings = Vec::new(); + let mut field_types = Vec::new(); + for field in struct_data.fields { + field_names.push(field.ident.clone().unwrap()); + field_name_strings.push(field.ident.clone().unwrap().to_string()); + field_types.push(field.ty); + } + + #[cfg(not(feature = "il2cpp"))] + { + quote! { + struct #binding_name { + class: MonoClassDef, + #(#field_names: usize,)* + } + + impl #struct_name { + fn bind(image: &MonoImage, process: &Process) -> Result<#binding_name, ()> { + let class = image + .classes(process) + .find(|c| { + c.klass + .name + .read_str(process, |v| v == #stuct_name_string.as_bytes()) + && c.klass + .name_space + .read_str(process, |v| v == #name_space_string.as_bytes()) + }) + .ok_or(())?; + + #( + let #field_names = class.find_field(process, #field_name_strings).ok_or(())?; + )* + Ok(#binding_name { + class, + #(#field_names,)* + }) + } + } + + impl #binding_name { + fn class(&self) -> &MonoClassDef { + &self.class + } + + fn load(&self, process: &Process, instance: Ptr) -> Result<#struct_name, ()> { + self.class + .klass + .get_instance( + instance, + process, + |instance_data| { + Ok(#struct_name {#( + #field_names: *bytemuck::from_bytes( + instance_data + .get(self.#field_names..).ok_or(())? + .get(..core::mem::size_of::<#field_types>()).ok_or(())?, + ), + )*}) + }, + )? + } + } + } + .into() + } + + #[cfg(feature = "il2cpp")] + { + quote! { + struct #binding_name { + class: MonoClass, + #(#field_names: i32,)* + } + + impl #struct_name { + fn bind(image: &MonoImage, process: &Process) -> Result<#binding_name, ()> { + let class = image + .classes(process)? + .find(|c| { + c + .name + .read_str(process, |v| v == #stuct_name_string.as_bytes()) + && c + .name_space + .read_str(process, |v| v == #name_space_string.as_bytes()) + }) + .ok_or(())?; + + #( + let #field_names = class.find_field(process, #field_name_strings).ok_or(())?; + )* + Ok(#binding_name { + class, + #(#field_names,)* + }) + } + } + + impl #binding_name { + fn class(&self) -> &MonoClass { + &self.class + } + + fn load(&self, process: &Process, instance: Ptr) -> Result<#struct_name, ()> { + self.class + .get_instance( + instance, + process, + |instance_data| { + Ok(#struct_name {#( + #field_names: *bytemuck::from_bytes( + instance_data + .get(self.#field_names as usize..).ok_or(())? + .get(..core::mem::size_of::<#field_types>()).ok_or(())?, + ), + )*}) + }, + )? + } + } + } + .into() + } +} diff --git a/asr-dotnet/src/lib.rs b/asr-dotnet/src/lib.rs new file mode 100644 index 0000000..56622f5 --- /dev/null +++ b/asr-dotnet/src/lib.rs @@ -0,0 +1,701 @@ +#![no_std] + +use core::{ + fmt, iter, + marker::PhantomData, + mem::{self, MaybeUninit}, + slice, +}; + +use asr::{Address, Process}; + +pub use asr; +pub use asr_dotnet_derive::*; +use bytemuck::{Pod, Zeroable}; + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(transparent)] +pub struct CStr; + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct Ptr(u64, PhantomData); + +unsafe impl Pod for Ptr {} +unsafe impl Zeroable for Ptr {} + +impl Ptr { + fn addr(self) -> Address { + Address(self.0) + } + + pub fn is_null(&self) -> bool { + self.0 == 0 + } + + pub fn cast(self) -> Ptr { + Ptr(self.0, PhantomData) + } + + pub fn byte_offset(self, bytes: u64) -> Self { + Self(self.0 + bytes, PhantomData) + } + + pub fn offset(self, count: u64) -> Self { + Self(self.0 + count * mem::size_of::() as u64, PhantomData) + } +} + +impl Ptr { + pub fn read(self, process: &Process) -> Result { + process.read(self.addr()).map_err(drop) + } + + fn index(self, process: &Process, idx: usize) -> Result { + process + .read(self.addr() + (idx * mem::size_of::()) as u64) + .map_err(drop) + } +} + +impl Ptr { + #[inline(never)] + pub fn read_str(self, process: &Process, f: impl FnOnce(&[u8]) -> R) -> R { + let mut addr = self.addr(); + let mut buf = [MaybeUninit::::uninit(); 32 << 10]; + let mut cursor = &mut buf[..]; + let total_len = loop { + // We round up to the 4 KiB address boundary as that's a single + // page, which is safe to read either fully or not at all. We do + // this to do a single read rather than many small ones as the + // syscall overhead is a quite high. + let end = (addr.0 & !((4 << 10) - 1)) + (4 << 10); + // However we limit it to 256 bytes as 512 bytes is roughly the + // break even point in terms of syscall overhead and realistic + // string sizes are probably even smaller than that. + let len = (end - addr.0).min(256); + let (current_read_buf, after) = cursor.split_at_mut(len as usize); + cursor = after; + let current_read_buf = process + .read_into_uninit_buf(addr, current_read_buf) + .unwrap(); + if let Some(pos) = bstr::ByteSlice::find_byte(current_read_buf, 0) { + let cursor_len = cursor.len(); + let current_read_buf_len = current_read_buf.len(); + let buf_len = buf.len(); + break buf_len - cursor_len - current_read_buf_len + pos; + } else { + addr = end.into(); + } + }; + f(unsafe { slice::from_raw_parts(buf.as_ptr().cast(), total_len) }) + } +} + +impl Ptr> { + pub fn iter(mut self, process: &Process) -> impl Iterator> + '_ { + iter::from_fn(move || { + if !self.is_null() { + let list: GList = self.read(process).ok()?; + self = list.next; + Some(list.data) + } else { + None + } + }) + } +} + +impl fmt::Debug for Ptr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_null() { + f.write_str("NULL") + } else { + write!(f, "{:x}", self.0) + } + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct GHashTable { + hash_func: Ptr, + key_equal_func: Ptr, + + table: Ptr>>, + table_size: i32, + in_use: i32, + threshold: i32, + last_rehash: i32, + value_destroy_func: Ptr, + key_destroy_func: Ptr, +} + +unsafe impl Pod for GHashTable {} +unsafe impl Zeroable for GHashTable {} + +impl GHashTable { + #[cfg(not(feature = "il2cpp"))] + fn iter<'a>(&'a self, process: &'a Process) -> impl Iterator, Ptr)> + 'a { + (0..self.table_size as usize) + .flat_map(move |i| { + let mut slot_ptr = self.table.index(process, i).ok()?; + Some(core::iter::from_fn(move || { + if !slot_ptr.is_null() { + let slot: Slot = slot_ptr.read(process).unwrap(); + slot_ptr = slot.next; + Some((slot.key, slot.value)) + } else { + None + } + })) + }) + .flatten() + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct Slot { + key: Ptr, + value: Ptr, + next: Ptr>, +} + +unsafe impl Pod for Slot {} +unsafe impl Zeroable for Slot {} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct GList { + data: Ptr, + next: Ptr>, + prev: Ptr>, +} + +unsafe impl Pod for GList {} +unsafe impl Zeroable for GList {} + +#[cfg(not(feature = "il2cpp"))] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoAssembly { + pub ref_count: i32, + _padding: [u8; 4], + pub basedir: Ptr, + pub aname: MonoAssemblyName, + pub image: Ptr, +} + +#[cfg(feature = "il2cpp")] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoAssembly { + pub image: Ptr, + pub token: u32, + pub referenced_assembly_start: i32, + pub referenced_assembly_count: i32, + _padding: [u8; 4], + pub aname: MonoAssemblyName, +} + +#[cfg(not(feature = "il2cpp"))] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoAssemblyName { + pub name: Ptr, + pub culture: Ptr, + pub hash_value: Ptr, + pub public_key: Ptr, + pub public_key_token: [u8; 17], + _padding1: [u8; 3], + pub hash_alg: u32, + pub hash_len: u32, + pub flags: u32, + pub major: MonoAssemblyNameInt, + pub minor: MonoAssemblyNameInt, + pub build: MonoAssemblyNameInt, + pub revision: MonoAssemblyNameInt, + pub arch: MonoAssemblyNameInt, + pub without_version: MonoBoolean, + pub without_culture: MonoBoolean, + pub without_public_key_token: MonoBoolean, + _padding2: [u8; 3], +} + +#[cfg(feature = "il2cpp")] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoAssemblyName { + pub name: Ptr, + pub culture: Ptr, + + pub public_key: Ptr, + pub hash_alg: u32, + pub hash_len: i32, + pub flags: u32, + + pub major: i32, + pub minor: i32, + pub build: i32, + pub revision: i32, + + pub public_key_token: [u8; 8], + _padding: [u8; 4], +} + +#[cfg(not(feature = "il2cpp"))] +type MonoBoolean = u8; + +#[cfg(not(feature = "il2cpp"))] +// u16 if netcore is not enabled +type MonoAssemblyNameInt = u16; + +#[cfg(not(feature = "il2cpp"))] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoImage { + ref_count: i32, + _padding: [u8; 4], + raw_data_handle: Ptr, + raw_data: Ptr, + raw_data_len: u32, + various_flags: u32, + name: Ptr, + assembly_name: Ptr, + module_name: Ptr, + version: Ptr, + md_version_major: i16, + md_version_minor: i16, + _padding2: [u8; 4], + guid: Ptr, + image_info: Ptr, + mempool: Ptr, + raw_metadta: Ptr, + heap_strings: MonoStreamHeader, + heap_us: MonoStreamHeader, + heap_blob: MonoStreamHeader, + heap_guid: MonoStreamHeader, + heap_tables: MonoStreamHeader, + heap_pdb: MonoStreamHeader, + tables_base: Ptr, + referenced_tables: u64, + referenced_table_rows: Ptr, + tables: [MonoTableInfo; MONO_TABLE_NUM], + references: Ptr>, + nreferences: i32, + _padding3: [u8; 4], + modules: Ptr>, + module_count: u32, + _padding4: [u8; 4], + modules_loaded: Ptr, // to gboolean + files: Ptr>, + file_count: u32, + _padding5: [u8; 4], + aot_module: Ptr, + aotid: [u8; 16], + assembly: Ptr, + method_cache: Ptr, + class_cache: MonoInternalHashTable, +} + +#[cfg(feature = "il2cpp")] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoImage { + name: Ptr, + name_no_ext: Ptr, + assembly: Ptr, + + type_count: u32, + exported_type_count: u32, + custom_attribute_count: u32, + + _padding1: [u8; 4], + + metadata_handle: Ptr, + name_to_class_hash_table: Ptr, + code_gen_module: Ptr, + token: u32, + dynamic: u8, + + _padding2: [u8; 3], +} + +impl MonoImage { + #[cfg(not(feature = "il2cpp"))] + pub fn classes<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { + (0..self.class_cache.size as usize).flat_map(move |i| { + let mut class_ptr = self.class_cache.table.index(process, i).unwrap(); + iter::from_fn(move || { + if !class_ptr.is_null() { + let class = class_ptr.read(process).ok()?; + class_ptr = class.next_class_cache.cast(); + Some(class) + } else { + None + } + }) + }) + } + + #[cfg(feature = "il2cpp")] + pub fn classes<'a>( + &'a self, + process: &'a Process, + ) -> Result + 'a, ()> { + let module = process.get_module("GameAssembly.dll").map_err(drop)?; + let type_info_definition_table: Ptr> = + process.read(module + 0x25CB530u64).map_err(drop)?; + let ptr = type_info_definition_table + .offset(self.metadata_handle.read(process).unwrap_or_default() as _); + Ok((0..self.type_count as usize).filter_map(move |i| { + let class_ptr = ptr.index(process, i).ok()?; + if class_ptr.is_null() { + None + } else { + class_ptr.read(process).ok() + } + })) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct MonoInternalHashTable { + hash_func: Ptr, + key_extract: Ptr, + next_value: Ptr, + size: i32, + num_entries: i32, + table: Ptr>, +} + +unsafe impl Pod for MonoInternalHashTable {} +unsafe impl Zeroable for MonoInternalHashTable {} + +#[cfg(not(feature = "il2cpp"))] +const MONO_TABLE_NUM: usize = 56; + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoStreamHeader { + data: Ptr, + size: u32, + _padding: [u8; 4], +} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoTableInfo { + base: Ptr, // might be CStr + rows_and_size: [u8; 3], + row_size: u8, + size_bitfield: u32, +} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClassDef { + pub klass: MonoClass, + pub flags: u32, + pub first_method_idx: u32, + pub first_field_idx: u32, + pub method_count: u32, + pub field_count: u32, + _padding: [u8; 4], + pub next_class_cache: Ptr, +} + +impl MonoClassDef { + pub fn fields<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { + (0..self.field_count as usize).flat_map(|i| self.klass.fields.index(process, i)) + } + + pub fn find_field(&self, process: &Process, name: &str) -> Option { + Some( + self.fields(process) + .find(|field| field.name.read_str(process, |n| n == name.as_bytes()))? + .offset as usize, + ) + } + + #[cfg(not(feature = "il2cpp"))] + pub fn find_singleton(&self, process: &Process, instance_field_name: &str) -> Result { + let instance_field = self.find_field(process, instance_field_name).ok_or(())?; + + let instance = self + .klass + .get_static_field_memory(process)? + .byte_offset(instance_field as u64) + .cast::() + .read(process)?; + + if instance.is_null() { + return Err(()); + } + + Ok(instance) + } +} + +#[cfg(not(feature = "il2cpp"))] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClass { + pub element_class: Ptr, + pub cast_class: Ptr, + pub supertypes: Ptr>, + pub idepth: u16, + pub rank: u8, + _padding1: u8, + pub instance_size: i32, + pub flags1: u32, + pub min_align: u8, + _padding2: [u8; 3], + pub flags2: u32, + _padding3: [u8; 4], + pub parent: Ptr, + pub nested_in: Ptr, + pub image: Ptr, + pub name: Ptr, + pub name_space: Ptr, + pub type_token: u32, + pub vtable_size: i32, + pub interface_count: u16, + _padding4: [u8; 2], + pub interface_id: u32, + pub max_interface_id: u32, + pub interface_offsets_count: u16, + _padding5: [u8; 2], + pub interfaces_packed: Ptr>, + pub interface_offsets_packed: Ptr, + pub interface_bitmap: Ptr, + pub interfaces: Ptr>, + pub sizes: i32, + _padding6: [u8; 4], + pub fields: Ptr, + pub methods: Ptr, + pub this_arg: MonoType, + pub byval_arg: MonoType, + pub gc_descr: MonoGCDescriptor, + pub runtime_info: Ptr, + pub vtable: Ptr, + pub infrequent_data: MonoPropertyBag, + pub unity_user_data: Ptr, +} + +#[cfg(feature = "il2cpp")] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClass { + image: Ptr, + gc_desc: Ptr, + pub name: Ptr, + pub name_space: Ptr, + byval_arg: MonoType, + this_arg: MonoType, + + element_class: Ptr, + cast_class: Ptr, + declaring_type: Ptr, + parent: Ptr, + generic_class: Ptr, // , + type_metadata_handle: Ptr, + interop_data: Ptr, + klass: Ptr, + + fields: Ptr, + events: Ptr, // + properties: Ptr, // + methods: Ptr, // + nested_types: Ptr>, + implemented_interfaces: Ptr>, + interface_offsets: Ptr, + + static_fields: Ptr, + + rgctx_data: Ptr, + type_hierarchy: Ptr>, + unity_user_data: Ptr, + initialization_exception_gc_handle: u32, + cctor_started: u32, + cctor_finished: u32, + _padding1: [u8; 4], + cctor_thread: u64, + generic_container_handle: Ptr, + + instance_size: u32, + actual_size: u32, + element_size: u32, + native_size: i32, + static_fields_size: u32, + thread_static_fields_size: u32, + thread_static_fields_offset: i32, + + flags: u32, + token: u32, + + method_count: u16, + property_count: u16, + field_count: u16, + event_count: u16, + nested_type_count: u16, + vtable_count: u16, + interfaces_count: u16, + interface_offsets_count: u16, + + type_hierarchy_depth: u8, + generic_recursion_depth: u8, + rank: u8, + minimum_alignment: u8, + natural_aligment: u8, + packing_size: u8, + + more_flags: [u8; 2], + // initialized_and_no_error: u8:1, + // valuetype: u8:1, + // initialized: u8:1, + // enumtype: u8:1, + // is_generic: u8:1, + // has_references: u8:1, + // init_pending: u8:1, + // size_inited: u8:1, + + // has_finalize: u8:1, + // has_cctor: u8:1, + // is_blittable: u8:1, + // is_import_or_windows_runtime: u8:1, + // is_vtable_initialized: u8:1, + // has_initialization_error: u8:1, + _padding2: [u8; 4], +} + +impl MonoClass { + pub fn get_instance( + &self, + instance: Ptr, + process: &Process, + f: impl FnOnce(&[u8]) -> R, + ) -> Result { + let mut buf = [0; 4 << 10]; + let buf = buf.get_mut(..self.instance_size as usize).ok_or(())?; + process.read_into_buf(instance.addr(), buf).map_err(drop)?; + Ok(f(buf)) + } + + #[cfg(not(feature = "il2cpp"))] + pub fn get_static_field_memory(&self, process: &Process) -> Result { + self.runtime_info + .byte_offset(mem::size_of::() as u64) + .cast::>() + .read(process)? + .byte_offset(mem::size_of::() as u64) + .cast::>() + .index(process, self.vtable_size as usize) + } + + #[cfg(feature = "il2cpp")] + pub fn fields<'a>(&'a self, process: &'a Process) -> impl Iterator + 'a { + (0..self.field_count as usize).flat_map(|i| self.fields.index(process, i)) + } + + #[cfg(feature = "il2cpp")] + pub fn find_field(&self, process: &Process, name: &str) -> Option { + Some( + self.fields(process) + .find(|field| field.name.read_str(process, |n| n == name.as_bytes()))? + .offset, + ) + } + + #[cfg(feature = "il2cpp")] + pub fn find_singleton(&self, process: &Process, instance_field_name: &str) -> Result { + let instance_field = self.find_field(process, instance_field_name).ok_or(())?; + + let instance = self + .static_fields + .byte_offset(instance_field as u64) + .cast::() + .read(process)?; + + if instance.is_null() { + return Err(()); + } + + Ok(instance) + } +} + +type MonoGCDescriptor = Ptr; + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoType { + data: Ptr, + attrs: u16, + r#type: u8, + flags: u8, + _padding: [u8; 4], +} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoPropertyBag { + head: Ptr, +} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClassRuntimeInfo { + max_domain: u16, + _padding: [u8; 6], +} + +#[cfg(not(feature = "il2cpp"))] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClassField { + pub r#type: Ptr, + pub name: Ptr, + pub parent: Ptr, + pub offset: i32, + _padding: [u8; 4], +} + +#[cfg(feature = "il2cpp")] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoClassField { + pub name: Ptr, + pub r#type: Ptr, + pub parent: Ptr, + pub offset: i32, + pub token: u32, +} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MonoVTable { + klass: Ptr, + gc_descr: MonoGCDescriptor, + domain: Ptr, + r#type: Ptr, + interface_bitmap: Ptr, + max_interface_id: u32, + rank: u8, + initialized: u8, + flags: u8, + _padding1: u8, + imt_collisions_bitmap: u32, + _padding2: [u8; 4], + runtime_generic_context: Ptr, +} + +pub fn g_str_hash_with_artificial_nul_terminator(value: &[u8]) -> u32 { + let mut hash: u32 = 0; + value.iter().copied().chain([0]).skip(1).for_each(|c| { + hash = (hash << 5).wrapping_sub(hash.wrapping_add(c as u32)); + }); + hash +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8a53542 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,262 @@ +#![no_std] +#![allow(non_snake_case)] // TODO: Fix + +use arrayvec::ArrayString; +use asr_dotnet::{ + asr::{ + self, + time::Duration, + timer::{self, TimerState}, + watcher::Watcher, + Process, + }, + MonoAssembly, MonoClass, MonoClassBinding, MonoImage, Ptr, +}; +use bytemuck::{Pod, Zeroable}; +use spinning_top::{const_spinlock, Spinlock}; + +#[cfg(all(not(test), target_arch = "wasm32"))] +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} + +struct GameInfo { + timer_instance: Ptr, + game_manager_instance: Ptr, + timer_binding: TimerBinding, + game_manager_binding: GameManagerBinding, +} + +struct ProcessInfo { + process: Process, + game_info: Option, +} + +impl GameInfo { + fn load(process: &Process) -> Result { + let module = process.get_module("GameAssembly.dll").map_err(drop)?; + // _assembliesTrg signature scan + let mut arr: Ptr> = process.read(module + 0x25CACD8u64).map_err(drop)?; + + let image = loop { + let ptr = arr.read(process)?; + if ptr.is_null() { + return Err(()); + } + let mono_assembly = ptr.read(process)?; + if mono_assembly + .aname + .name + .read_str(process, |name| name == b"Assembly-CSharp") + { + break mono_assembly.image.read(process)?; + } + arr = arr.offset(1); + }; + + asr::print_message("Found Assembly-CSharp"); + + let timer_binding = Timer::bind(&image, process)?; + let timer_instance = timer_binding.class.find_singleton(process, "_instance")?; + + asr::print_message("Found Timer"); + + let game_manager_binding = GameManager::bind(&image, process)?; + let game_manager_instance = game_manager_binding + .class + .find_singleton(process, "k__BackingField")?; + + asr::print_message("Found GameManager"); + + Ok(Self { + timer_instance, + game_manager_instance, + timer_binding, + game_manager_binding, + }) + } +} + +impl ProcessInfo { + fn new(process: Process) -> Self { + Self { + process, + game_info: None, + } + } +} + +#[repr(C)] +#[derive(Default, Copy, Clone, Pod, Zeroable, Debug)] +struct Digits { + minutes: f32, + seconds: f32, + hundredths: f32, +} + +impl Digits { + fn format_into(&self, string: &mut ArrayString) { + let mut buffer = itoa::Buffer::new(); + let _ = string.try_push_str(buffer.format(self.minutes as u32)); + let _ = string.try_push(':'); + let seconds = buffer.format(self.seconds as u8); + if seconds.len() < 2 { + let _ = string.try_push('0'); + } + let _ = string.try_push_str(seconds); + let _ = string.try_push('.'); + let hundredths = buffer.format(self.hundredths as u8); + if hundredths.len() < 2 { + let _ = string.try_push('0'); + } + let _ = string.try_push_str(hundredths); + } +} + +impl GameManager { + fn stage(&self) -> i32 { + (self.currentLevel / 2) + 1 + } + + fn act(&self) -> i32 { + (self.currentLevel & 1) + 1 + } + + fn format_level_into(&self, string: &mut ArrayString) { + let mut buffer = itoa::Buffer::new(); + let _ = string.try_push_str(buffer.format(self.stage())); + let _ = string.try_push('-'); + let _ = string.try_push_str(buffer.format(self.act())); + } +} + +#[derive(Copy, Clone, Default, MonoClassBinding)] +struct GameManager { + gameState: i32, + _points: i32, + _deaths: i32, + currentLevel: i32, +} + +#[derive(Copy, Clone, Default, MonoClassBinding)] +struct Timer { + currentLevelTime: f32, + currentLevelTimeVector: Digits, + character: u32, +} + +#[allow(unused)] +mod game_state { + pub const MISSION: i32 = 0; + pub const TITLE_SCREEN: i32 = 1; + pub const MENU: i32 = 2; + pub const CUTSCENE: i32 = 3; + pub const DEATH: i32 = 4; + pub const RESPAWN: i32 = 5; + pub const RESULTS: i32 = 6; + pub const LOAD: i32 = 7; +} + +impl Timer { + fn character(&self) -> &'static str { + match self.character { + 0 => "Hana", + 1 => "Toree", + 2 => "Toukie", + _ => "Unknown", + } + } +} + +#[derive(Default)] +struct State { + process_info: Option, + timer: Watcher, + game_manager: Watcher, + run_time: f32, +} + +impl State { + fn update(&mut self) { + if self.process_info.is_none() { + self.process_info = Process::attach("Lunistice.exe").map(ProcessInfo::new); + } + if let Some(process_info) = &mut self.process_info { + if !process_info.process.is_open() { + self.process_info = None; + return; + } + + if process_info.game_info.is_none() { + process_info.game_info = GameInfo::load(&process_info.process).ok(); + } + + if let Some(game_info) = &process_info.game_info { + let game_manager = self.game_manager.update( + game_info + .game_manager_binding + .load(&process_info.process, game_info.game_manager_instance) + .ok(), + ); + + let timer = self.timer.update( + game_info + .timer_binding + .load(&process_info.process, game_info.timer_instance) + .ok(), + ); + + if let (Some(game_manager), Some(timer)) = (game_manager, timer) { + let mut buffer = itoa::Buffer::new(); + timer::set_variable("Points", buffer.format(game_manager._points)); + timer::set_variable("Resets", buffer.format(game_manager._deaths)); + + let mut string_buffer = ArrayString::<32>::new(); + timer.currentLevelTimeVector.format_into(&mut string_buffer); + timer::set_variable("Level Time", &string_buffer); + string_buffer.clear(); + game_manager.format_level_into(&mut string_buffer); + timer::set_variable("Level", &string_buffer); + timer::set_variable("Character", timer.character()); + + match timer::state() { + TimerState::NotRunning => { + if game_manager.old.currentLevel == 1 + && game_manager.current.currentLevel == 2 + { + self.run_time = 0.0; + timer::start(); + timer::pause_game_time(); + } + } + TimerState::Paused | TimerState::Running => { + if timer.current.currentLevelTime < timer.old.currentLevelTime { + self.run_time += timer.old.currentLevelTime; + } + timer::set_game_time(Duration::seconds_f32( + self.run_time + timer.currentLevelTime, + )); + if game_manager.check(|g| g.gameState == game_state::RESULTS) { + timer::split(); + } + } + TimerState::Ended => {} + } + } + } + } + } +} + +static STATE: Spinlock = const_spinlock(State { + process_info: None, + timer: Watcher::new(), + game_manager: Watcher::new(), + run_time: 0.0, +}); + +#[no_mangle] +pub extern "C" fn update() { + STATE.lock().update(); +}