diff --git a/.gitignore b/.gitignore index 3ab5292..385ef42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /Cargo.lock .idea +**/.DS_Store diff --git a/src/game_engine/unity/il2cpp/class.rs b/src/game_engine/unity/il2cpp/class.rs index 4e1ba9c..92e9541 100644 --- a/src/game_engine/unity/il2cpp/class.rs +++ b/src/game_engine/unity/il2cpp/class.rs @@ -13,7 +13,20 @@ pub struct Class { } impl Class { - pub(super) fn get_name( + pub fn get_from_component( + process: &Process, + module: &Module, + component: Address, + ) -> Result { + process + .read_pointer(component, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + .map(|class| Class { class }) + .ok_or(Error {}) + } + + pub fn get_name( &self, process: &Process, module: &Module, diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index baa3bfa..ab48c48 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -83,6 +83,9 @@ // https://github.com/Unity-Technologies/mono // https://github.com/CryZe/lunistice-auto-splitter/blob/b8c01031991783f7b41044099ee69edd54514dba/asr-dotnet/src/lib.rs +use crate::file_format::{elf, macho, pe}; +use crate::{Error, PointerSize, Process}; + pub mod il2cpp; pub mod mono; pub mod scene_manager; @@ -91,9 +94,15 @@ const CSTR: usize = 128; #[derive(Copy, Clone, PartialEq, Hash, Debug)] #[non_exhaustive] +/// The file format of a given executable / binary - different per operating system. +/// See https://en.wikipedia.org/wiki/Comparison_of_executable_file_formats enum BinaryFormat { + /// [Portable Executable](https://en.wikipedia.org/wiki/Portable_Executable), for Windows. PE, + /// [Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format), + /// for Linux. ELF, + /// [Mach-O](https://en.wikipedia.org/wiki/Mach-O) (Mach object), for Mac. MachO, } @@ -103,3 +112,22 @@ fn get_backing_name(name: &str) -> Option<&str> { let end = name[start + 1..].find('>')?; Some(&name[start + 1..start + 1 + end]) } + +impl Process { + /// Get the [BinaryFormat](BinaryFormat) and pointer size of the main module of the Process + // FIXME: BinaryFormat and this function should probably be widely available (rather + // than gated by Unity) + fn get_format_and_pointer_size(&self) -> Result<(BinaryFormat, PointerSize), Error> { + let main_module = self.get_main_module_range()?; + + if let Some(x) = pe::MachineType::read(self, main_module.0) { + Ok((BinaryFormat::PE, x.pointer_size().ok_or(Error {})?)) + } else if let Some(pointer_size) = elf::pointer_size(self, main_module.0) { + Ok((BinaryFormat::ELF, pointer_size)) + } else if let Some(pointer_size) = macho::pointer_size(self, main_module) { + Ok((BinaryFormat::MachO, pointer_size)) + } else { + Err(Error {}) + } + } +} diff --git a/src/game_engine/unity/mono/class.rs b/src/game_engine/unity/mono/class.rs index 2ffe93c..e63d991 100644 --- a/src/game_engine/unity/mono/class.rs +++ b/src/game_engine/unity/mono/class.rs @@ -7,13 +7,15 @@ use crate::{future::retry, string::ArrayCString, Address, Error, Process}; pub use asr_derive::MonoClass as Class; /// A .NET class that is part of an [`Image`](Image). -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Class { - pub(super) class: Address, + /// The address of the class + pub class: Address, } impl Class { - pub(super) fn get_name( + /// Get the name of the class + pub fn get_name( &self, process: &Process, module: &Module, @@ -215,3 +217,32 @@ impl Class { retry(|| self.get_parent(process, module)).await } } + +/** Represents a MonoObject, which is an instantiation of a MonoVTable (and therefore MonoClass). It + * is where the actual fields of the class live. + * + * See https://github.com/mono/mono/blob/0f53e9e151d92944cacab3e24ac359410c606df6/mono/metadata/object.h#L31. + */ +#[derive(Debug, Clone)] +pub struct Object { + /// The address of the Mono object. + pub address: Address, +} + +impl Object { + /** Get the Mono [class](Class) of this object. */ + pub fn get_class(&self, process: &Process, module: &Module) -> Result { + process + // MonoVTable *vtable + // See https://github.com/mono/mono/blob/0f53e9e151d92944cacab3e24ac359410c606df6/mono/metadata/object.h#L32 + .read_pointer(self.address, module.pointer_size) + .ok() + .filter(|val| !val.is_null()) + // MonoClass *klass + // See https://github.com/mono/mono/blob/0f53e9e151d92944cacab3e24ac359410c606df6/mono/metadata/class-internals.h#L360 + .and_then(|addr| process.read_pointer(addr, module.pointer_size).ok()) + .filter(|val| !val.is_null()) + .map(|class| Class { class }) + .ok_or(Error {}) + } +} diff --git a/src/game_engine/unity/mono/mod.rs b/src/game_engine/unity/mono/mod.rs index 56b44f4..3308e39 100644 --- a/src/game_engine/unity/mono/mod.rs +++ b/src/game_engine/unity/mono/mod.rs @@ -16,7 +16,7 @@ use assembly::Assembly; mod image; pub use image::Image; mod class; -pub use class::Class; +pub use class::{Class, Object}; mod field; use field::Field; mod version; @@ -193,12 +193,12 @@ impl Module { } /// Retrieve the [Mono version](Version) of the module. - pub fn get_version(&self) -> Version { + pub const fn get_version(&self) -> Version { self.version } /// Retrieve the [pointer size](PointerSize) of the process/module. - pub fn get_pointer_size(&self) -> PointerSize { + pub const fn get_pointer_size(&self) -> PointerSize { self.pointer_size } diff --git a/src/game_engine/unity/mono/offsets.rs b/src/game_engine/unity/mono/offsets.rs index dbfb5e5..2a77ae4 100644 --- a/src/game_engine/unity/mono/offsets.rs +++ b/src/game_engine/unity/mono/offsets.rs @@ -241,118 +241,126 @@ impl MonoOffsets { }, v_table: MonoVTableOffsets { vtable: 0x28 }, }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V3, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x60, - }, - image: ImageOffsets { class_cache: 0x4D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - image: 0x38, - name: 0x40, - namespace: 0x48, - vtable_size: 0x54, - fields: 0x90, - runtime_info: 0xC8, - field_count: 0xF8, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V2, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x60, - }, - image: ImageOffsets { class_cache: 0x4C0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - image: 0x38, - name: 0x40, - namespace: 0x48, - vtable_size: 0x54, - fields: 0x90, - runtime_info: 0xC8, - field_count: 0xF8, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x40 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1Cattrs, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x58, - }, - image: ImageOffsets { class_cache: 0x3D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - image: 0x40, - name: 0x48, - namespace: 0x50, - vtable_size: 0x18, - fields: 0xA8, - runtime_info: 0xF8, - field_count: 0x94, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x58, - }, - image: ImageOffsets { class_cache: 0x3D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - image: 0x38, - name: 0x40, - namespace: 0x48, - vtable_size: 0x18, - fields: 0xA0, - runtime_info: 0xF0, - field_count: 0x8C, - next_class_cache: 0xF8, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V3, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: 0x10, + image: 0x60, + }, + image: ImageOffsets { class_cache: 0x4D0 }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + image: 0x38, + name: 0x40, + namespace: 0x48, + vtable_size: 0x54, + fields: 0x90, + runtime_info: 0xC8, + field_count: 0xF8, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V2, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: 0x10, + image: 0x60, + }, + image: ImageOffsets { class_cache: 0x4C0 }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + image: 0x38, + name: 0x40, + namespace: 0x48, + vtable_size: 0x54, + fields: 0x90, + runtime_info: 0xC8, + field_count: 0xF8, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1Cattrs, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: 0x10, + image: 0x58, + }, + image: ImageOffsets { class_cache: 0x3D0 }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + image: 0x40, + name: 0x48, + namespace: 0x50, + vtable_size: 0x18, + fields: 0xA8, + runtime_info: 0xF8, + field_count: 0x94, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: 0x10, + image: 0x58, + }, + image: ImageOffsets { class_cache: 0x3D0 }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + image: 0x38, + name: 0x40, + namespace: 0x48, + vtable_size: 0x18, + fields: 0xA0, + runtime_info: 0xF0, + field_count: 0x8C, + next_class_cache: 0xF8, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } _ => None, } } diff --git a/src/game_engine/unity/scene_manager/component.rs b/src/game_engine/unity/scene_manager/component.rs new file mode 100644 index 0000000..359a25e --- /dev/null +++ b/src/game_engine/unity/scene_manager/component.rs @@ -0,0 +1,59 @@ +use crate::game_engine::unity::mono; +use crate::game_engine::unity::scene_manager::SceneManager; +use crate::{Address, Error, Process}; + +/// Representing a Unity Component, usually attached to a GameObject +#[derive(Debug)] +pub struct Component { + /// the base address of the component + pub address: Address, +} + +impl Component { + /** Retrieves the MonoObject this Component holds */ + pub fn get_mono_object( + &self, + process: &Process, + scene_manager: &SceneManager, + module: &mono::Module, + ) -> Result { + // FIXME: I'm using mono version as a proxy, this has not been well tested and may require changes + match module.get_version() { + mono::Version::V1 | mono::Version::V1Cattrs => { + // FIXME: Understand this better in the mono source code + // It's probably similar to the V3 logic + // e.g. not sure if "scripting object handle" is correct + process + .read_pointer( + self.address + scene_manager.offsets.scripting_object_handle, + scene_manager.pointer_size, + ) + .ok() + .filter(|val| !val.is_null()) + .map(|address| mono::Object { address }) + .ok_or(Error {}) + } + // FIXME: Untested on V2 + mono::Version::V2 | mono::Version::V3 => { + // NOTE: I'm like 80% sure the following is true + process + // class ScriptingGCHandle m_MonoReference + // See https://gist.github.com/just-ero/92457b51baf85bd1e5b8c87de8c9835e#file-object-hpp-L18 + // In Mono this is a MonoObjectHandle, which is a MonoObject** + // MonoObject **__raw + // See https://github.com/mono/mono/blob/0f53e9e151d92944cacab3e24ac359410c606df6/mono/metadata/handle-decl.h#L72 + .read_pointer( + self.address + scene_manager.offsets.scripting_object_handle, + scene_manager.pointer_size, + ) + .ok() + .filter(|val| !val.is_null()) + // MonoObject *__raw + .and_then(|addr| process.read_pointer(addr, scene_manager.pointer_size).ok()) + .filter(|val| !val.is_null()) + .map(|address| mono::Object { address }) + .ok_or(Error {}) + } + } + } +} diff --git a/src/game_engine/unity/scene_manager/game_object.rs b/src/game_engine/unity/scene_manager/game_object.rs new file mode 100644 index 0000000..4a4e491 --- /dev/null +++ b/src/game_engine/unity/scene_manager/game_object.rs @@ -0,0 +1,193 @@ +use super::{Component, SceneManager, CSTR}; +use crate::game_engine::unity::{il2cpp, mono}; +use crate::string::ArrayCString; +use crate::{Address, Address32, Address64, Error, PointerSize, Process}; +use core::array; +use core::mem::MaybeUninit; + +/// Representing a GameObject. From a GameObject, you can get the attached components (includes the +/// C# scripts). +#[derive(Clone, Debug)] +pub struct GameObject { + pub(super) address: Address, +} + +impl GameObject { + /// Get the name of the GameObject. + pub fn get_name( + &self, + process: &Process, + scene_manager: &SceneManager, + ) -> Result, Error> { + process.read_pointer_path( + self.address, + scene_manager.pointer_size, + &[scene_manager.offsets.game_object_name as u64, 0x0], + ) + } + + /// Traverse the components attached to this game object. + pub fn components<'a>( + &'a self, + process: &'a Process, + scene_manager: &'a SceneManager, + ) -> Result + 'a, Error> { + let (number_of_components, component_pair_array): (usize, Address) = + match scene_manager.pointer_size { + PointerSize::Bit64 => { + let array = process + .read::<[Address64; 3]>(self.address + scene_manager.offsets.game_object)?; + (array[2].value() as usize, array[0].into()) + } + _ => { + let array = process + .read::<[Address32; 3]>(self.address + scene_manager.offsets.game_object)?; + (array[2].value() as usize, array[0].into()) + } + }; + + if number_of_components == 0 { + return Err(Error {}); + } + + const ARRAY_SIZE: usize = 128; + + let components: [Address; ARRAY_SIZE] = match scene_manager.pointer_size { + PointerSize::Bit64 => { + let mut buf = [MaybeUninit::<[Address64; 2]>::uninit(); ARRAY_SIZE]; + let slice = process.read_into_uninit_slice( + component_pair_array, + &mut buf[..number_of_components], + )?; + + let mut iter = slice.iter_mut(); + array::from_fn(|_| { + iter.next() + .map(|&mut [_, second]| second.into()) + .unwrap_or_default() + }) + } + _ => { + let mut buf = [MaybeUninit::<[Address32; 2]>::uninit(); ARRAY_SIZE]; + let slice = process.read_into_uninit_slice( + component_pair_array, + &mut buf[..number_of_components], + )?; + + let mut iter = slice.iter_mut(); + array::from_fn(|_| { + iter.next() + .map(|&mut [_, second]| second.into()) + .unwrap_or_default() + }) + } + }; + + // 0 is always Transform + // FIXME: It would be good to be able to read those components here too + Ok((1..number_of_components).map(move |m| Component { + address: components[m], + })) + } + + /// Get a Component attached to the current `GameObject` by name of it's class. + /// + /// For Mono. + pub fn get_component_mono( + &self, + process: &Process, + scene_manager: &SceneManager, + module: &mono::Module, + name: &str, + ) -> Result { + if scene_manager.is_il2cpp { + return Err(Error {}); + } + + self.components(process, scene_manager)? + .find(|component| { + let val = component + .get_mono_object(process, scene_manager, module) + .and_then(|object| object.get_class(process, module)) + .and_then(|c| c.get_name::(process, module)); + + val.is_ok_and(|class_name| class_name.matches(name)) + }) + .ok_or(Error {}) + } + + /// Get the Mono object of a Component attached to the current `GameObject` + /// by name of it's class. + /// + /// For Mono. + pub fn get_component_mono_object( + &self, + process: &Process, + scene_manager: &SceneManager, + module: &mono::Module, + name: &str, + ) -> Result { + if scene_manager.is_il2cpp { + return Err(Error {}); + } + + self.components(process, scene_manager)? + .filter_map(|component| { + component + .get_mono_object(process, scene_manager, module) + .ok() + }) + .find(|object| { + let val = object + .get_class(process, module) + .and_then(|c| c.get_name::(process, module)); + + val.is_ok_and(|class_name| class_name.matches(name)) + }) + .ok_or(Error {}) + } + + /// Tries to find the base address of a class in the current `GameObject` by name. + /// + /// IL2CPP only. + pub fn get_class_il2cpp( + &self, + process: &Process, + scene_manager: &SceneManager, + module: &il2cpp::Module, + name: &str, + ) -> Result { + if !scene_manager.is_il2cpp { + return Err(Error {}); + } + + self.components(process, scene_manager)? + .find(|comp| { + let val = il2cpp::Class::get_from_component(process, module, comp.address) + .and_then(|c| c.get_name::(process, module)); + + val.is_ok_and(|class_name| class_name.matches(name)) + }) + .ok_or(Error {}) + } + + /// Returns whether the game object is considered "active" by the scene (if it or any of its + /// parents are inactive, then the game object is inactive) + pub fn is_active_in_hierarchy( + &self, + process: &Process, + scene_manager: &SceneManager, + ) -> Result { + process.read::(self.address + scene_manager.offsets.game_object_activeinhierarchy) + } + + /// Returns whether the game object is considered "active" by itself (irrespective of its + /// parents) + pub fn is_active_self( + &self, + process: &Process, + scene_manager: &SceneManager, + ) -> Result { + process.read::(self.address + scene_manager.offsets.game_object_activeself) + } +} diff --git a/src/game_engine/unity/scene_manager/game_objects.rs b/src/game_engine/unity/scene_manager/game_objects.rs deleted file mode 100644 index d5ed19b..0000000 --- a/src/game_engine/unity/scene_manager/game_objects.rs +++ /dev/null @@ -1,80 +0,0 @@ -use core::iter::{self, FusedIterator}; - -use super::{transform::Transform, Scene, SceneManager, CSTR}; -use crate::{Address, Address32, Address64, Error, PointerSize, Process}; - -impl SceneManager { - /// Iterates over all root [`Transform`]s declared for the - /// specified scene. - /// - /// Each Unity scene normally has a linked list of [`Transform`]s. - /// Each one can, recursively, have one or more children [`Transform`]s - /// (and so on), as well as a list of `Component`s, which are classes (eg. - /// `MonoBehaviour`) containing data we might want to retrieve for the auto - /// splitter logic. - fn root_game_objects<'a>( - &'a self, - process: &'a Process, - scene: &Scene, - ) -> impl FusedIterator + 'a { - let list_first = process - .read_pointer( - scene.address + self.offsets.root_storage_container, - self.pointer_size, - ) - .ok() - .filter(|val| !val.is_null()); - - let mut current_list = list_first; - - iter::from_fn(move || { - let [first, _, third]: [Address; 3] = match self.pointer_size { - PointerSize::Bit64 => process - .read::<[Address64; 3]>(current_list?) - .ok() - .filter(|[first, _, third]| !first.is_null() && !third.is_null())? - .map(|a| a.into()), - _ => process - .read::<[Address32; 3]>(current_list?) - .ok() - .filter(|[first, _, third]| !first.is_null() && !third.is_null())? - .map(|a| a.into()), - }; - - if first == list_first? { - current_list = None; - } else { - current_list = Some(first); - } - - Some(Transform { address: third }) - }) - .fuse() - } - - /// Tries to find the specified root [`Transform`] from the currently - /// active Unity scene. - pub fn get_root_game_object(&self, process: &Process, name: &str) -> Result { - self.root_game_objects(process, &self.get_current_scene(process)?) - .find(|obj| { - obj.get_name::(process, self) - .is_ok_and(|obj_name| obj_name.matches(name)) - }) - .ok_or(Error {}) - } - - /// Tries to find the specified root [`Transform`] from the - /// `DontDestroyOnLoad` Unity scene. - pub fn get_game_object_from_dont_destroy_on_load( - &self, - process: &Process, - name: &str, - ) -> Result { - self.root_game_objects(process, &self.get_dont_destroy_on_load_scene()) - .find(|obj| { - obj.get_name::(process, self) - .is_ok_and(|obj_name| obj_name.matches(name)) - }) - .ok_or(Error {}) - } -} diff --git a/src/game_engine/unity/scene_manager/mod.rs b/src/game_engine/unity/scene_manager/mod.rs index 126b991..cf2dcfb 100644 --- a/src/game_engine/unity/scene_manager/mod.rs +++ b/src/game_engine/unity/scene_manager/mod.rs @@ -14,20 +14,42 @@ use crate::{ Address, Address32, Error, PointerSize, Process, }; -mod game_objects; - mod offsets; +pub use offsets::Offsets; mod transform; pub use transform::Transform; -use offsets::Offsets; +mod game_object; +pub use game_object::GameObject; +mod component; +pub use component::Component; mod scene; + pub use scene::Scene; use super::{BinaryFormat, CSTR}; +fn try_attach_unity_player(process: &Process) -> Option<((Address, u64), BinaryFormat)> { + [ + ("UnityPlayer.dll", BinaryFormat::PE), + ("UnityPlayer.so", BinaryFormat::ELF), + ("UnityPlayer.dylib", BinaryFormat::MachO), + ] + .into_iter() + .find_map(|(name, format)| match format { + BinaryFormat::PE => { + let address = process.get_module_address(name).ok()?; + Some(( + (address, pe::read_size_of_image(process, address)? as u64), + format, + )) + } + _ => Some((process.get_module_range(name).ok()?, format)), + }) +} + /// The scene manager allows you to easily identify the current scene loaded in /// the attached Unity game. /// @@ -43,8 +65,23 @@ pub struct SceneManager { impl SceneManager { /// Attaches to the scene manager in the given process. pub fn attach(process: &Process) -> Option { - const SIG_64_BIT_PE: Signature<13> = + SceneManager::attach_with_offsets(process, None) + } + + /// Attaches to the scene manager in the given process with known offsets. + /// Use this function if the default offsets are wrong. + /// + /// FIXME: This function is a bandaid while we don't have support for all + /// scene manager versions in asr. We should remove this by supporting + /// those versions without override. + pub fn attach_with_offsets( + process: &Process, + offsets: Option<&'static Offsets>, + ) -> Option { + const SIG_64_BIT_PE_1: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ?? ?? ?? ?? 33 F6"); + const SIG_64_BIT_PE_2: Signature<13> = + Signature::new("48 83 EC 20 48 8B 2D ?? ?? ?? ?? 33 F6"); const SIG_64_BIT_ELF: Signature<13> = Signature::new("41 54 53 50 4C 8B ?5 ?? ?? ?? ?? 41 83"); const SIG_64_BIT_MACHO: Signature<13> = @@ -53,63 +90,105 @@ impl SceneManager { const SIG_32_2: Signature<6> = Signature::new("53 8D 41 ?? 33 DB"); const SIG_32_3: Signature<14> = Signature::new("55 8B EC 83 EC 18 A1 ?? ?? ?? ?? 33 C9 53"); - let (unity_player, format) = [ - ("UnityPlayer.dll", BinaryFormat::PE), - ("UnityPlayer.so", BinaryFormat::ELF), - ("UnityPlayer.dylib", BinaryFormat::MachO), - ] - .into_iter() - .find_map(|(name, format)| match format { - BinaryFormat::PE => { - let address = process.get_module_address(name).ok()?; - Some(( - (address, pe::read_size_of_image(process, address)? as u64), - format, - )) - } - _ => Some((process.get_module_range(name).ok()?, format)), - })?; - - let pointer_size = match format { - BinaryFormat::PE => pe::MachineType::read(process, unity_player.0)?.pointer_size()?, - BinaryFormat::ELF => elf::pointer_size(process, unity_player.0)?, - BinaryFormat::MachO => macho::pointer_size(process, unity_player)?, - }; + const SIG_32_MAIN_1: Signature<17> = + Signature::new("55 8B EC E8 ?? ?? ?? ?? 8B C8 E8 ?? ?? ?? ?? 85 C0"); + const SIG_64_BIT_MACHO_MAIN_1: Signature<18> = + Signature::new("48 89 FB E8 ?? ?? ?? ?? 48 89 C7 E8 ?? ?? ?? ?? 31 C9"); - let is_il2cpp = process.get_module_address("GameAssembly.dll").is_ok(); + let (pointer_size, base_address) = if let Some((unity_player, format)) = + try_attach_unity_player(process) + { + let pointer_size = match format { + BinaryFormat::PE => { + pe::MachineType::read(process, unity_player.0)?.pointer_size()? + } + BinaryFormat::ELF => elf::pointer_size(process, unity_player.0)?, + BinaryFormat::MachO => macho::pointer_size(process, unity_player)?, + }; - // There are multiple signatures that can be used, depending on the version of Unity - // used in the target game. - let base_address: Address = match (pointer_size, format) { - (PointerSize::Bit64, BinaryFormat::PE) => { - let addr = SIG_64_BIT_PE.scan_process_range(process, unity_player)? + 7; - addr + 0x4 + process.read::(addr).ok()? - } - (PointerSize::Bit64, BinaryFormat::ELF) => { - let addr = SIG_64_BIT_ELF.scan_process_range(process, unity_player)? + 7; - addr + 0x4 + process.read::(addr).ok()? - } - (PointerSize::Bit64, BinaryFormat::MachO) => { - let addr = SIG_64_BIT_MACHO.scan_process_range(process, unity_player)? + 7; - addr + 0x4 + process.read::(addr).ok()? - } - (PointerSize::Bit32, BinaryFormat::PE) => { - if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) { - process.read::(addr + 5).ok()?.into() - } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) { - process.read::(addr.add_signed(-4)).ok()?.into() - } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) { - process.read::(addr + 7).ok()?.into() - } else { + // There are multiple signatures that can be used, depending on the version of Unity + // used in the target game. + let base_address: Address = match (pointer_size, format) { + (PointerSize::Bit64, BinaryFormat::PE) => { + if let Some(addr) = SIG_64_BIT_PE_1.scan_process_range(process, unity_player) { + addr + 0x4 + process.read::(addr + 7).ok()? + 7 + } else if let Some(addr) = + SIG_64_BIT_PE_2.scan_process_range(process, unity_player) + { + addr + 0x4 + process.read::(addr + 7).ok()? + 7 + } else { + return None; + } + } + (PointerSize::Bit64, BinaryFormat::ELF) => { + let addr = SIG_64_BIT_ELF.scan_process_range(process, unity_player)? + 7; + addr + 0x4 + process.read::(addr).ok()? + } + (PointerSize::Bit64, BinaryFormat::MachO) => { + let addr = SIG_64_BIT_MACHO.scan_process_range(process, unity_player)? + 7; + addr + 0x4 + process.read::(addr).ok()? + } + (PointerSize::Bit32, BinaryFormat::PE) => { + if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) { + process.read::(addr + 5).ok()?.into() + } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) { + process.read::(addr.add_signed(-4)).ok()?.into() + } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) { + process.read::(addr + 7).ok()?.into() + } else { + return None; + } + } + _ => { return None; } - } - _ => { - return None; - } + }; + + (pointer_size, base_address) + } else { + // If it's not in UnityPlayer, then it's in the main module (old unity versions) + let main_module_range = process.get_main_module_range().ok()?; + let (format, pointer_size) = process.get_format_and_pointer_size().ok()?; + + // These signatures are for the GetActiveScene function, which calls GetSceneManager, + // which is the thing that actually references the scene manager address + + let base_address: Address = match (pointer_size, format) { + (PointerSize::Bit32, BinaryFormat::PE) => { + // pointer to GetSceneManager + let gsm_ptr = + SIG_32_MAIN_1.scan_process_range(process, main_module_range)? + 0x4; + + // pointer to g_RuntimeSceneManager + // mov [address] + let rsm_ptr = gsm_ptr + process.read::(gsm_ptr).ok()? + 0x5; + + Address::from(process.read::(rsm_ptr).ok()?) + } + (PointerSize::Bit64, BinaryFormat::MachO) => { + // pointing to the rip-relative address of GetSceneManager + let addr = SIG_64_BIT_MACHO_MAIN_1 + .scan_process_range(process, main_module_range)? + + 0x4; + // pointing to the beginning of GetSceneManager + let gsm_addr = addr + process.read::(addr).ok()? + 0x4; + + // Cuphead.GetSceneManager() - 55 - push rbp + // Cuphead.GetSceneManager()+1 - 48 89 E5 - mov rbp,rsp + // Cuphead.GetSceneManager()+4 - 48 8B 05 4D821101 - mov rax,[Cuphead.g_RuntimeSceneManager] + // pointing to the rip-relative address of g_RuntimeSceneManager + let addr = gsm_addr + 0x7; + addr + process.read::(addr).ok()? + 0x4 + } + // FIXME: support missing cases + _ => return None, + }; + + (pointer_size, base_address) }; - let offsets = Offsets::new(pointer_size)?; + let offsets = offsets.or_else(|| Offsets::new(pointer_size))?; + let is_il2cpp = process.get_module_address("GameAssembly.dll").is_ok(); // Dereferencing one level because this pointer never changes as long as the game is open. // It might not seem a lot, but it helps make things a bit faster when querying for scene stuff. diff --git a/src/game_engine/unity/scene_manager/offsets.rs b/src/game_engine/unity/scene_manager/offsets.rs index 25f485d..a6c4f37 100644 --- a/src/game_engine/unity/scene_manager/offsets.rs +++ b/src/game_engine/unity/scene_manager/offsets.rs @@ -1,20 +1,24 @@ use crate::PointerSize; -pub(super) struct Offsets { - pub(super) scene_count: u8, - pub(super) active_scene: u8, - pub(super) dont_destroy_on_load_scene: u8, - pub(super) asset_path: u8, - pub(super) build_index: u8, - pub(super) root_storage_container: u8, - pub(super) game_object: u8, - pub(super) game_object_name: u8, - pub(super) klass: u8, - pub(super) klass_name: u8, - pub(super) children_pointer: u8, +pub struct Offsets { + pub scene_count: u8, + pub active_scene: u8, + pub dont_destroy_on_load_scene: u8, + pub asset_path: u8, + pub build_index: u8, + pub root_storage_container: u8, + pub game_object: u8, + pub game_object_name: u8, + pub game_object_activeself: u8, + pub game_object_activeinhierarchy: u8, + /// a handle to the scripting object + /// MonoObjectHandle for Mono for e.g. + pub scripting_object_handle: u8, + pub children_pointer: u8, } impl Offsets { + // FIXME: This needs to support differentiation on unity version / binary format pub(super) const fn new(pointer_size: PointerSize) -> Option<&'static Self> { match pointer_size { PointerSize::Bit64 => Some(&Self { @@ -26,8 +30,12 @@ impl Offsets { root_storage_container: 0xB0, game_object: 0x30, game_object_name: 0x60, - klass: 0x28, - klass_name: 0x48, + // NOTE: I'm not sure what version of unity the above offsets were + // gotten with, so I can't get the appropriate offsets for the active + // fields + game_object_activeself: 0x0, + game_object_activeinhierarchy: 0x1, + scripting_object_handle: 0x18, children_pointer: 0x70, }), PointerSize::Bit32 => Some(&Self { @@ -39,8 +47,12 @@ impl Offsets { root_storage_container: 0x88, game_object: 0x1C, game_object_name: 0x3C, - klass: 0x18, - klass_name: 0x2C, + // NOTE: I'm not sure what version of unity the above offsets were + // gotten with, so I can't get the appropriate offsets for the active + // fields + game_object_activeself: 0x0, + game_object_activeinhierarchy: 0x1, + scripting_object_handle: 0x18, children_pointer: 0x50, }), _ => None, diff --git a/src/game_engine/unity/scene_manager/scene.rs b/src/game_engine/unity/scene_manager/scene.rs index caae805..2c8b673 100644 --- a/src/game_engine/unity/scene_manager/scene.rs +++ b/src/game_engine/unity/scene_manager/scene.rs @@ -1,9 +1,12 @@ -use super::{SceneManager, CSTR}; -use crate::{string::ArrayCString, Address, Error, Process}; +use super::{SceneManager, Transform, CSTR}; +use crate::{string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process}; +use core::iter; +use core::iter::FusedIterator; /// A scene loaded in the attached game. +#[derive(Debug)] pub struct Scene { - pub(super) address: Address, + pub address: Address, } impl Scene { @@ -66,4 +69,85 @@ impl Scene { let cs = path.rsplit_once('/').unwrap_or(("", &path)).1; Ok(cs.rsplit_once('.').unwrap_or((cs, "")).0.into()) } + + /// Iterates over all root [`Transform`]s declared for the + /// specified scene. + /// + /// Each Unity scene normally has a linked list of [`Transform`]s. + /// Each one can, recursively, have one or more children [`Transform`]s + /// (and so on), as well as a list of `Component`s, which are classes (eg. + /// `MonoBehaviour`) containing data we might want to retrieve for the auto + /// splitter logic. + pub fn root_game_objects<'a>( + &'a self, + process: &'a Process, + scene_manager: &'a SceneManager, + ) -> impl FusedIterator + 'a { + let list_first = process + .read_pointer( + self.address + scene_manager.offsets.root_storage_container, + scene_manager.pointer_size, + ) + .ok() + .filter(|val| !val.is_null()); + + let mut current_list = list_first; + + iter::from_fn(move || { + let [_prev, next, current]: [Address; 3] = match scene_manager.pointer_size { + PointerSize::Bit64 => process + .read::<[Address64; 3]>(current_list?) + .ok() + .filter(|[_prev, next, current]| !next.is_null() && !current.is_null())? + .map(|a| a.into()), + _ => process + .read::<[Address32; 3]>(current_list?) + .ok() + .filter(|[_prev, next, current]| !next.is_null() && !current.is_null())? + .map(|a| a.into()), + }; + + if next == list_first? { + return None; + } else { + current_list = Some(next); + } + + Some(Transform { address: current }) + }) + .fuse() + } + + /// Tries to find the specified root [`Transform`] from the currently + /// active Unity scene. + pub fn get_root_game_object( + &self, + process: &Process, + scene_manager: &SceneManager, + name: &str, + ) -> Result { + self.root_game_objects(process, scene_manager) + .find(|obj| { + obj.get_name::(process, scene_manager) + .is_ok_and(|obj_name| obj_name.matches(name)) + }) + .ok_or(Error {}) + } + + pub fn find_transform( + &self, + process: &Process, + scene_manager: &SceneManager, + root_object_name: &str, + child_path: &[&str], + ) -> Result { + let mut current_transform = + self.get_root_game_object(process, scene_manager, root_object_name)?; + + for object_name in child_path { + current_transform = current_transform.get_child(process, scene_manager, object_name)?; + } + + Ok(current_transform) + } } diff --git a/src/game_engine/unity/scene_manager/transform.rs b/src/game_engine/unity/scene_manager/transform.rs index 857a07e..4059d42 100644 --- a/src/game_engine/unity/scene_manager/transform.rs +++ b/src/game_engine/unity/scene_manager/transform.rs @@ -1,126 +1,44 @@ -use super::{SceneManager, CSTR}; +use super::{GameObject, SceneManager, CSTR}; use crate::{string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process}; use core::{array, mem::MaybeUninit}; /// A `Transform` is a base class for all entities used in a Unity scene. All /// classes of interest useful for an auto splitter can be found starting from /// the addresses of the root `Transform`s linked in each scene. +#[derive(Debug)] pub struct Transform { - pub(super) address: Address, + pub address: Address, } impl Transform { - /// Tries to return the name of the current `Transform`. + /// Get the name of the [`GameObject`] associated with this `Transform`. pub fn get_name( &self, process: &Process, scene_manager: &SceneManager, ) -> Result, Error> { - process.read_pointer_path( - self.address, - scene_manager.pointer_size, - &[ - scene_manager.offsets.game_object as u64, - scene_manager.offsets.game_object_name as u64, - 0x0, - ], - ) + self.get_game_object(process, scene_manager) + .and_then(|obj| obj.get_name(process, scene_manager)) } - /// Iterates over the classes referred to in the current `Transform`. - pub fn classes<'a>( - &'a self, - process: &'a Process, - scene_manager: &'a SceneManager, - ) -> Result + 'a, Error> { + /// Get the game object attached to this `Transform`, if any + pub fn get_game_object( + &self, + process: &Process, + scene_manager: &SceneManager, + ) -> Result { let game_object = process.read_pointer( self.address + scene_manager.offsets.game_object, scene_manager.pointer_size, )?; - let (number_of_components, main_object): (usize, Address) = match scene_manager.pointer_size - { - PointerSize::Bit64 => { - let array = process - .read::<[Address64; 3]>(game_object + scene_manager.offsets.game_object)?; - (array[2].value() as usize, array[0].into()) - } - _ => { - let array = process - .read::<[Address32; 3]>(game_object + scene_manager.offsets.game_object)?; - (array[2].value() as usize, array[0].into()) - } - }; - - if number_of_components == 0 { + if game_object.is_null() { return Err(Error {}); } - const ARRAY_SIZE: usize = 128; - - let components: [Address; ARRAY_SIZE] = match scene_manager.pointer_size { - PointerSize::Bit64 => { - let mut buf = [MaybeUninit::<[Address64; 2]>::uninit(); ARRAY_SIZE]; - let slice = process - .read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; - - let mut iter = slice.iter_mut(); - array::from_fn(|_| { - iter.next() - .map(|&mut [_, second]| second.into()) - .unwrap_or_default() - }) - } - _ => { - let mut buf = [MaybeUninit::<[Address32; 2]>::uninit(); ARRAY_SIZE]; - let slice = process - .read_into_uninit_slice(main_object, &mut buf[..number_of_components])?; - - let mut iter = slice.iter_mut(); - array::from_fn(|_| { - iter.next() - .map(|&mut [_, second]| second.into()) - .unwrap_or_default() - }) - } - }; - - Ok((1..number_of_components).filter_map(move |m| { - process - .read_pointer( - components[m] + scene_manager.offsets.klass, - scene_manager.pointer_size, - ) - .ok() - .filter(|val| !val.is_null()) - })) - } - - /// Tries to find the base address of a class in the current `GameObject`. - pub fn get_class( - &self, - process: &Process, - scene_manager: &SceneManager, - name: &str, - ) -> Result { - self.classes(process, scene_manager)? - .find(|&addr| { - let val: Result, Error> = match scene_manager.is_il2cpp { - true => process.read_pointer_path( - addr, - scene_manager.pointer_size, - &[0x0, scene_manager.size_of_ptr().wrapping_mul(2), 0x0], - ), - false => process.read_pointer_path( - addr, - scene_manager.pointer_size, - &[0x0, 0x0, scene_manager.offsets.klass_name as u64, 0x0], - ), - }; - - val.is_ok_and(|class_name| class_name.matches(name)) - }) - .ok_or(Error {}) + Ok(GameObject { + address: game_object, + }) } /// Iterates over children `Transform`s referred by the current one