diff --git a/src/game_engine/unity/il2cpp/mod.rs b/src/game_engine/unity/il2cpp/mod.rs index 8d93756e..fef1f9bf 100644 --- a/src/game_engine/unity/il2cpp/mod.rs +++ b/src/game_engine/unity/il2cpp/mod.rs @@ -86,6 +86,25 @@ impl Module { Self::attach_with(process, il2cpp_module, format, profile) } + /// Tries attaching to a Unity game that is using the IL2CPP backend, in + /// the module at `il2cpp_module`, with the provided measured [`Profile`]. + /// Use this when the runtime sits in a module [`attach`](Self::attach) + /// doesn't look for, such as a renamed `GameAssembly.dll`. + /// [`Process::get_module_address`] finds a module by its name without + /// allocating. The module must be a PE or an ELF image whose pointer width + /// matches the profile. + pub fn attach_to_module( + process: &Process, + il2cpp_module: Address, + profile: Profile, + ) -> Option { + let (il2cpp_module, format) = super::module_at(process, il2cpp_module)?; + let pointer_size = Self::pointer_size(process, il2cpp_module, format)?; + (pointer_size == profile.pointer_size).then_some(())?; + + Self::attach_with(process, il2cpp_module, format, profile) + } + /// Finds the module the runtime is compiled into, which is /// `GameAssembly.dll` on Windows and `GameAssembly.so` on Linux. fn find_runtime_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> { @@ -480,6 +499,38 @@ impl Module { retry(|| Self::attach(process, profile)).await } + /// Attaches to a Unity game that is using the IL2CPP backend, in the + /// module at `il2cpp_module`, with the provided measured [`Profile`]. + /// + /// This is the `await`able version of + /// [`attach_to_module`](Self::attach_to_module), yielding back to the + /// runtime between each try. + pub async fn wait_attach_to_module( + process: &Process, + il2cpp_module: Address, + profile: Profile, + ) -> Module { + retry(|| Self::attach_to_module(process, il2cpp_module, profile)).await + } + + /// Creates a module from the addresses of the `s_Assemblies` vector and + /// the `s_TypeInfoDefinitionTable` pointer, with the provided measured + /// [`Profile`]. Use this when the auto splitter finds the two globals + /// itself because [`attach`](Self::attach) doesn't find them in a game. + /// Nothing checks the addresses or the profile against the game. + pub const fn new( + assemblies: Address, + type_info_definition_table: Address, + profile: Profile, + ) -> Self { + Self { + assemblies, + type_info_definition_table, + profile, + pointer_size: profile.pointer_size, + } + } + /// Looks for the specified binary [image](Image) inside the target process. /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game diff --git a/src/game_engine/unity/il2cpp/walk_tests.rs b/src/game_engine/unity/il2cpp/walk_tests.rs index 5069b244..4b4d0ed5 100644 --- a/src/game_engine/unity/il2cpp/walk_tests.rs +++ b/src/game_engine/unity/il2cpp/walk_tests.rs @@ -220,6 +220,30 @@ fn attach_auto_detect_uses_a_measured_build() { assert_eq!(module.profile.class.static_fields, 0xA0); } +// A runtime under a file name `attach` doesn't look for still attaches +// through the module the auto splitter hands in. An address that holds no +// module doesn't attach. +#[test] +fn a_given_module_attaches_under_any_name() { + let image = Player::game_assembly(); + with_modules( + &[(GAME_ASSEMBLY, &image)], + &[("GameAssembly_x64.dll", GAME_ASSEMBLY, 0x1000)], + |process| { + let profile = super::profiles::UNITY_6000_5_0F1_X86_64; + assert!(Module::attach(process, profile).is_none()); + let module = process.get_module_address("GameAssembly_x64.dll").unwrap(); + let attached = Module::attach_to_module(process, module, profile).unwrap(); + assert_eq!(attached.profile, profile); + assert!(Module::attach_to_module(process, module + 0x800, profile).is_none()); + assert!(matches!( + poll_once(Module::wait_attach_to_module(process, module, profile)), + Poll::Ready(_) + )); + }, + ); +} + #[test] fn attach_uses_the_explicit_profile_and_checks_its_width() { let player = Player { @@ -668,6 +692,49 @@ fn type_counts_no_image_has_read_as_no_classes() { }); } +// An auto splitter that found the two globals itself hands them in and gets +// the module `attach` would build from them. The pointer width comes from the +// profile. +#[test] +fn given_globals_read_like_found_ones() { + let found = Player { + unity: MEASURED_6000_5, + } + .attach() + .unwrap(); + let given = Module::new( + found.assemblies, + found.type_info_definition_table, + found.profile, + ); + assert_eq!(given.assemblies, found.assemblies); + assert_eq!( + given.type_info_definition_table, + found.type_info_definition_table + ); + assert_eq!(given.profile, found.profile); + assert_eq!(given.pointer_size, found.pointer_size); + + for unity in [MEASURED_2019, MEASURED_2022] { + with_process(&[(BASE, &image(unity))], |process| { + let module = Module::new( + Address::new(BASE), + Address::new(BASE + 0x10), + measured(unity, PointerSize::Bit64), + ); + let image = module.get_default_image(process).unwrap(); + assert!(image.get_class(process, &module, "GameManager").is_some()); + assert_eq!(image.classes(process, &module).count(), 5); + }); + } + let x86 = Module::new( + Address::new(BASE), + Address::new(BASE + 0x10), + measured(MEASURED_2019, PointerSize::Bit32), + ); + assert_eq!(x86.get_pointer_size(), PointerSize::Bit32); +} + #[test] fn classes_resolve_by_name_and_namespace() { for unity in [MEASURED_2019, MEASURED_2022] { diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index d0cb62b7..849c8919 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -89,7 +89,14 @@ pub use managed::{DictionaryOffsets, HashSetOffsets, ListOffsets, ManagedString} pub mod mono; pub mod scene_manager; -use crate::{signature::Signature, Address, Process}; +#[cfg(all(test, not(target_family = "wasm")))] +mod module_tests; + +use crate::{ + file_format::{elf, pe}, + signature::Signature, + Address, Process, +}; const CSTR: usize = 128; @@ -102,6 +109,19 @@ enum BinaryFormat { MachO, } +/// Reads the format and the size of the module at `address` from its +/// header. A PE image spans the size of image its optional header gives and +/// an ELF image runs to the end of its last load segment. asr has no reader +/// for a Mach-O image's size, so a Mach-O image gives `None` like any header +/// it doesn't know. +fn module_at(process: &Process, address: Address) -> Option<((Address, u64), BinaryFormat)> { + if let Some(size) = pe::read_size_of_image(process, address) { + return Some(((address, size as u64), BinaryFormat::PE)); + } + let size = elf::read_size_of_image(process, address)?; + Some(((address, size), BinaryFormat::ELF)) +} + /// Finds the Unity version string in a player, `2021.3.11f1` for /// example, and returns its three numbers. The fourth part of a file /// version has no equal in the string, so it is 0. A NUL heads the diff --git a/src/game_engine/unity/module_tests.rs b/src/game_engine/unity/module_tests.rs new file mode 100644 index 00000000..f1742f6d --- /dev/null +++ b/src/game_engine/unity/module_tests.rs @@ -0,0 +1,61 @@ +//! Tests reading a module's format and size from its header, for auto +//! splitters that hand in the module themselves. + +use super::{module_at, BinaryFormat}; +use crate::{runtime::mock::with_process, Address}; + +use std::vec; + +const BASE: u64 = 0x7FF6_1000_0000; + +fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); +} + +#[test] +fn pe_module_spans_its_size_of_image() { + // The DOS header points at the COFF header, which is followed by a PE32+ + // optional header that says the image is 0x5000 bytes. + let mut image = vec![0; 0x400]; + put(&mut image, 0, b"MZ"); + put(&mut image, 0x3C, &0x80_u32.to_le_bytes()); + put(&mut image, 0x80, b"PE\0\0"); + put(&mut image, 0x84, &0x8664_u16.to_le_bytes()); + put(&mut image, 0x94, &0xF0_u16.to_le_bytes()); + put(&mut image, 0x98, &0x20B_u16.to_le_bytes()); + put(&mut image, 0x98 + 0x38, &0x5000_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert_eq!( + module_at(process, Address::new(BASE)), + Some(((Address::new(BASE), 0x5000), BinaryFormat::PE)) + ); + }); +} + +#[test] +fn elf_module_spans_to_its_last_load_segment() { + // An ELF64 header with one program header, a load segment that runs to + // 0x3000 in memory. + let mut image = vec![0; 0x400]; + put(&mut image, 0, b"\x7fELF\x02\x01\x01"); + put(&mut image, 0x10, &3_u16.to_le_bytes()); + put(&mut image, 0x20, &0x40_u64.to_le_bytes()); + put(&mut image, 0x36, &56_u16.to_le_bytes()); + put(&mut image, 0x38, &1_u16.to_le_bytes()); + put(&mut image, 0x40, &1_u32.to_le_bytes()); + put(&mut image, 0x68, &0x3000_u64.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert_eq!( + module_at(process, Address::new(BASE)), + Some(((Address::new(BASE), 0x3000), BinaryFormat::ELF)) + ); + }); +} + +#[test] +fn unknown_header_gives_no_module() { + let image = vec![0; 0x400]; + with_process(&[(BASE, &image)], |process| { + assert_eq!(module_at(process, Address::new(BASE)), None); + }); +} diff --git a/src/game_engine/unity/mono/attachment_tests.rs b/src/game_engine/unity/mono/attachment_tests.rs index f6378ae5..b8337c00 100644 --- a/src/game_engine/unity/mono/attachment_tests.rs +++ b/src/game_engine/unity/mono/attachment_tests.rs @@ -63,6 +63,25 @@ fn runtime() -> Vec { image } +// An auto splitter that found the global itself hands it in and gets the +// module `attach` would build from it. +#[test] +fn a_given_global_builds_the_attached_module() { + let runtime = runtime(); + with_modules( + &[(RUNTIME, &runtime)], + &[("libmono.so", RUNTIME, runtime.len() as u64)], + |process| { + let profile = profiles::UNITY_5_6_0F1_LINUX_MONO_X86_64; + let found = Module::attach(process, profile).unwrap(); + let given = Module::new(found.assemblies, profile); + assert_eq!(given.assemblies, found.assemblies); + assert_eq!(given.profile, found.profile); + assert_eq!(given.pointer_size, found.pointer_size); + }, + ); +} + #[test] fn an_explicit_profile_needs_neither_a_player_nor_alloc() { let runtime = runtime(); @@ -111,3 +130,29 @@ fn an_unknown_legacy_runtime_uses_the_executables_version() { }, ); } + +// A runtime under a file name `attach` doesn't look for still attaches +// through the module the auto splitter hands in, which doesn't need `alloc`. +// An address that holds no module doesn't attach. +#[test] +fn a_given_module_attaches_under_any_name() { + let runtime = runtime(); + with_modules( + &[(RUNTIME, &runtime)], + &[("libmono-game.so", RUNTIME, runtime.len() as u64)], + |process| { + let profile = profiles::UNITY_5_6_0F1_LINUX_MONO_X86_64; + assert!(Module::attach(process, profile).is_none()); + let module = process.get_module_address("libmono-game.so").unwrap(); + let attached = Module::attach_to_module(process, module, profile).unwrap(); + assert_eq!(attached.profile, profile); + assert!(Module::attach_to_module(process, module + 0x1000, profile).is_none()); + let Poll::Ready(attached) = + poll_once(Module::wait_attach_to_module(process, module, profile)) + else { + panic!("the given module should attach immediately"); + }; + assert_eq!(attached.profile, profile); + }, + ); +} diff --git a/src/game_engine/unity/mono/mod.rs b/src/game_engine/unity/mono/mod.rs index 4b6f941a..177fa90c 100644 --- a/src/game_engine/unity/mono/mod.rs +++ b/src/game_engine/unity/mono/mod.rs @@ -5,7 +5,9 @@ //! a separate player module can be identified through its executable. A //! `no_std` auto splitter using allocation must provide a global allocator. Use //! [`Module::attach`] or [`Module::wait_attach`] with a known [`Profile`] to -//! attach without allocation on Windows and Linux. macOS support requires +//! attach without allocation on Windows and Linux. +//! [`Module::attach_to_module`] does the same for a runtime the auto splitter +//! finds itself. macOS support requires //! `alloc` for Mach-O symbol lookup even with an explicit profile. #[cfg(feature = "alloc")] @@ -228,6 +230,28 @@ impl Module { Self::attach_with(process, module_range, format, profile) } + /// Tries attaching to a Unity game that is using the standard Mono + /// backend, in the module at `mono_module`, with the provided measured + /// [`Profile`]. Use this when the runtime sits in a module + /// [`attach`](Self::attach) doesn't look for, such as a renamed runtime + /// library. [`Process::get_module_address`] finds a module by its name + /// without allocating. The module must be a PE or an ELF image whose + /// pointer width matches the profile and the profile's library must be + /// the runtime the module holds. + pub fn attach_to_module( + process: &Process, + mono_module: Address, + profile: Profile, + ) -> Option { + let (module_range, format) = super::module_at(process, mono_module)?; + let pointer_size = Self::pointer_size(process, module_range, format)?; + if pointer_size != profile.pointer_size { + return None; + } + + Self::attach_with(process, module_range, format, profile) + } + fn find_runtime_module( process: &Process, ) -> Option<((Address, u64), BinaryFormat, &'static str, Library)> { @@ -739,6 +763,33 @@ impl Module { retry(|| Self::attach(process, profile)).await } + /// Attaches to a Unity game that is using the standard Mono backend, in + /// the module at `mono_module`, with the provided measured [`Profile`]. + /// + /// This is the `await`able version of + /// [`attach_to_module`](Self::attach_to_module), yielding back to the + /// runtime between each try. + pub async fn wait_attach_to_module( + process: &Process, + mono_module: Address, + profile: Profile, + ) -> Self { + retry(|| Self::attach_to_module(process, mono_module, profile)).await + } + + /// Creates a module from the address of the `loaded_assemblies` global, + /// the one that holds the pointer to the list, with the provided measured + /// [`Profile`]. Use this when the auto splitter finds the global itself + /// because [`attach`](Self::attach) doesn't find it in a game. Nothing + /// checks the address or the profile against the game. + pub const fn new(assemblies: Address, profile: Profile) -> Self { + Self { + assemblies, + profile, + pointer_size: profile.pointer_size, + } + } + /// Looks for the specified binary [image](Image) inside the target process. /// An [image](Image) is a .NET DLL that is loaded /// by the game. The `Assembly-CSharp` [image](Image) is the main game diff --git a/src/game_engine/unity/mono/walk_tests.rs b/src/game_engine/unity/mono/walk_tests.rs index 5c6c3178..3e0e85ef 100644 --- a/src/game_engine/unity/mono/walk_tests.rs +++ b/src/game_engine/unity/mono/walk_tests.rs @@ -327,6 +327,23 @@ fn images_resolve_by_name_through_both_routes() { }); } +// A module built from the `loaded_assemblies` global reads the same as one +// `attach` built from it. The pointer width comes from the profile. +#[test] +fn given_assemblies_read_like_found_ones() { + with_process(&[(BASE, &image())], |process| { + let module = Module::new(Address::new(BASE), measured()); + let image = module.get_default_image(process).unwrap(); + assert!(image.get_class(process, &module, "GameManager").is_some()); + assert_eq!(image.classes(process, &module).count(), 6); + }); + let x86 = Module::new( + Address::new(BASE), + profiles::UNITY_2017_2_0F1_WINDOWS_MONO_BDWGC_X86, + ); + assert_eq!(x86.get_pointer_size(), PointerSize::Bit32); +} + #[test] fn classes_resolve_by_name_and_namespace() { on_fixture(measured(), |process, module| { diff --git a/src/game_engine/unity/scene_manager/attach_tests.rs b/src/game_engine/unity/scene_manager/attach_tests.rs index 951b7b9d..e4f1e91b 100644 --- a/src/game_engine/unity/scene_manager/attach_tests.rs +++ b/src/game_engine/unity/scene_manager/attach_tests.rs @@ -2,7 +2,7 @@ //! the anchor of the build is scanned in the engine module, every match //! must point at the same global, and the global holds the manager. -use super::{builds, linux_builds, Scene, SceneManager}; +use super::{builds, linux_builds, profiles, Profile, Scene, SceneManager}; use crate::{runtime::mock::with_modules, Address, PointerSize, Process}; use std::format; use std::vec; @@ -43,7 +43,7 @@ fn anchor_x86(image: &mut [u8], at: u64, global: u64) { } fn attach(image: &[u8], pointer_size: PointerSize) -> Option { - let profile = &builds::nearest((6000, 3, 21, 9777), pointer_size) + let profile = builds::nearest((6000, 3, 21, 9777), pointer_size) .unwrap() .profile; let base = match pointer_size { @@ -222,7 +222,7 @@ fn scenes_come_from_the_loaded_scene_array() { &[(BASE, &image)], &[("UnityPlayer.dll", BASE, MODULE)], |process: &Process| { - let profile = &builds::nearest((6000, 3, 21, 9777), PointerSize::Bit64) + let profile = builds::nearest((6000, 3, 21, 9777), PointerSize::Bit64) .unwrap() .profile; let manager = @@ -258,7 +258,7 @@ fn anchor_old_x86(image: &mut [u8], at: u64, global: u64, scenes: u8) { #[test] fn the_old_x86_anchor_leaves_the_scene_list_offset_open() { - let profile = &builds::nearest((5, 6, 7, 3267), PointerSize::Bit32) + let profile = builds::nearest((5, 6, 7, 3267), PointerSize::Bit32) .unwrap() .profile; let mut image = vec![0; 0x2000]; @@ -277,7 +277,6 @@ fn the_old_x86_anchor_leaves_the_scene_list_offset_open() { // The least of a PE header: the DOS header pointing at the COFF header, an // x64 machine, and an optional header carrying the size of the image. -#[cfg(feature = "alloc")] fn pe_header(image: &mut [u8]) { put(image, 0, b"MZ"); put(image, 0x3C, &0x80_u32.to_le_bytes()); @@ -288,6 +287,43 @@ fn pe_header(image: &mut [u8]) { put(image, 0x98 + 0x38, &(MODULE as u32).to_le_bytes()); } +// A given profile attaches through the player module without reading the +// version, so it needs no `alloc`. A profile for the other pointer size does +// not attach. Read as an x86 absolute address, the displacement of the x64 +// load points at a pointer in low memory, so only the pointer size check +// stops the wrong profile. +#[test] +fn a_given_profile_attaches_through_the_player() { + let mut player = vec![0; 0x2000]; + pe_header(&mut player); + anchor_x64(&mut player, 0x200, BASE + 0x800); + put(&mut player, 0x800, &(BASE + 0x900).to_le_bytes()); + let mut low = vec![0; 0x200]; + put( + &mut low, + 0x800 - (0x200 + 7) - 0x500, + &0x1234_u32.to_le_bytes(), + ); + with_modules( + &[(BASE, &player), (0x500, &low)], + &[("UnityPlayer.dll", BASE, MODULE)], + |process| { + let profile = profiles::UNITY_6000_3_0F1_WINDOWS_X86_64; + let manager = SceneManager::attach(process, profile).unwrap(); + assert_eq!(manager.address, Address::new(BASE + 0x900)); + let x86 = Profile { + pointer_size: PointerSize::Bit32, + ..profile + }; + assert!(SceneManager::attach(process, x86).is_none()); + assert!(matches!( + crate::runtime::mock::poll_once(SceneManager::wait_attach(process, profile)), + core::task::Poll::Ready(_), + )); + }, + ); +} + #[cfg(feature = "alloc")] #[test] fn the_engine_module_is_the_player_when_there_is_one() { @@ -373,7 +409,7 @@ fn a_linux_player_takes_the_build_measured_on_linux() { let manager = with_modules( &[(BASE, &image)], &[("UnityPlayer.so", BASE, MODULE)], - SceneManager::attach, + SceneManager::attach_auto_detect, ) .unwrap(); assert_eq!(manager.address, Address::new(BASE + 0x900)); @@ -381,11 +417,17 @@ fn a_linux_player_takes_the_build_measured_on_linux() { assert_eq!(manager.profile.scene.roots, 0xe8); } -#[cfg(feature = "alloc")] -#[test] -fn an_executable_only_linux_player_attaches_automatically() { +// A Unity 5.6.7 Linux engine module, holding the manager at BASE + 0x900. +// One load segment covers the module, so its size can be read from the +// header. +fn old_linux_engine() -> Vec { let mut image = vec![0; 0x2000]; put(&mut image, 0, b"\x7fELF\x02\x01\x01"); + put(&mut image, 0x20, &0x40_u64.to_le_bytes()); + put(&mut image, 0x36, &56_u16.to_le_bytes()); + put(&mut image, 0x38, &1_u16.to_le_bytes()); + put(&mut image, 0x40, &1_u32.to_le_bytes()); + put(&mut image, 0x68, &MODULE.to_le_bytes()); put(&mut image, 0x300, b"\x005.6.7f1\0"); // The old Linux anchor loads the manager global into rbp before // checking the scene count at 0x18. @@ -402,16 +444,77 @@ fn an_executable_only_linux_player_attaches_automatically() { &[0x53, 0x8B, 0x45, 0x18, 0x85, 0xC0, 0x74], ); put(&mut image, 0x800, &(BASE + 0x900).to_le_bytes()); + image +} +// A given profile attaches through `UnityPlayer.so` too, without `alloc`. +#[test] +fn a_given_profile_attaches_through_the_linux_player() { + let image = old_linux_engine(); + with_modules( + &[(BASE, &image)], + &[("UnityPlayer.so", BASE, MODULE)], + |process| { + let profile = profiles::UNITY_5_6_2F1_LINUX_X86_64; + let manager = SceneManager::attach(process, profile).unwrap(); + assert_eq!(manager.address, Address::new(BASE + 0x900)); + }, + ); +} + +// A game that keeps the engine in its executable attaches through the module +// the auto splitter hands in, which doesn't need `alloc`. +#[test] +fn a_given_profile_attaches_through_a_given_module() { + let image = old_linux_engine(); + with_modules( + &[(BASE, &image)], + &[("game.x86_64", BASE, MODULE)], + |process| { + let module = process.get_module_address("game.x86_64").unwrap(); + let profile = profiles::UNITY_5_6_2F1_LINUX_X86_64; + #[cfg(not(feature = "alloc"))] + assert!(SceneManager::attach(process, profile).is_none()); + let manager = SceneManager::attach_to_module(process, module, profile).unwrap(); + assert_eq!(manager.address, Address::new(BASE + 0x900)); + assert!(matches!( + crate::runtime::mock::poll_once(SceneManager::wait_attach_to_module( + process, module, profile + )), + core::task::Poll::Ready(_), + )); + }, + ); +} + +// With `alloc`, a given profile also finds the engine in the executable of a +// game that has no player module. +#[cfg(feature = "alloc")] +#[test] +fn a_given_profile_attaches_through_the_executable() { + let image = old_linux_engine(); with_modules(&[(BASE, &image)], &[("fixture", BASE, MODULE)], |process| { - let manager = SceneManager::attach(process).unwrap(); + let profile = profiles::UNITY_5_6_2F1_LINUX_X86_64; + let manager = SceneManager::attach(process, profile).unwrap(); assert_eq!(manager.address, Address::new(BASE + 0x900)); - assert!(core::ptr::eq( - manager.profile, - &linux_builds::nearest((5, 6, 7, 0)).unwrap().profile, - )); + }); +} + +#[cfg(feature = "alloc")] +#[test] +fn an_executable_only_linux_player_attaches_automatically() { + let image = old_linux_engine(); + with_modules(&[(BASE, &image)], &[("fixture", BASE, MODULE)], |process| { + let manager = SceneManager::attach_auto_detect(process).unwrap(); + assert_eq!(manager.address, Address::new(BASE + 0x900)); + // `Signature` has no `PartialEq`, so the profiles compare by their + // debug output. + assert_eq!( + std::format!("{:?}", manager.profile), + std::format!("{:?}", linux_builds::nearest((5, 6, 7, 0)).unwrap().profile), + ); assert!(matches!( - crate::runtime::mock::poll_once(SceneManager::wait_attach(process)), + crate::runtime::mock::poll_once(SceneManager::wait_attach_auto_detect(process)), core::task::Poll::Ready(_), )); }); diff --git a/src/game_engine/unity/scene_manager/builds.rs b/src/game_engine/unity/scene_manager/builds.rs index 94aa7937..1924e25c 100644 --- a/src/game_engine/unity/scene_manager/builds.rs +++ b/src/game_engine/unity/scene_manager/builds.rs @@ -12,6 +12,7 @@ use crate::{signature::Signature, PointerSize}; /// One measured Unity player and the layout of its scene manager. pub(super) struct Build { + #[cfg(any(feature = "alloc", test))] pub(super) unity: (u16, u16, u16, u16), pub(super) profile: Profile, } @@ -21,6 +22,7 @@ pub(super) struct Build { /// executable before Unity 2017.2, and the first three of them are the /// patch. A player takes the newest build whose patch is at or below its own /// patch. A player below every build takes none. +#[cfg(any(feature = "alloc", test))] pub(super) fn nearest( unity: (u16, u16, u16, u16), pointer_size: PointerSize, @@ -30,6 +32,7 @@ pub(super) fn nearest( /// Finds the build for a player in the given table by the rule of /// [`nearest`]. +#[cfg(any(feature = "alloc", test))] pub(super) fn nearest_in( builds: &'static [Build], unity: (u16, u16, u16, u16), @@ -99,6 +102,7 @@ const SCENE_AT_GETTER_X86: Anchor = Anchor { pub(super) const BUILDS: &[Build] = &[ // Unity 5.6.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 0, 23754), profile: Profile { pointer_size: PointerSize::Bit64, @@ -130,6 +134,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 5.6.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 0, 23754), profile: Profile { pointer_size: PointerSize::Bit32, @@ -162,6 +167,7 @@ pub(super) const BUILDS: &[Build] = &[ // Unity 5.6.2f1, x64. Same layout as 5.6.0, but with the DontDestroyOnLoad // scene. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 2, 37180), profile: Profile { pointer_size: PointerSize::Bit64, @@ -194,6 +200,7 @@ pub(super) const BUILDS: &[Build] = &[ // Unity 5.6.2f1, x86. Same layout as 5.6.0, but with the DontDestroyOnLoad // scene. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 2, 37180), profile: Profile { pointer_size: PointerSize::Bit32, @@ -225,6 +232,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2017.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2017, 1, 0, 32737), profile: Profile { pointer_size: PointerSize::Bit64, @@ -256,6 +264,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2017.1.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2017, 1, 0, 32737), profile: Profile { pointer_size: PointerSize::Bit32, @@ -287,6 +296,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2017.3.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2017, 3, 0, 20311), profile: Profile { pointer_size: PointerSize::Bit32, @@ -320,6 +330,7 @@ pub(super) const BUILDS: &[Build] = &[ // pointer at its start here, so every member of a scene sits one pointer // further along than it does in 2018.1 and in 2018.3. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 2, 0, 44229), profile: Profile { pointer_size: PointerSize::Bit64, @@ -351,6 +362,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2018.2.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 2, 0, 44229), profile: Profile { pointer_size: PointerSize::Bit32, @@ -382,6 +394,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2018.3.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 3, 0, 9156), profile: Profile { pointer_size: PointerSize::Bit64, @@ -413,6 +426,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2018.3.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 3, 0, 9156), profile: Profile { pointer_size: PointerSize::Bit32, @@ -444,6 +458,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2021.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2021, 1, 0, 42313), profile: Profile { pointer_size: PointerSize::Bit64, @@ -475,6 +490,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2022.2.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2022, 2, 0, 56532), profile: Profile { pointer_size: PointerSize::Bit32, @@ -506,6 +522,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2022.3.5f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2022, 3, 5, 29734), profile: Profile { pointer_size: PointerSize::Bit64, @@ -537,6 +554,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2022.3.5f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2022, 3, 5, 29734), profile: Profile { pointer_size: PointerSize::Bit32, @@ -568,6 +586,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 0, 2298), profile: Profile { pointer_size: PointerSize::Bit64, @@ -599,6 +618,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 0, 2298), profile: Profile { pointer_size: PointerSize::Bit32, @@ -630,6 +650,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.4f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 4, 6702), profile: Profile { pointer_size: PointerSize::Bit64, @@ -661,6 +682,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.4f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 4, 6702), profile: Profile { pointer_size: PointerSize::Bit32, @@ -692,6 +714,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.2.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 2, 0, 54845), profile: Profile { pointer_size: PointerSize::Bit64, @@ -723,6 +746,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.2.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 2, 0, 54845), profile: Profile { pointer_size: PointerSize::Bit32, @@ -754,6 +778,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.0.59f2, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 0, 59, 10268), profile: Profile { pointer_size: PointerSize::Bit32, @@ -785,6 +810,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.1.0f1, x86. Same layout as 2023.2.0. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 1, 0, 41298), profile: Profile { pointer_size: PointerSize::Bit32, @@ -817,6 +843,7 @@ pub(super) const BUILDS: &[Build] = &[ // Unity 6000.2.0f1, x64. Same layout as 2023.2.0, but the anchor changed // from PROLOGUE_LOAD_X64 to SCENE_COUNT_GETTER_X64. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 2, 0, 53701), profile: Profile { pointer_size: PointerSize::Bit64, @@ -848,6 +875,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.2.2f1, x86. Same layout as 6000.0.59. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 2, 2, 14734), profile: Profile { pointer_size: PointerSize::Bit32, @@ -879,6 +907,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.3.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 3, 0, 34572), profile: Profile { pointer_size: PointerSize::Bit64, @@ -910,6 +939,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.3.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 3, 0, 34572), profile: Profile { pointer_size: PointerSize::Bit32, @@ -941,6 +971,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.5.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 5, 0, 46204), profile: Profile { pointer_size: PointerSize::Bit64, @@ -972,6 +1003,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.5.0f1, x86. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 5, 0, 46204), profile: Profile { pointer_size: PointerSize::Bit32, diff --git a/src/game_engine/unity/scene_manager/components_tests.rs b/src/game_engine/unity/scene_manager/components_tests.rs index 65579284..785c0bd4 100644 --- a/src/game_engine/unity/scene_manager/components_tests.rs +++ b/src/game_engine/unity/scene_manager/components_tests.rs @@ -33,7 +33,7 @@ fn manager(unity: (u16, u16, u16, u16)) -> SceneManager { SceneManager { pointer_size: PointerSize::Bit64, address: Address::new(BASE), - profile: &builds::nearest(unity, PointerSize::Bit64).unwrap().profile, + profile: builds::nearest(unity, PointerSize::Bit64).unwrap().profile, } } diff --git a/src/game_engine/unity/scene_manager/linux_builds.rs b/src/game_engine/unity/scene_manager/linux_builds.rs index 6fbbb2ef..53978eb3 100644 --- a/src/game_engine/unity/scene_manager/linux_builds.rs +++ b/src/game_engine/unity/scene_manager/linux_builds.rs @@ -4,7 +4,9 @@ //! the offsets of the Windows x64 build that the same version takes, so only //! the anchors differ. -use super::builds::{nearest_in, Build}; +#[cfg(any(feature = "alloc", test))] +use super::builds::nearest_in; +use super::builds::Build; use super::offsets::{ Anchor, GameObjectOffsets, ManagerOffsets, ObjectOffsets, PathShape, Profile, ReferenceShape, SceneOffsets, TransformOffsets, @@ -14,6 +16,7 @@ use crate::{signature::Signature, PointerSize}; /// Finds the build for a Linux x64 player by the rule of [`nearest_in`]. The /// Unity version comes from the version string in the engine module, so its /// fourth part is zero. +#[cfg(any(feature = "alloc", test))] pub(super) fn nearest(unity: (u16, u16, u16, u16)) -> Option<&'static Build> { nearest_in(BUILDS, unity, PointerSize::Bit64) } @@ -87,6 +90,7 @@ const VIRTUAL_CALL_X64: Anchor = Anchor { pub(super) const BUILDS: &[Build] = &[ // Unity 5.6.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 0, 23754), profile: Profile { pointer_size: PointerSize::Bit64, @@ -119,6 +123,7 @@ pub(super) const BUILDS: &[Build] = &[ // Unity 5.6.2f1, x64. Same layout as 5.6.0, but with the DontDestroyOnLoad // scene. Build { + #[cfg(any(feature = "alloc", test))] unity: (5, 6, 2, 37180), profile: Profile { pointer_size: PointerSize::Bit64, @@ -150,6 +155,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2017.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2017, 1, 0, 32737), profile: Profile { pointer_size: PointerSize::Bit64, @@ -181,6 +187,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2017.3.0f1, x64. Same layout as 2017.1.0, but the anchor changed. Build { + #[cfg(any(feature = "alloc", test))] unity: (2017, 3, 0, 20311), profile: Profile { pointer_size: PointerSize::Bit64, @@ -212,6 +219,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2018.2.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 2, 0, 44229), profile: Profile { pointer_size: PointerSize::Bit64, @@ -243,6 +251,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2018.3.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2018, 3, 0, 9156), profile: Profile { pointer_size: PointerSize::Bit64, @@ -274,6 +283,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2019.1.0f1, x64. Same layout as 2018.3.0, but the anchor changed. Build { + #[cfg(any(feature = "alloc", test))] unity: (2019, 1, 0, 21026), profile: Profile { pointer_size: PointerSize::Bit64, @@ -305,6 +315,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2019.3.0f1, x64. Same layout as 2018.3.0, but the anchor changed. Build { + #[cfg(any(feature = "alloc", test))] unity: (2019, 3, 0, 44266), profile: Profile { pointer_size: PointerSize::Bit64, @@ -336,6 +347,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2020.2.0f1, x64. Same layout as 2018.3.0, but the anchor changed. Build { + #[cfg(any(feature = "alloc", test))] unity: (2020, 2, 0, 8671), profile: Profile { pointer_size: PointerSize::Bit64, @@ -367,6 +379,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2021.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2021, 1, 0, 42313), profile: Profile { pointer_size: PointerSize::Bit64, @@ -398,6 +411,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2022.3.5f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2022, 3, 5, 29734), profile: Profile { pointer_size: PointerSize::Bit64, @@ -429,6 +443,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 0, 2298), profile: Profile { pointer_size: PointerSize::Bit64, @@ -460,6 +475,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.1.4f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 1, 4, 6702), profile: Profile { pointer_size: PointerSize::Bit64, @@ -491,6 +507,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 2023.2.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (2023, 2, 0, 54845), profile: Profile { pointer_size: PointerSize::Bit64, @@ -522,6 +539,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.3.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 3, 0, 34572), profile: Profile { pointer_size: PointerSize::Bit64, @@ -553,6 +571,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.5.0f1, x64. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 5, 0, 46204), profile: Profile { pointer_size: PointerSize::Bit64, @@ -584,6 +603,7 @@ pub(super) const BUILDS: &[Build] = &[ }, // Unity 6000.6.0f1, x64. Same layout as 6000.5.0, but the anchor changed. Build { + #[cfg(any(feature = "alloc", test))] unity: (6000, 6, 0, 63725), profile: Profile { pointer_size: PointerSize::Bit64, diff --git a/src/game_engine/unity/scene_manager/mod.rs b/src/game_engine/unity/scene_manager/mod.rs index 19c146c6..332931fe 100644 --- a/src/game_engine/unity/scene_manager/mod.rs +++ b/src/game_engine/unity/scene_manager/mod.rs @@ -2,8 +2,12 @@ //! //! Automatic attachment requires the `alloc` feature so that a game without //! a separate player module can be identified through its executable. A -//! `no_std` auto splitter must provide a global allocator. Reading scenes, -//! transforms, and components does not require allocation. +//! `no_std` auto splitter must provide a global allocator. Attaching to a +//! player module with a known [`Profile`] through [`SceneManager::attach`] +//! doesn't allocate on Windows and Linux. Neither does +//! [`SceneManager::attach_to_module`], where the auto splitter hands in the +//! module itself. Reading scenes, transforms, and components doesn't allocate +//! either. // References: // https://gist.githubusercontent.com/just-ero/92457b51baf85bd1e5b8c87de8c9835e/raw/8aa3e6b8da01fd03ff2ff0c03cbd018e522ef988/UnityScene.hpp @@ -12,18 +16,17 @@ // Transforms and GameObjects is taken from https://github.com/Micrologist/UnityInstanceDumper #[cfg(feature = "alloc")] +use crate::{file_format::macho, print_limited}; use crate::{ - file_format::{elf, macho, pe}, + file_format::{elf, pe}, future::retry, - print_limited, + string::ArrayCString, + Address, Error, PointerSize, Process, }; -use crate::{string::ArrayCString, Address, Error, PointerSize, Process}; -// The tables are also used by allocation-free tests of the scene readers. -#[cfg(any(feature = "alloc", test))] mod builds; -#[cfg(any(feature = "alloc", test))] mod linux_builds; +pub mod profiles; mod game_objects; @@ -39,19 +42,23 @@ mod roots_tests; #[cfg(all(test, not(target_family = "wasm")))] mod components_tests; +#[cfg(all(test, not(target_family = "wasm")))] +mod profiles_tests; + mod offsets; mod transform; pub use transform::Transform; -use offsets::Profile; +pub use offsets::{ + Anchor, GameObjectOffsets, ManagerOffsets, ObjectOffsets, PathShape, Profile, ReferenceShape, + SceneOffsets, TransformOffsets, +}; mod scene; pub use scene::Scene; -#[cfg(feature = "alloc")] -use super::BinaryFormat; -use super::CSTR; +use super::{BinaryFormat, CSTR}; /// The most scenes a game can have loaded at once. No game comes anywhere /// near 4,096. @@ -65,7 +72,7 @@ const SCENES: u32 = 1 << 12; pub struct SceneManager { pointer_size: PointerSize, address: Address, - profile: &'static Profile, + profile: Profile, } impl SceneManager { @@ -78,10 +85,11 @@ impl SceneManager { /// module keeps the engine in its executable, and finding that executable /// allocates its path. A `no_std` auto splitter must provide a global /// allocator. Basic scene, transform, and component reads do not require - /// allocation. + /// allocation. If you know the build in advance, use + /// [`attach`](Self::attach) with its profile instead. #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub fn attach(process: &Process) -> Option { + pub fn attach_auto_detect(process: &Process) -> Option { let (unity_player, format) = Self::engine_module(process)?; let profile = match format { @@ -91,7 +99,7 @@ impl SceneManager { let unity = Self::unity_version(process, unity_player.0)?; let build = builds::nearest(unity, pointer_size)?; Self::print_build(unity, build); - &build.profile + build.profile } BinaryFormat::ELF => { if elf::pointer_size(process, unity_player.0)? != PointerSize::Bit64 { @@ -100,10 +108,10 @@ impl SceneManager { let unity = super::version_string(process, unity_player)?; let build = linux_builds::nearest(unity)?; Self::print_build(unity, build); - &build.profile + build.profile } BinaryFormat::MachO => match macho::pointer_size(process, unity_player)? { - PointerSize::Bit64 => &builds::MACHO_X64, + PointerSize::Bit64 => builds::MACHO_X64, _ => return None, }, }; @@ -129,27 +137,104 @@ impl SceneManager { /// Attaches to the scene manager in the given process. /// - /// This is the `await`able version of the [`attach`](Self::attach) - /// function, yielding back to the runtime between each try. It requires - /// the `alloc` feature so that the executable's path can be allocated when - /// no player module is present. A `no_std` auto splitter must provide a - /// global allocator. + /// This is the `await`able version of the + /// [`attach_auto_detect`](Self::attach_auto_detect) function, yielding back + /// to the runtime between each try. It requires the `alloc` feature so that + /// the executable's path can be allocated when no player module is present. + /// A `no_std` auto splitter must provide a global allocator. For + /// allocation-free attachment, use [`wait_attach`](Self::wait_attach) with + /// a known profile instead. #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] - pub async fn wait_attach(process: &Process) -> SceneManager { - retry(|| Self::attach(process)).await + pub async fn wait_attach_auto_detect(process: &Process) -> SceneManager { + retry(|| Self::attach_auto_detect(process)).await } - /// Finds the module that holds the engine: `UnityPlayer.dll` and its - /// Linux and Mac siblings, or the game's own executable, which linked the - /// engine in before Unity 2017.2 on Windows and on older Linux players. - /// Finding the executable needs its name, so that part needs the `alloc` - /// feature. - #[cfg(feature = "alloc")] - fn engine_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> { - let player = [ + /// Attaches to the scene manager in the given process with the provided + /// measured [`Profile`]. The profile's pointer width must match the game. + /// Use a built-in [`profiles`] constant for a known player, or construct a + /// custom profile for another measured layout. + /// + /// This looks for the engine in `UnityPlayer.dll` or `UnityPlayer.so`. + /// With the `alloc` feature, it also looks in `UnityPlayer.dylib`, and in + /// the game's own executable when there is no player module. Finding the + /// executable allocates its path, so without `alloc` a game that keeps the + /// engine in its executable needs [`attach_to_module`](Self::attach_to_module). + /// Finding a player module does not allocate on Windows and Linux. + pub fn attach(process: &Process, profile: Profile) -> Option { + #[cfg(feature = "alloc")] + let (unity_player, format) = Self::engine_module(process)?; + #[cfg(not(feature = "alloc"))] + let (unity_player, format) = Self::player_module(process)?; + Self::attach_in(process, unity_player, format, profile) + } + + /// Attaches to the scene manager in the given process with the provided + /// measured [`Profile`]. The profile's pointer width must match the game. + /// Finding a player module does not allocate on Windows and Linux. + /// + /// This is the `await`able version of [`attach`](Self::attach), yielding + /// back to the runtime between each try. + pub async fn wait_attach(process: &Process, profile: Profile) -> SceneManager { + retry(|| Self::attach(process, profile)).await + } + + /// Attaches to the scene manager in the module at `engine_module` with + /// the provided measured [`Profile`]. Use this when the engine sits in a + /// module [`attach`](Self::attach) doesn't look for, such as a renamed + /// player module or, without the `alloc` feature, the executable of a game + /// that has no player module. [`Process::get_module_address`] finds a + /// module by its name without allocating. The module must be a PE or an + /// ELF image whose pointer width matches the profile. + pub fn attach_to_module( + process: &Process, + engine_module: Address, + profile: Profile, + ) -> Option { + let (engine_module, format) = super::module_at(process, engine_module)?; + Self::attach_in(process, engine_module, format, profile) + } + + /// Attaches to the scene manager in the module at `engine_module` with + /// the provided measured [`Profile`]. + /// + /// This is the `await`able version of + /// [`attach_to_module`](Self::attach_to_module), yielding back to the + /// runtime between each try. + pub async fn wait_attach_to_module( + process: &Process, + engine_module: Address, + profile: Profile, + ) -> SceneManager { + retry(|| Self::attach_to_module(process, engine_module, profile)).await + } + + /// Attaches through the anchor of the profile once the engine module is + /// known, if the module's pointer width matches the profile. + fn attach_in( + process: &Process, + engine_module: (Address, u64), + format: BinaryFormat, + profile: Profile, + ) -> Option { + let pointer_size = match format { + BinaryFormat::PE => pe::MachineType::read(process, engine_module.0)?.pointer_size()?, + BinaryFormat::ELF => elf::pointer_size(process, engine_module.0)?, + #[cfg(feature = "alloc")] + BinaryFormat::MachO => macho::pointer_size(process, engine_module)?, + }; + (pointer_size == profile.pointer_size).then_some(())?; + + Self::attach_with(process, engine_module, profile) + } + + /// Finds the player module that holds the engine: `UnityPlayer.dll` and + /// its Linux sibling, plus its Mac sibling with the `alloc` feature. + fn player_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> { + [ ("UnityPlayer.dll", BinaryFormat::PE), ("UnityPlayer.so", BinaryFormat::ELF), + #[cfg(feature = "alloc")] ("UnityPlayer.dylib", BinaryFormat::MachO), ] .into_iter() @@ -162,9 +247,17 @@ impl SceneManager { )) } _ => Some((process.get_module_range(name).ok()?, format)), - }); + }) + } - player.or_else(|| { + /// Finds the module that holds the engine: `UnityPlayer.dll` and its + /// Linux and Mac siblings, or the game's own executable, which linked the + /// engine in before Unity 2017.2 on Windows and on older Linux players. + /// Finding the executable needs its name, so that part needs the `alloc` + /// feature. + #[cfg(feature = "alloc")] + fn engine_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> { + Self::player_module(process).or_else(|| { let executable = process.get_main_module_range().ok()?; if pe::MachineType::read(process, executable.0).is_some() { return Some((executable, BinaryFormat::PE)); @@ -190,11 +283,10 @@ impl SceneManager { /// Finds the scene manager in the engine module through the anchor of /// the profile. Every match of the anchor must name the same global, /// and the global must hold the manager. - #[cfg(any(feature = "alloc", test))] fn attach_with( process: &Process, unity_player: (Address, u64), - profile: &'static Profile, + profile: Profile, ) -> Option { let pointer_size = profile.pointer_size; let displacement = profile.anchor.displacement as u64; diff --git a/src/game_engine/unity/scene_manager/offsets.rs b/src/game_engine/unity/scene_manager/offsets.rs index d3158135..3e982ea1 100644 --- a/src/game_engine/unity/scene_manager/offsets.rs +++ b/src/game_engine/unity/scene_manager/offsets.rs @@ -1,20 +1,20 @@ -#[cfg(any(feature = "alloc", test))] use crate::{signature::Signature, PointerSize}; /// How the scene manager global is found: the body of a function that loads /// the global, with the displacement of the load masked out, and where that -/// displacement starts inside a match. A body shorter than the signature is -/// padded with wildcards. -#[derive(Debug)] -#[cfg(any(feature = "alloc", test))] -pub(super) struct Anchor { - pub(super) signature: Signature<24>, - pub(super) displacement: u8, +/// displacement starts inside a match. The signature always holds 24 bytes, +/// so a shorter body is written out with wildcards after it. +#[derive(Copy, Clone, Debug)] +pub struct Anchor { + /// The body of the function, with the displacement masked out. + pub signature: Signature<24>, + /// Where the displacement of the load starts inside a match. + pub displacement: u8, } /// How a scene keeps its path in the 32 bytes of its path field. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(super) enum PathShape { +pub enum PathShape { /// A pointer to the characters. Pointer, /// A path of up to 31 characters sits inline, with a NUL after it. A @@ -29,7 +29,7 @@ pub(super) enum PathShape { /// How a component reaches its managed object through the managed reference /// of its `Object` base. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(super) enum ReferenceShape { +pub enum ReferenceShape { /// The reference holds the managed object. CachedObject, /// The reference points at a slot that holds the managed object. @@ -41,58 +41,126 @@ pub(super) enum ReferenceShape { /// count two pointers after it. The `DontDestroyOnLoad` scene is embedded in /// the manager by value. Unity 5.6.0 and 5.6.1 have no such scene, so it is /// `None` for them. -#[derive(Debug)] -pub(super) struct ManagerOffsets { - pub(super) scenes: u8, - pub(super) active_scene: u8, - pub(super) dont_destroy_on_load_scene: Option, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ManagerOffsets { + /// The pointer to the loaded scenes. + pub scenes: u8, + /// The pointer to the active scene. + pub active_scene: u8, + /// The `DontDestroyOnLoad` scene, when the build has one. + pub dont_destroy_on_load_scene: Option, } /// The members of `UnityScene` the walk reads. The roots are a circular list /// whose head is embedded in the scene at `roots`. -#[derive(Debug)] -pub(super) struct SceneOffsets { - pub(super) path: u8, - pub(super) build_index: u8, - pub(super) roots: u16, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SceneOffsets { + /// The 32-byte path field, read by the [`PathShape`] of the build. + pub path: u8, + /// The build index. + pub build_index: u8, + /// The head of the list of root transforms. + pub roots: u16, } /// The members of `Transform` the walk reads. The game object is a member of /// the `Component` base. The children are a dynamic array of transforms. -#[derive(Debug)] -pub(super) struct TransformOffsets { - pub(super) game_object: u8, - pub(super) children: u8, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TransformOffsets { + /// The pointer to the game object. + pub game_object: u8, + /// The pointer to the child transforms. + pub children: u8, } /// The members of `GameObject` the walk reads. The components are a dynamic /// array of pairs, each a type index and a pointer to the component. -#[derive(Debug)] -pub(super) struct GameObjectOffsets { - pub(super) components: u8, - pub(super) name: u8, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct GameObjectOffsets { + /// The pointer to the components. + pub components: u8, + /// The pointer to the name. + pub name: u8, } /// The members of `Object` the walk reads, which every component starts /// with. The managed reference leads to the managed object of the component /// by the [`ReferenceShape`] of the build. -#[derive(Debug)] -pub(super) struct ObjectOffsets { - pub(super) managed_reference: u8, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ObjectOffsets { + /// The managed reference. + pub managed_reference: u8, } -/// What the walk needs to know about one player. -#[derive(Debug)] -pub(super) struct Profile { - #[cfg(any(feature = "alloc", test))] - pub(super) pointer_size: PointerSize, - #[cfg(any(feature = "alloc", test))] - pub(super) anchor: Anchor, - pub(super) path: PathShape, - pub(super) reference: ReferenceShape, - pub(super) manager: ManagerOffsets, - pub(super) scene: SceneOffsets, - pub(super) transform: TransformOffsets, - pub(super) game_object: GameObjectOffsets, - pub(super) object: ObjectOffsets, +/// A complete measured layout of the scene manager and the objects it +/// reaches, with the anchor that finds the manager. Built-in measured +/// profiles are available in [`profiles`](super::profiles). +/// +/// A custom profile lists the complete layout. Every field is written out on +/// purpose: struct update syntax would let the custom profile silently take +/// fields that a later release adds to the source profile. +/// +/// ``` +/// use asr::{ +/// game_engine::unity::scene_manager::{ +/// Anchor, GameObjectOffsets, ManagerOffsets, ObjectOffsets, PathShape, Profile, +/// ReferenceShape, SceneOffsets, TransformOffsets, +/// }, +/// signature::Signature, +/// PointerSize, +/// }; +/// +/// const CUSTOM_PROFILE: Profile = Profile { +/// pointer_size: PointerSize::Bit64, +/// anchor: Anchor { +/// signature: Signature::new( +/// "48 8B 05 ?? ?? ?? ?? 8B 40 18 C3 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??", +/// ), +/// displacement: 3, +/// }, +/// path: PathShape::InlineSpare, +/// reference: ReferenceShape::RootSlot, +/// manager: ManagerOffsets { +/// scenes: 0x8, +/// active_scene: 0x48, +/// dont_destroy_on_load_scene: Some(0x70), +/// }, +/// scene: SceneOffsets { +/// path: 0x10, +/// build_index: 0x98, +/// roots: 0xe8, +/// }, +/// transform: TransformOffsets { +/// game_object: 0x20, +/// children: 0x48, +/// }, +/// game_object: GameObjectOffsets { +/// components: 0x20, +/// name: 0x50, +/// }, +/// object: ObjectOffsets { +/// managed_reference: 0x18, +/// }, +/// }; +/// ``` +#[derive(Copy, Clone, Debug)] +pub struct Profile { + /// The pointer width this profile was measured for. + pub pointer_size: PointerSize, + /// The anchor that finds the scene manager. + pub anchor: Anchor, + /// How a scene keeps its path. + pub path: PathShape, + /// How a component reaches its managed object. + pub reference: ReferenceShape, + /// Offsets within `RuntimeSceneManager`. + pub manager: ManagerOffsets, + /// Offsets within `UnityScene`. + pub scene: SceneOffsets, + /// Offsets within `Transform`. + pub transform: TransformOffsets, + /// Offsets within `GameObject`. + pub game_object: GameObjectOffsets, + /// Offsets within `Object`. + pub object: ObjectOffsets, } diff --git a/src/game_engine/unity/scene_manager/profiles.rs b/src/game_engine/unity/scene_manager/profiles.rs new file mode 100644 index 00000000..e9cf7760 --- /dev/null +++ b/src/game_engine/unity/scene_manager/profiles.rs @@ -0,0 +1,120 @@ +//! Complete scene manager layouts measured from specific Unity players. +//! +//! Each constant is the layout for the platform and pointer size in its +//! name, measured on the first player that has it. The layout holds until the +//! version of the next constant for the same platform and pointer size. Pass a +//! profile directly to [`SceneManager::attach`](super::SceneManager::attach) +//! or [`SceneManager::wait_attach`](super::SceneManager::wait_attach) when the +//! target game is known. This avoids linking the automatic lookup tables and +//! their other profiles into the final auto splitter. + +use super::{builds, linux_builds, Profile}; + +macro_rules! profiles { + ($table:path; $($(#[$meta:meta])* $name:ident = $index:literal;)+) => { + $( + $(#[$meta])* + pub const $name: Profile = $table[$index].profile; + )+ + }; +} + +profiles! { + builds::BUILDS; + /// Unity 5.6.0f1, Windows, x86-64, file version `5.6.0.23754`. + UNITY_5_6_0F1_WINDOWS_X86_64 = 0; + /// Unity 5.6.0f1, Windows, x86, file version `5.6.0.23754`. + UNITY_5_6_0F1_WINDOWS_X86 = 1; + /// Unity 5.6.2f1, Windows, x86-64, file version `5.6.2.37180`. + UNITY_5_6_2F1_WINDOWS_X86_64 = 2; + /// Unity 5.6.2f1, Windows, x86, file version `5.6.2.37180`. + UNITY_5_6_2F1_WINDOWS_X86 = 3; + /// Unity 2017.1.0f1, Windows, x86-64, file version `2017.1.0.32737`. + UNITY_2017_1_0F1_WINDOWS_X86_64 = 4; + /// Unity 2017.1.0f1, Windows, x86, file version `2017.1.0.32737`. + UNITY_2017_1_0F1_WINDOWS_X86 = 5; + /// Unity 2017.3.0f1, Windows, x86, file version `2017.3.0.20311`. + UNITY_2017_3_0F1_WINDOWS_X86 = 6; + /// Unity 2018.2.0f1, Windows, x86-64, file version `2018.2.0.44229`. + UNITY_2018_2_0F1_WINDOWS_X86_64 = 7; + /// Unity 2018.2.0f1, Windows, x86, file version `2018.2.0.44229`. + UNITY_2018_2_0F1_WINDOWS_X86 = 8; + /// Unity 2018.3.0f1, Windows, x86-64, file version `2018.3.0.9156`. + UNITY_2018_3_0F1_WINDOWS_X86_64 = 9; + /// Unity 2018.3.0f1, Windows, x86, file version `2018.3.0.9156`. + UNITY_2018_3_0F1_WINDOWS_X86 = 10; + /// Unity 2021.1.0f1, Windows, x86-64, file version `2021.1.0.42313`. + UNITY_2021_1_0F1_WINDOWS_X86_64 = 11; + /// Unity 2022.2.0f1, Windows, x86, file version `2022.2.0.56532`. + UNITY_2022_2_0F1_WINDOWS_X86 = 12; + /// Unity 2022.3.5f1, Windows, x86-64, file version `2022.3.5.29734`. + UNITY_2022_3_5F1_WINDOWS_X86_64 = 13; + /// Unity 2022.3.5f1, Windows, x86, file version `2022.3.5.29734`. + UNITY_2022_3_5F1_WINDOWS_X86 = 14; + /// Unity 2023.1.0f1, Windows, x86-64, file version `2023.1.0.2298`. + UNITY_2023_1_0F1_WINDOWS_X86_64 = 15; + /// Unity 2023.1.0f1, Windows, x86, file version `2023.1.0.2298`. + UNITY_2023_1_0F1_WINDOWS_X86 = 16; + /// Unity 2023.1.4f1, Windows, x86-64, file version `2023.1.4.6702`. + UNITY_2023_1_4F1_WINDOWS_X86_64 = 17; + /// Unity 2023.1.4f1, Windows, x86, file version `2023.1.4.6702`. + UNITY_2023_1_4F1_WINDOWS_X86 = 18; + /// Unity 2023.2.0f1, Windows, x86-64, file version `2023.2.0.54845`. + UNITY_2023_2_0F1_WINDOWS_X86_64 = 19; + /// Unity 2023.2.0f1, Windows, x86, file version `2023.2.0.54845`. + UNITY_2023_2_0F1_WINDOWS_X86 = 20; + /// Unity 6000.0.59f2, Windows, x86, file version `6000.0.59.10268`. + UNITY_6000_0_59F2_WINDOWS_X86 = 21; + /// Unity 6000.1.0f1, Windows, x86, file version `6000.1.0.41298`. + UNITY_6000_1_0F1_WINDOWS_X86 = 22; + /// Unity 6000.2.0f1, Windows, x86-64, file version `6000.2.0.53701`. + UNITY_6000_2_0F1_WINDOWS_X86_64 = 23; + /// Unity 6000.2.2f1, Windows, x86, file version `6000.2.2.14734`. + UNITY_6000_2_2F1_WINDOWS_X86 = 24; + /// Unity 6000.3.0f1, Windows, x86-64, file version `6000.3.0.34572`. + UNITY_6000_3_0F1_WINDOWS_X86_64 = 25; + /// Unity 6000.3.0f1, Windows, x86, file version `6000.3.0.34572`. + UNITY_6000_3_0F1_WINDOWS_X86 = 26; + /// Unity 6000.5.0f1, Windows, x86-64, file version `6000.5.0.46204`. + UNITY_6000_5_0F1_WINDOWS_X86_64 = 27; + /// Unity 6000.5.0f1, Windows, x86, file version `6000.5.0.46204`. + UNITY_6000_5_0F1_WINDOWS_X86 = 28; +} + +profiles! { + linux_builds::BUILDS; + /// Unity 5.6.0f1, Linux, x86-64. + UNITY_5_6_0F1_LINUX_X86_64 = 0; + /// Unity 5.6.2f1, Linux, x86-64. + UNITY_5_6_2F1_LINUX_X86_64 = 1; + /// Unity 2017.1.0f1, Linux, x86-64. + UNITY_2017_1_0F1_LINUX_X86_64 = 2; + /// Unity 2017.3.0f1, Linux, x86-64. + UNITY_2017_3_0F1_LINUX_X86_64 = 3; + /// Unity 2018.2.0f1, Linux, x86-64. + UNITY_2018_2_0F1_LINUX_X86_64 = 4; + /// Unity 2018.3.0f1, Linux, x86-64. + UNITY_2018_3_0F1_LINUX_X86_64 = 5; + /// Unity 2019.1.0f1, Linux, x86-64. + UNITY_2019_1_0F1_LINUX_X86_64 = 6; + /// Unity 2019.3.0f1, Linux, x86-64. + UNITY_2019_3_0F1_LINUX_X86_64 = 7; + /// Unity 2020.2.0f1, Linux, x86-64. + UNITY_2020_2_0F1_LINUX_X86_64 = 8; + /// Unity 2021.1.0f1, Linux, x86-64. + UNITY_2021_1_0F1_LINUX_X86_64 = 9; + /// Unity 2022.3.5f1, Linux, x86-64. + UNITY_2022_3_5F1_LINUX_X86_64 = 10; + /// Unity 2023.1.0f1, Linux, x86-64. + UNITY_2023_1_0F1_LINUX_X86_64 = 11; + /// Unity 2023.1.4f1, Linux, x86-64. + UNITY_2023_1_4F1_LINUX_X86_64 = 12; + /// Unity 2023.2.0f1, Linux, x86-64. + UNITY_2023_2_0F1_LINUX_X86_64 = 13; + /// Unity 6000.3.0f1, Linux, x86-64. + UNITY_6000_3_0F1_LINUX_X86_64 = 14; + /// Unity 6000.5.0f1, Linux, x86-64. + UNITY_6000_5_0F1_LINUX_X86_64 = 15; + /// Unity 6000.6.0f1, Linux, x86-64. + UNITY_6000_6_0F1_LINUX_X86_64 = 16; +} diff --git a/src/game_engine/unity/scene_manager/profiles_tests.rs b/src/game_engine/unity/scene_manager/profiles_tests.rs new file mode 100644 index 00000000..02cb73ac --- /dev/null +++ b/src/game_engine/unity/scene_manager/profiles_tests.rs @@ -0,0 +1,203 @@ +//! Tests pinning the public profile constants: each is the layout of the +//! measured player it came from. + +use super::profiles::*; +use super::{builds, linux_builds, Profile}; +use crate::PointerSize; + +use std::format; + +// `Signature` has no `PartialEq`, so the profiles compare by their debug +// output. +fn same(a: &Profile, b: &Profile) -> bool { + format!("{a:?}") == format!("{b:?}") +} + +// Every constant is pinned to its build, so an entry added between two +// others, which renumbers the ones after it, fails here rather than in a +// splitter. +#[test] +fn windows_constants_are_their_table_entries() { + let profiles: [((u16, u16, u16, u16), PointerSize, Profile); 29] = [ + ( + (5, 6, 0, 23754), + PointerSize::Bit64, + UNITY_5_6_0F1_WINDOWS_X86_64, + ), + ( + (5, 6, 0, 23754), + PointerSize::Bit32, + UNITY_5_6_0F1_WINDOWS_X86, + ), + ( + (5, 6, 2, 37180), + PointerSize::Bit64, + UNITY_5_6_2F1_WINDOWS_X86_64, + ), + ( + (5, 6, 2, 37180), + PointerSize::Bit32, + UNITY_5_6_2F1_WINDOWS_X86, + ), + ( + (2017, 1, 0, 32737), + PointerSize::Bit64, + UNITY_2017_1_0F1_WINDOWS_X86_64, + ), + ( + (2017, 1, 0, 32737), + PointerSize::Bit32, + UNITY_2017_1_0F1_WINDOWS_X86, + ), + ( + (2017, 3, 0, 20311), + PointerSize::Bit32, + UNITY_2017_3_0F1_WINDOWS_X86, + ), + ( + (2018, 2, 0, 44229), + PointerSize::Bit64, + UNITY_2018_2_0F1_WINDOWS_X86_64, + ), + ( + (2018, 2, 0, 44229), + PointerSize::Bit32, + UNITY_2018_2_0F1_WINDOWS_X86, + ), + ( + (2018, 3, 0, 9156), + PointerSize::Bit64, + UNITY_2018_3_0F1_WINDOWS_X86_64, + ), + ( + (2018, 3, 0, 9156), + PointerSize::Bit32, + UNITY_2018_3_0F1_WINDOWS_X86, + ), + ( + (2021, 1, 0, 42313), + PointerSize::Bit64, + UNITY_2021_1_0F1_WINDOWS_X86_64, + ), + ( + (2022, 2, 0, 56532), + PointerSize::Bit32, + UNITY_2022_2_0F1_WINDOWS_X86, + ), + ( + (2022, 3, 5, 29734), + PointerSize::Bit64, + UNITY_2022_3_5F1_WINDOWS_X86_64, + ), + ( + (2022, 3, 5, 29734), + PointerSize::Bit32, + UNITY_2022_3_5F1_WINDOWS_X86, + ), + ( + (2023, 1, 0, 2298), + PointerSize::Bit64, + UNITY_2023_1_0F1_WINDOWS_X86_64, + ), + ( + (2023, 1, 0, 2298), + PointerSize::Bit32, + UNITY_2023_1_0F1_WINDOWS_X86, + ), + ( + (2023, 1, 4, 6702), + PointerSize::Bit64, + UNITY_2023_1_4F1_WINDOWS_X86_64, + ), + ( + (2023, 1, 4, 6702), + PointerSize::Bit32, + UNITY_2023_1_4F1_WINDOWS_X86, + ), + ( + (2023, 2, 0, 54845), + PointerSize::Bit64, + UNITY_2023_2_0F1_WINDOWS_X86_64, + ), + ( + (2023, 2, 0, 54845), + PointerSize::Bit32, + UNITY_2023_2_0F1_WINDOWS_X86, + ), + ( + (6000, 0, 59, 10268), + PointerSize::Bit32, + UNITY_6000_0_59F2_WINDOWS_X86, + ), + ( + (6000, 1, 0, 41298), + PointerSize::Bit32, + UNITY_6000_1_0F1_WINDOWS_X86, + ), + ( + (6000, 2, 0, 53701), + PointerSize::Bit64, + UNITY_6000_2_0F1_WINDOWS_X86_64, + ), + ( + (6000, 2, 2, 14734), + PointerSize::Bit32, + UNITY_6000_2_2F1_WINDOWS_X86, + ), + ( + (6000, 3, 0, 34572), + PointerSize::Bit64, + UNITY_6000_3_0F1_WINDOWS_X86_64, + ), + ( + (6000, 3, 0, 34572), + PointerSize::Bit32, + UNITY_6000_3_0F1_WINDOWS_X86, + ), + ( + (6000, 5, 0, 46204), + PointerSize::Bit64, + UNITY_6000_5_0F1_WINDOWS_X86_64, + ), + ( + (6000, 5, 0, 46204), + PointerSize::Bit32, + UNITY_6000_5_0F1_WINDOWS_X86, + ), + ]; + for (unity, pointer_size, profile) in profiles { + let build = builds::nearest(unity, pointer_size).unwrap(); + assert_eq!(build.unity, unity, "{unity:?} {pointer_size:?}"); + assert!(same(&build.profile, &profile), "{unity:?} {pointer_size:?}"); + assert_eq!(profile.pointer_size, pointer_size); + } +} + +#[test] +fn linux_constants_are_their_table_entries() { + let profiles: [((u16, u16, u16, u16), Profile); 17] = [ + ((5, 6, 0, 23754), UNITY_5_6_0F1_LINUX_X86_64), + ((5, 6, 2, 37180), UNITY_5_6_2F1_LINUX_X86_64), + ((2017, 1, 0, 32737), UNITY_2017_1_0F1_LINUX_X86_64), + ((2017, 3, 0, 20311), UNITY_2017_3_0F1_LINUX_X86_64), + ((2018, 2, 0, 44229), UNITY_2018_2_0F1_LINUX_X86_64), + ((2018, 3, 0, 9156), UNITY_2018_3_0F1_LINUX_X86_64), + ((2019, 1, 0, 21026), UNITY_2019_1_0F1_LINUX_X86_64), + ((2019, 3, 0, 44266), UNITY_2019_3_0F1_LINUX_X86_64), + ((2020, 2, 0, 8671), UNITY_2020_2_0F1_LINUX_X86_64), + ((2021, 1, 0, 42313), UNITY_2021_1_0F1_LINUX_X86_64), + ((2022, 3, 5, 29734), UNITY_2022_3_5F1_LINUX_X86_64), + ((2023, 1, 0, 2298), UNITY_2023_1_0F1_LINUX_X86_64), + ((2023, 1, 4, 6702), UNITY_2023_1_4F1_LINUX_X86_64), + ((2023, 2, 0, 54845), UNITY_2023_2_0F1_LINUX_X86_64), + ((6000, 3, 0, 34572), UNITY_6000_3_0F1_LINUX_X86_64), + ((6000, 5, 0, 46204), UNITY_6000_5_0F1_LINUX_X86_64), + ((6000, 6, 0, 63725), UNITY_6000_6_0F1_LINUX_X86_64), + ]; + for (unity, profile) in profiles { + let build = linux_builds::nearest(unity).unwrap(); + assert_eq!(build.unity, unity, "{unity:?}"); + assert!(same(&build.profile, &profile), "{unity:?}"); + assert_eq!(profile.pointer_size, PointerSize::Bit64); + } +} diff --git a/src/game_engine/unity/scene_manager/roots_tests.rs b/src/game_engine/unity/scene_manager/roots_tests.rs index b0f0eac0..d48f5a49 100644 --- a/src/game_engine/unity/scene_manager/roots_tests.rs +++ b/src/game_engine/unity/scene_manager/roots_tests.rs @@ -23,7 +23,7 @@ fn manager() -> SceneManager { SceneManager { pointer_size: PointerSize::Bit64, address: Address::new(BASE), - profile: &builds::nearest((6000, 3, 21, 9777), PointerSize::Bit64) + profile: builds::nearest((6000, 3, 21, 9777), PointerSize::Bit64) .unwrap() .profile, } @@ -145,7 +145,7 @@ fn manager_of(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> SceneMa SceneManager { pointer_size, address: Address::new(BASE), - profile: &builds::nearest(unity, pointer_size).unwrap().profile, + profile: builds::nearest(unity, pointer_size).unwrap().profile, } } diff --git a/src/game_engine/unity/scene_manager/scene.rs b/src/game_engine/unity/scene_manager/scene.rs index 0cca8217..b2ed9f02 100644 --- a/src/game_engine/unity/scene_manager/scene.rs +++ b/src/game_engine/unity/scene_manager/scene.rs @@ -88,12 +88,10 @@ impl Scene { /// Returns the length of the path kept inline in the field, or [`None`] /// when the field holds no inline path. fn inline_len(field: &[u8; 32], shape: PathShape) -> Option { - let len = if shape == PathShape::InlineNul { - field.iter().position(|&b| b == 0)? - } else if shape == PathShape::InlineSpare { - 31_usize.checked_sub(field[31] as usize)? - } else { - return None; + let len = match shape { + PathShape::InlineNul => field.iter().position(|&b| b == 0)?, + PathShape::InlineSpare => 31_usize.checked_sub(field[31] as usize)?, + PathShape::Pointer => return None, }; let terminated = len == 31 || field[len] == 0; (len <= 31 && terminated && !field[..len].contains(&0)).then_some(len) diff --git a/src/game_engine/unity/scene_manager/scene_tests.rs b/src/game_engine/unity/scene_manager/scene_tests.rs index 02cec757..cff82687 100644 --- a/src/game_engine/unity/scene_manager/scene_tests.rs +++ b/src/game_engine/unity/scene_manager/scene_tests.rs @@ -22,7 +22,7 @@ fn manager(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> SceneManag SceneManager { pointer_size, address: Address::new(BASE), - profile: &builds::nearest(unity, pointer_size).unwrap().profile, + profile: builds::nearest(unity, pointer_size).unwrap().profile, } } diff --git a/src/game_engine/unity/scene_manager/transform.rs b/src/game_engine/unity/scene_manager/transform.rs index 58b69400..578040b4 100644 --- a/src/game_engine/unity/scene_manager/transform.rs +++ b/src/game_engine/unity/scene_manager/transform.rs @@ -98,12 +98,9 @@ impl Transform { }; Ok((1..number_of_components).filter_map(move |m| { let at = components[m] + reference; - if scene_manager.profile.reference == ReferenceShape::CachedObject { - read(at) - } else if scene_manager.profile.reference == ReferenceShape::RootSlot { - read(read(at)?) - } else { - None + match scene_manager.profile.reference { + ReferenceShape::CachedObject => read(at), + ReferenceShape::RootSlot => read(read(at)?), } })) }