diff --git a/src/game_engine/unity/il2cpp/builds.rs b/src/game_engine/unity/il2cpp/builds.rs index 028cf043..76b32340 100644 --- a/src/game_engine/unity/il2cpp/builds.rs +++ b/src/game_engine/unity/il2cpp/builds.rs @@ -1,64 +1,59 @@ -//! Known IL2CPP builds. Each entry is one measured player. The version of -//! its `global-metadata.dat` and the full Unity version say which player. -//! The offsets come from that player's `GameAssembly.pdb`. +//! Known IL2CPP builds. Each entry is one measured player. The full Unity +//! version says which player. The offsets come from that player's +//! `GameAssembly.pdb`. use super::offsets::{ - AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, IL2CPPOffsets, ImageOffsets, - TypeOffsets, + AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, ImageOffsets, Profile, + TypeOffsets, TypeStart, }; -use super::Version; use crate::PointerSize; /// One measured IL2CPP player and the offsets from its PDB. +#[derive(Copy, Clone)] pub(super) struct Build { - pub(super) metadata: u32, pub(super) unity: (u16, u16, u16, u16), - pub(super) pointer_size: PointerSize, - pub(super) version: Version, - pub(super) offsets: IL2CPPOffsets, + pub(super) profile: Profile, } -/// Finds the build measured on this exact metadata version, Unity version -/// and width. `GameAssembly.dll` is compiled per game, so the Unity version -/// stands in for the binary. The Unity version is the four parts of -/// `UnityPlayer.dll`'s file version, and the last part tells `f1` from `f2` -/// and an alpha from its release. A player nobody measured gets no answer. -pub(super) fn find( - metadata: u32, +/// Finds the build for a player at its width. The Unity version is the four +/// parts of `UnityPlayer.dll`'s file version, and the last part tells `f1` +/// from `f2` and an alpha from its release. A measured player gets its own +/// build. Any other player gets the newest build whose major.minor is at or +/// below its own, since patch releases of one major.minor share their +/// layouts, or the oldest build when nothing is below. The table reads +/// oldest to newest, so the newest match is the last one. +pub(super) fn nearest( unity: (u16, u16, u16, u16), pointer_size: PointerSize, ) -> Option<&'static Build> { - BUILDS.iter().find(|build| { - build.metadata == metadata && build.unity == unity && build.pointer_size == pointer_size - }) -} + let at_width = || { + BUILDS + .iter() + .filter(move |build| build.profile.pointer_size == pointer_size) + }; -/// Whether some build was measured on this Unity version at this width, -/// whatever its metadata says. -pub(super) fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> bool { - BUILDS - .iter() - .any(|build| build.unity == unity && build.pointer_size == pointer_size) + at_width() + .find(|build| build.unity == unity) + .or_else(|| at_width().rfind(|build| (build.unity.0, build.unity.1) <= (unity.0, unity.1))) + .or_else(|| at_width().next()) } // The table reads from the oldest player to the newest. -static BUILDS: &[Build] = &[ +pub(super) const BUILDS: &[Build] = &[ // Unity 2018.4.36f1, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2018, 4, 36, 54151), - pointer_size: PointerSize::Bit64, - version: Version::Base, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -73,7 +68,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -81,26 +76,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2018.4.36f1, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2018, 4, 36, 54151), - pointer_size: PointerSize::Bit32, - version: Version::Base, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -115,7 +108,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -123,26 +116,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2019.4.41f2, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2019, 4, 41, 9172), - pointer_size: PointerSize::Bit64, - version: Version::V2019, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -157,7 +148,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -165,26 +156,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2019.4.41f2, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2019, 4, 41, 9172), - pointer_size: PointerSize::Bit32, - version: Version::V2019, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -199,7 +188,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -207,26 +196,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2020.1.18f1, metadata version 24, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2020, 1, 18, 38512), - pointer_size: PointerSize::Bit64, - version: Version::V2019, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x1c, - metadata_handle: 0x18, + type_start: TypeStart::Inline(0x18), }, class: ClassOffsets { name: 0x10, @@ -241,7 +228,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -249,26 +236,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2020.1.18f1, metadata version 24, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 24, unity: (2020, 1, 18, 38512), - pointer_size: PointerSize::Bit32, - version: Version::V2019, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0x10, - metadata_handle: 0xc, + type_start: TypeStart::Inline(0xc), }, class: ClassOffsets { name: 0x8, @@ -283,7 +268,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -291,26 +276,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2021.3.11f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2021, 3, 11, 23713), - pointer_size: PointerSize::Bit64, - version: Version::V2020, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -325,7 +308,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -333,26 +316,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2021.3.11f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2021, 3, 11, 23713), - pointer_size: PointerSize::Bit32, - version: Version::V2020, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -367,7 +348,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -375,26 +356,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2022.3.0f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2022, 3, 0, 4507), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -409,7 +388,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -417,26 +396,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2022.3.0f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2022, 3, 0, 4507), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -451,7 +428,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -459,26 +436,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2023.1.0f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 0, 2298), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -493,7 +468,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -501,26 +476,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2023.1.0f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 0, 2298), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -535,7 +508,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -543,26 +516,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 2023.1.22f1, metadata version 29, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 22, 16744), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -577,7 +548,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -585,26 +556,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 2023.1.22f1, metadata version 29, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 29, unity: (2023, 1, 22, 16744), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -619,7 +588,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -627,26 +596,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 6000.2.12f1, metadata version 31, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 31, unity: (6000, 2, 12, 40285), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -661,7 +628,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -669,26 +636,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 6000.2.12f1, metadata version 31, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 31, unity: (6000, 2, 12, 40285), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -703,7 +668,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -711,26 +676,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 6000.3.21f1, metadata version 39, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 39, unity: (6000, 3, 21, 9777), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -745,7 +708,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -753,26 +716,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 6000.3.21f1, metadata version 39, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 39, unity: (6000, 3, 21, 9777), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -787,7 +748,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -795,26 +756,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 6000.5.10f1, metadata version 107, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 107, unity: (6000, 5, 10, 54518), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -829,7 +788,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x18), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -837,26 +796,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 6000.5.10f1, metadata version 107, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 107, unity: (6000, 5, 10, 54518), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -871,7 +828,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0xc), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -879,26 +836,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, // Unity 6000.7.0a3, metadata version 110, x64. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 110, unity: (6000, 7, 0, 5476), - pointer_size: PointerSize::Bit64, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit64, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x8), type_count: 0x18, - metadata_handle: 0x28, + type_start: TypeStart::Handle(0x28), }, class: ClassOffsets { name: 0x10, @@ -913,7 +868,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x10), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0xa), }, @@ -921,26 +876,24 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x8), offset: 0x18, - struct_size: 0x20, + size: 0x20, }, }, }, // Unity 6000.7.0a3, metadata version 110, x86. // Offsets from the player's own GameAssembly.pdb. Build { - metadata: 110, unity: (6000, 7, 0, 5476), - pointer_size: PointerSize::Bit32, - version: Version::V2022, - offsets: IL2CPPOffsets { + profile: Profile { + pointer_size: PointerSize::Bit32, assembly: AssemblyOffsets { image: 0x0, - aname: None, + name: None, }, image: ImageOffsets { assembly_name: Some(0x4), type_count: 0xc, - metadata_handle: 0x18, + type_start: TypeStart::Handle(0x18), }, class: ClassOffsets { name: 0x8, @@ -955,7 +908,7 @@ static BUILDS: &[Build] = &[ generic: GenericOffsets { cached_class: Some(0x8), }, - type_words: TypeOffsets { + type_: TypeOffsets { data: Some(0x0), kind: Some(0x6), }, @@ -963,7 +916,7 @@ static BUILDS: &[Build] = &[ name: 0x0, type_: Some(0x4), offset: 0xc, - struct_size: 0x14, + size: 0x14, }, }, }, @@ -971,108 +924,62 @@ static BUILDS: &[Build] = &[ #[cfg(all(test, not(target_family = "wasm")))] mod tests { - use super::{find, IL2CPPOffsets, BUILDS}; + use super::{nearest, BUILDS}; use crate::PointerSize; #[test] fn table_reads_oldest_to_newest() { - assert!(BUILDS - .windows(2) - .all(|pair| (pair[0].metadata, pair[0].unity) <= (pair[1].metadata, pair[1].unity))); + assert!(BUILDS.windows(2).all(|pair| pair[0].unity <= pair[1].unity)); } - const UNITY_6000_3: (u16, u16, u16, u16) = (6000, 3, 21, 9777); - // Every player was measured at both widths, so each version in the // table has an x64 entry and an x86 entry. #[test] fn every_version_is_measured_at_both_widths() { for build in BUILDS { - let other = match build.pointer_size { + let other = match build.profile.pointer_size { PointerSize::Bit64 => PointerSize::Bit32, _ => PointerSize::Bit64, }; - assert!( - find(build.metadata, build.unity, other).is_some(), + let twin = nearest(build.unity, other).unwrap(); + assert_eq!( + twin.unity, build.unity, "{:?} at {:?} has no twin", - build.unity, - other + build.unity, other ); } } + // A player nobody measured takes the newest build at or below its + // major.minor, or the oldest build when nothing is below. A measured + // player takes its own build even when a newer patch was measured. #[test] - fn finds_exact_builds() { - let build = find(39, UNITY_6000_3, PointerSize::Bit64).unwrap(); - assert_eq!(build.metadata, 39); - assert_eq!(build.unity, UNITY_6000_3); - - let narrow = find(39, UNITY_6000_3, PointerSize::Bit32).unwrap(); - assert_eq!(narrow.metadata, 39); - assert_eq!(narrow.pointer_size, PointerSize::Bit32); - } - - // A build is one measured player. An identity off it in any part, the - // metadata version, any part of the Unity version, or the width, is a - // player nobody measured and gets no answer. - #[test] - fn identities_off_a_measured_build_answer_nothing() { - let (major, minor, patch, build) = UNITY_6000_3; - assert!(find(40, UNITY_6000_3, PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor, patch, build + 1), PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor, patch + 1, build), PointerSize::Bit64).is_none()); - assert!(find(39, (major, minor + 1, patch, build), PointerSize::Bit64).is_none()); - assert!(find(200, (7000, 0, 0, 0), PointerSize::Bit64).is_none()); - assert!(find(16, (5, 6, 7, 0), PointerSize::Bit64).is_none()); + fn nearest_takes_the_newest_build_at_or_below_the_major_minor() { + let x64 = PointerSize::Bit64; + let unity = |player| nearest(player, x64).unwrap().unity; + assert_eq!(unity((2021, 3, 5, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((2021, 3, 45, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((2023, 1, 10, 1)), (2023, 1, 22, 16744)); + assert_eq!(unity((2023, 1, 0, 2298)), (2023, 1, 0, 2298)); + assert_eq!(unity((2022, 1, 0, 1)), (2021, 3, 11, 23713)); + assert_eq!(unity((7000, 0, 0, 0)), (6000, 7, 0, 5476)); + assert_eq!(unity((5, 6, 7, 0)), (2018, 4, 36, 54151)); } - // A version table's value for any of the grown members must match every - // measured build it stands in for, or say nothing. #[test] - fn version_tables_never_contradict_a_measured_build() { - fn agrees(table: Option, measured: Option) -> bool { - table.is_none() || table == measured - } - + fn nearest_is_the_build_itself_on_a_measured_version() { for build in BUILDS { - let Some(table) = IL2CPPOffsets::new(build.version, build.pointer_size) else { - continue; - }; - assert!(agrees( - table.class.declaring_type, - build.offsets.class.declaring_type - )); - assert!(agrees( - table.class.instance_size, - build.offsets.class.instance_size - )); - assert!(agrees( - table.generic.cached_class, - build.offsets.generic.cached_class - )); - assert!(agrees(table.type_words.data, build.offsets.type_words.data)); - assert!(agrees(table.type_words.kind, build.offsets.type_words.kind)); - assert!(agrees(table.field.type_, build.offsets.field.type_)); + let found = nearest(build.unity, build.profile.pointer_size).unwrap(); + assert_eq!(found.unity, build.unity); + assert_eq!(found.profile.pointer_size, build.profile.pointer_size); } } - // The version table for 6000.5 and 6000.7 puts static_fields where 2022.3 - // had it. Both measured players put it lower, and 6000.7 moves field_count - // too. #[test] - fn unity_6000_5_builds_diverge_from_their_version_table_on_statics() { - for (metadata, unity, static_fields) in [ - (107, (6000, 5, 10, 54518), 0xA0), - (110, (6000, 7, 0, 5476), 0x98), - ] { - let build = find(metadata, unity, PointerSize::Bit64).unwrap(); - let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); - assert_eq!(build.offsets.class.static_fields, static_fields); - assert_ne!(build.offsets.class.static_fields, table.class.static_fields); - } - - let build = find(110, (6000, 7, 0, 5476), PointerSize::Bit64).unwrap(); - let table = IL2CPPOffsets::new(build.version, build.pointer_size).unwrap(); - assert_ne!(build.offsets.class.field_count, table.class.field_count); + fn nearest_keeps_the_width() { + let build = nearest((2021, 3, 5, 1), PointerSize::Bit32).unwrap(); + assert_eq!(build.unity, (2021, 3, 11, 23713)); + assert_eq!(build.profile.pointer_size, PointerSize::Bit32); + assert!(nearest((2021, 3, 5, 1), PointerSize::Bit16).is_none()); } } diff --git a/src/game_engine/unity/il2cpp/collections_tests.rs b/src/game_engine/unity/il2cpp/collections_tests.rs index 33dfa132..ee011e65 100644 --- a/src/game_engine/unity/il2cpp/collections_tests.rs +++ b/src/game_engine/unity/il2cpp/collections_tests.rs @@ -4,7 +4,7 @@ //! is the route every corlib dictionary takes. use super::super::managed::DictionaryShape; -use super::{IL2CPPOffsets, Module, Version}; +use super::Module; use crate::runtime::mock::with_process; use crate::{Address, PointerSize, Process}; @@ -12,6 +12,7 @@ use std::vec; use std::vec::Vec; const BASE: u64 = 0x60_0000; +const MEASURED_2019: (u16, u16, u16, u16) = (2019, 4, 41, 9172); fn put(image: &mut [u8], at: u64, bytes: &[u8]) { let at = at as usize; @@ -32,11 +33,12 @@ fn field(image: &mut [u8], at: u64, name: u64, type_: u64, offset: i32) { put(image, at + 0x18, &offset.to_le_bytes()); } -fn image(version: Version) -> Vec { - let field_count_at = match version { - Version::V2019 => 0x11C, - _ => 0x124, - }; +fn image(unity: (u16, u16, u16, u16)) -> Vec { + let field_count_at = super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .profile + .class + .field_count as u64; let mut i = vec![0; 0x3000]; let strings = [ @@ -198,25 +200,26 @@ fn image(version: Version) -> Vec { i } -fn module(version: Version) -> Module { +fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x40), - version, - offsets: IL2CPPOffsets::new(version, PointerSize::Bit64).unwrap(), + profile: super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .profile, pointer_size: PointerSize::Bit64, } } -fn on_fixture(version: Version, test: impl FnOnce(&Process, &Module)) { - with_process(&[(BASE, &image(version))], |process| { - test(process, &module(version)); +fn on_fixture(unity: (u16, u16, u16, u16), test: impl FnOnce(&Process, &Module)) { + with_process(&[(BASE, &image(unity))], |process| { + test(process, &module(unity)); }); } #[test] fn dictionaries_resolve_through_the_cached_class() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let offsets = module .get_dictionary_offsets(process, Address::new(BASE)) .unwrap(); @@ -240,12 +243,9 @@ fn dictionaries_resolve_through_the_cached_class() { }); } -// The 2022.2-and-later fallback table carries no cached class, because the -// measured builds inside that stretch disagree. A fallback attach misses -// cleanly; known builds carry their own value. #[test] fn dictionaries_read_their_live_pairs() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -257,7 +257,7 @@ fn dictionaries_read_their_live_pairs() { #[test] fn dictionaries_derived_from_corlibs_dictionary_resolve() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x8); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -269,7 +269,7 @@ fn dictionaries_derived_from_corlibs_dictionary_resolve() { #[test] fn dictionary_shaped_objects_are_not_dictionaries() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { assert!(module .get_dictionary_offsets(process, Address::new(BASE + 0x10)) .is_none()); @@ -278,7 +278,7 @@ fn dictionary_shaped_objects_are_not_dictionaries() { #[test] fn dictionaries_without_allocated_backing_read_empty() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x18); let offsets = module.get_dictionary_offsets(process, slot).unwrap(); let pairs = module @@ -290,7 +290,7 @@ fn dictionaries_without_allocated_backing_read_empty() { #[test] fn hash_sets_read_their_live_values() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x20); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -302,7 +302,7 @@ fn hash_sets_read_their_live_values() { #[test] fn hash_sets_derived_from_corlibs_hash_set_resolve() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x28); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -314,7 +314,7 @@ fn hash_sets_derived_from_corlibs_hash_set_resolve() { #[test] fn hash_set_shaped_objects_are_not_hash_sets() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { assert!(module .get_hash_set_offsets(process, Address::new(BASE + 0x30)) .is_none()); @@ -323,7 +323,7 @@ fn hash_set_shaped_objects_are_not_hash_sets() { #[test] fn hash_sets_without_allocated_backing_read_empty() { - on_fixture(Version::V2019, |process, module| { + on_fixture(MEASURED_2019, |process, module| { let slot = Address::new(BASE + 0x38); let offsets = module.get_hash_set_offsets(process, slot).unwrap(); let values = module @@ -332,12 +332,3 @@ fn hash_sets_without_allocated_backing_read_empty() { assert!(values.is_empty()); }); } - -#[test] -fn fallback_tables_without_a_cached_class_answer_nothing() { - on_fixture(Version::V2022, |process, module| { - assert!(module - .get_dictionary_offsets(process, Address::new(BASE)) - .is_none()); - }); -} diff --git a/src/game_engine/unity/il2cpp/mod.rs b/src/game_engine/unity/il2cpp/mod.rs index 7112bb93..8ed937e2 100644 --- a/src/game_engine/unity/il2cpp/mod.rs +++ b/src/game_engine/unity/il2cpp/mod.rs @@ -13,14 +13,16 @@ mod image; pub use image::Image; mod class; pub use class::Class; -mod version; -pub use version::Version; mod pointer; pub use pointer::UnityPointer; mod offsets; -use offsets::IL2CPPOffsets; +pub use offsets::{ + AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, ImageOffsets, Profile, + TypeOffsets, TypeStart, +}; #[cfg(all(test, not(target_family = "wasm")))] mod collections_tests; +pub mod profiles; #[cfg(all(test, not(target_family = "wasm")))] mod readers_tests; #[cfg(all(test, not(target_family = "wasm")))] @@ -32,66 +34,47 @@ use super::{managed, DictionaryOffsets, HashSetOffsets, ListOffsets, ManagedStri pub struct Module { assemblies: Address, type_info_definition_table: Address, - version: Version, - offsets: &'static IL2CPPOffsets, + profile: Profile, pointer_size: PointerSize, } impl Module { - /// Tries attaching to a Unity game that is using the IL2CPP backend. If - /// the game's metadata and Unity versions name a measured build, this - /// function uses that build's offsets. Otherwise this function detects - /// the [IL2CPP version](Version). If you know the version in advance or - /// it fails detecting it, use [`attach`](Self::attach) instead. + /// Tries attaching to a Unity game that is using the IL2CPP backend. The + /// game gets the offsets of the measured build nearest to its Unity + /// version, its own build when someone measured that version. pub fn attach_auto_detect(process: &Process) -> Option { let il2cpp_module = Self::find_runtime_module(process)?; let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; - - let unity = Self::unity_version(process); - let metadata = Self::metadata_version(process); - - match (unity, metadata) { - (Some(unity), Some(metadata)) => { - if let Some(build) = builds::find(metadata, unity, pointer_size) { - let module = Self::attach_with( - process, - il2cpp_module, - pointer_size, - build.version, - &build.offsets, - )?; - print_limited::<128>(&format_args!( - "known il2cpp build: metadata {metadata}, unity {}.{}.{}.{}", - unity.0, unity.1, unity.2, unity.3, - )); - return Some(module); - } - print_limited::<128>(&format_args!( - "unknown il2cpp build: metadata {metadata}, unity {}.{}.{}.{}", - unity.0, unity.1, unity.2, unity.3, - )); - } - // The game maps its metadata after GameAssembly.dll. A player - // with a measured build waits for it, since attaching now would - // take the version table's offsets and keep them. - (Some(unity), None) if builds::measured(unity, pointer_size) => return None, - _ => {} - } - - let version = Version::detect(process)?; - Self::attach(process, version) + let unity = Self::unity_version(process)?; + let build = builds::nearest(unity, pointer_size)?; + + let module = Self::attach_with(process, il2cpp_module, build.profile)?; + print_limited::<128>(&format_args!( + "il2cpp: unity {}.{}.{}.{} takes the build measured on {}.{}.{}.{}", + unity.0, + unity.1, + unity.2, + unity.3, + build.unity.0, + build.unity.1, + build.unity.2, + build.unity.3, + )); + Some(module) } /// Tries attaching to a Unity game that is using the IL2CPP backend with - /// the [IL2CPP version](Version) provided. The version needs to be - /// correct for this function to work. If you don't know the version in - /// advance, use [`attach_auto_detect`](Self::attach_auto_detect) instead. - pub fn attach(process: &Process, version: Version) -> Option { + /// the provided measured [`Profile`]. The profile's pointer width must + /// match the target process. Use a built-in [`profiles`] constant for a + /// known player, or construct a custom profile for another measured + /// layout. If the target is not known in advance, use + /// [`attach_auto_detect`](Self::attach_auto_detect) instead. + pub fn attach(process: &Process, profile: Profile) -> Option { let il2cpp_module = Self::find_runtime_module(process)?; let pointer_size = pe::MachineType::read(process, il2cpp_module.0)?.pointer_size()?; - let offsets = IL2CPPOffsets::new(version, pointer_size)?; + (pointer_size == profile.pointer_size).then_some(())?; - Self::attach_with(process, il2cpp_module, pointer_size, version, offsets) + Self::attach_with(process, il2cpp_module, profile) } fn find_runtime_module(process: &Process) -> Option<(Address, u64)> { @@ -113,24 +96,12 @@ impl Module { )) } - /// Reads the version of the game's metadata. The mapped - /// `global-metadata.dat` starts with a sanity value, then the version. - fn metadata_version(process: &Process) -> Option { - process.memory_ranges().find_map(|range| { - let [sanity, version] = process.read::<[u32; 2]>(range.address().ok()?).ok()?; - // Versions are small numbers. Unity 6 renumbered them and reaches - // the low hundreds. - (sanity == 0xFAB1_1BAF && (16..=999).contains(&version)).then_some(version) - }) - } - fn attach_with( process: &Process, il2cpp_module: (Address, u64), - pointer_size: PointerSize, - version: Version, - offsets: &'static IL2CPPOffsets, + profile: Profile, ) -> Option { + let pointer_size = profile.pointer_size; let (assemblies, type_info_definition_table) = match pointer_size { PointerSize::Bit64 => Self::globals_x64(process, il2cpp_module)?, PointerSize::Bit32 => Self::globals_x86(process, il2cpp_module)?, @@ -140,8 +111,7 @@ impl Module { Some(Self { assemblies, type_info_definition_table, - version, - offsets, + profile, pointer_size, }) } @@ -273,38 +243,43 @@ impl Module { } fn walk(&self) -> managed::Walk { + let (metadata_handle, handle_is_inline) = match self.profile.image.type_start { + TypeStart::Inline(at) => (at, true), + TypeStart::Handle(at) => (at, false), + }; + managed::Walk { runtime: managed::Runtime::Il2Cpp(managed::Il2CppRuntime { assemblies: self.assemblies, type_info_definition_table: self.type_info_definition_table, - type_count: self.offsets.image.type_count.into(), - metadata_handle: self.offsets.image.metadata_handle.into(), - handle_is_inline: matches!(self.version, Version::Base | Version::V2019), - field_count: self.offsets.class.field_count, - static_fields: self.offsets.class.static_fields.into(), - cached_class: self.offsets.generic.cached_class, - type_data: self.offsets.type_words.data, - type_kind: self.offsets.type_words.kind, + type_count: self.profile.image.type_count.into(), + metadata_handle: metadata_handle.into(), + handle_is_inline, + field_count: self.profile.class.field_count, + static_fields: self.profile.class.static_fields.into(), + cached_class: self.profile.generic.cached_class, + type_data: self.profile.type_.data, + type_kind: self.profile.type_.kind, }), offsets: managed::WalkOffsets { assembly: managed::AssemblyOffsets { - name_in_image: self.offsets.image.assembly_name.map(u16::from), - name_in_assembly: self.offsets.assembly.aname.map(u16::from), - image: self.offsets.assembly.image.into(), + name_in_image: self.profile.image.assembly_name.map(u16::from), + name_in_assembly: self.profile.assembly.name.map(u16::from), + image: self.profile.assembly.image.into(), }, class: managed::ClassOffsets { - name: self.offsets.class.name.into(), - namespace: self.offsets.class.namespace.into(), - parent: self.offsets.class.parent.into(), - declaring: self.offsets.class.declaring_type, - instance_size: self.offsets.class.instance_size, - fields: self.offsets.class.fields.into(), + name: self.profile.class.name.into(), + namespace: self.profile.class.namespace.into(), + parent: self.profile.class.parent.into(), + declaring: self.profile.class.declaring_type, + instance_size: self.profile.class.instance_size, + fields: self.profile.class.fields.into(), }, field: managed::FieldOffsets { - name: self.offsets.field.name.into(), - type_: self.offsets.field.type_, - offset: self.offsets.field.offset.into(), - stride: self.offsets.field.struct_size.into(), + name: self.profile.field.name.into(), + type_: self.profile.field.type_, + offset: self.profile.field.offset.into(), + stride: self.profile.field.size.into(), }, }, stop: managed::ClimbStop::UNITY, @@ -545,10 +520,8 @@ impl Module { managed::read_reference_list(process, self.pointer_size, offsets, at) } - /// Attaches to a Unity game that is using the IL2CPP backend. This function - /// automatically detects the [IL2CPP version](Version). If you know the - /// version in advance or it fails detecting it, use - /// [`wait_attach`](Self::wait_attach) instead. + /// Attaches to a Unity game that is using the IL2CPP backend. The game + /// gets the offsets of the measured build nearest to its Unity version. /// /// This is the `await`able version of the /// [`attach_auto_detect`](Self::attach_auto_detect) function, yielding back @@ -558,14 +531,13 @@ impl Module { } /// Attaches to a Unity game that is using the IL2CPP backend with the - /// [IL2CPP version](Version) provided. The version needs to be correct - /// for this function to work. If you don't know the version in advance, use - /// [`wait_attach_auto_detect`](Self::wait_attach_auto_detect) instead. + /// provided measured [`Profile`]. The profile's pointer width must match + /// the target process. /// - /// This is the `await`able version of the [`attach`](Self::attach) - /// function, yielding back to the runtime between each try. - pub async fn wait_attach(process: &Process, version: Version) -> Module { - retry(|| Self::attach(process, version)).await + /// 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) -> Module { + retry(|| Self::attach(process, profile)).await } /// Looks for the specified binary [image](Image) inside the target process. @@ -640,24 +612,3 @@ impl Module { retry(|| self.get_hash_set_offsets(process, at)).await } } - -#[cfg(all(test, not(target_family = "wasm")))] -mod tests { - use super::Module; - use crate::runtime::mock::with_process; - - #[test] - fn reads_the_metadata_version_off_the_mapped_file() { - let mapped = [0xAF_u8, 0x1B, 0xB1, 0xFA, 39, 0, 0, 0]; - // The sanity value with nothing sane behind it must not answer. - let stray = [0xAF_u8, 0x1B, 0xB1, 0xFA, 0, 0, 0, 0]; - - with_process(&[(0x10000, &stray), (0x20000, &mapped)], |process| { - assert_eq!(Module::metadata_version(process), Some(39)); - }); - - with_process(&[(0x10000, &stray)], |process| { - assert!(Module::metadata_version(process).is_none()); - }); - } -} diff --git a/src/game_engine/unity/il2cpp/offsets.rs b/src/game_engine/unity/il2cpp/offsets.rs index a1f1df05..88f1e873 100644 --- a/src/game_engine/unity/il2cpp/offsets.rs +++ b/src/game_engine/unity/il2cpp/offsets.rs @@ -1,191 +1,157 @@ -use crate::{game_engine::unity::il2cpp::Version, PointerSize}; +use crate::PointerSize; -pub(super) struct IL2CPPOffsets { - pub(super) assembly: AssemblyOffsets, - pub(super) image: ImageOffsets, - pub(super) class: ClassOffsets, - pub(super) generic: GenericOffsets, - pub(super) type_words: TypeOffsets, - pub(super) field: FieldInfoOffsets, +/// A complete measured IL2CPP runtime layout. +/// +/// Profiles are deliberately exhaustive. If a future ASR release needs more +/// layout information, custom profiles must provide it rather than silently +/// inheriting an offset that may be wrong. Built-in measured profiles are +/// available in [`profiles`](super::profiles). +/// +/// A custom profile lists the complete layout. Every field is written out +/// intentionally: using struct-update syntax would opt the custom profile in +/// to silently inheriting fields added to the source profile in the future. +/// +/// ``` +/// use asr::{ +/// game_engine::unity::il2cpp::{ +/// AssemblyOffsets, ClassOffsets, FieldInfoOffsets, GenericOffsets, +/// ImageOffsets, Profile, TypeOffsets, TypeStart, +/// }, +/// PointerSize, +/// }; +/// +/// const CUSTOM_PROFILE: Profile = Profile { +/// pointer_size: PointerSize::Bit64, +/// assembly: AssemblyOffsets { +/// image: 0x0, +/// name: None, +/// }, +/// image: ImageOffsets { +/// assembly_name: Some(0x8), +/// type_count: 0x18, +/// type_start: TypeStart::Handle(0x28), +/// }, +/// class: ClassOffsets { +/// name: 0x10, +/// namespace: 0x18, +/// declaring_type: Some(0x50), +/// parent: 0x58, +/// fields: 0x80, +/// static_fields: 0xb8, +/// instance_size: Some(0xf8), +/// field_count: 0x124, +/// }, +/// generic: GenericOffsets { +/// cached_class: Some(0x18), +/// }, +/// type_: TypeOffsets { +/// data: Some(0x0), +/// kind: Some(0xa), +/// }, +/// field: FieldInfoOffsets { +/// name: 0x0, +/// type_: Some(0x8), +/// offset: 0x18, +/// size: 0x20, +/// }, +/// }; +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Profile { + /// The pointer width this profile was measured for. + pub pointer_size: PointerSize, + /// Offsets within `Il2CppAssembly`. + pub assembly: AssemblyOffsets, + /// Offsets within `Il2CppImage`. + pub image: ImageOffsets, + /// Offsets within `Il2CppClass`. + pub class: ClassOffsets, + /// Offsets within `Il2CppGenericClass`. + pub generic: GenericOffsets, + /// Offsets within `Il2CppType`. + pub type_: TypeOffsets, + /// Offsets and size of `FieldInfo`. + pub field: FieldInfoOffsets, } -impl IL2CPPOffsets { - pub(super) fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> { - match pointer_size { - PointerSize::Bit64 => Some(match version { - Version::V2022 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x18, - metadata_handle: 0x28, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: Some(0x50), // 2023.1 through 6000.7 - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x124, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: Some(0x0), // 2023.1 through 6000.7 - kind: Some(0xA), // 2023.1 through 6000.7 - }, - field: FieldInfoOffsets { - name: 0x0, - type_: Some(0x8), // 2023.1 through 6000.7 - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::V2020 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x18, - metadata_handle: 0x28, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: None, - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x120, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: None, - kind: None, - }, - field: FieldInfoOffsets { - name: 0x0, - type_: None, - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::V2019 => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x1C, - metadata_handle: 0x18, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: Some(0x50), // 2019.4, 2020.1 - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: Some(0xF4), // 2019.4, 2020.1 - field_count: 0x11C, - }, - generic: GenericOffsets { - cached_class: Some(0x18), // 2019.4, 2020.1 - }, - type_words: TypeOffsets { - data: Some(0x0), // 2019.4, 2020.1 - kind: Some(0xA), // 2019.4, 2020.1 - }, - field: FieldInfoOffsets { - name: 0x0, - type_: Some(0x8), // 2019.4, 2020.1 - offset: 0x18, - struct_size: 0x20, - }, - }, - Version::Base => &Self { - assembly: AssemblyOffsets { - image: 0x0, - aname: Some(0x18), - }, - image: ImageOffsets { - assembly_name: None, - type_count: 0x1C, - metadata_handle: 0x18, - }, - class: ClassOffsets { - name: 0x10, - namespace: 0x18, - declaring_type: None, - parent: 0x58, - fields: 0x80, - static_fields: 0xB8, - instance_size: None, - field_count: 0x114, - }, - generic: GenericOffsets { cached_class: None }, - type_words: TypeOffsets { - data: None, - kind: None, - }, - field: FieldInfoOffsets { - name: 0x0, - type_: None, - offset: 0x18, - struct_size: 0x20, - }, - }, - }), - _ => None, - } - } +/// Offsets within `Il2CppAssembly`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AssemblyOffsets { + /// The image pointer. + pub image: u8, + /// The assembly-name pointer, when the name is stored on the assembly. + /// Otherwise [`ImageOffsets::assembly_name`] locates it. + pub name: Option, } -pub(super) struct AssemblyOffsets { - pub(super) image: u8, - pub(super) aname: Option, // Either this or ImageOffsets::assembly_name locates the name +/// Offsets within `Il2CppImage`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ImageOffsets { + /// The assembly-name pointer, when the name is stored on the image. + /// Otherwise [`AssemblyOffsets::name`] locates it. + pub assembly_name: Option, + /// The number of types in the image. + pub type_count: u8, + /// Where the image stores the index of its first type. + pub type_start: TypeStart, } -pub(super) struct ImageOffsets { - pub(super) assembly_name: Option, // Either this or AssemblyOffsets::aname locates the name - pub(super) type_count: u8, - pub(super) metadata_handle: u8, +/// Where an image keeps the index of its first type in the type table. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TypeStart { + /// The index sits directly in the image at this offset. + Inline(u8), + /// A pointer sits in the image at this offset; the index sits where it + /// points. + Handle(u8), } -pub(super) struct ClassOffsets { - pub(super) name: u8, - pub(super) namespace: u8, - pub(super) declaring_type: Option, // Where a class keeps the one declaring it - pub(super) parent: u8, - pub(super) fields: u8, - pub(super) static_fields: u8, - pub(super) instance_size: Option, // What one instance occupies, boxed header included - pub(super) field_count: u16, +/// Offsets within `Il2CppClass`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ClassOffsets { + /// The class-name pointer. + pub name: u8, + /// The namespace-name pointer. + pub namespace: u8, + /// The declaring class, when measured. + pub declaring_type: Option, + /// The parent-class pointer. + pub parent: u8, + /// The field table pointer. + pub fields: u8, + /// The static-field storage pointer. + pub static_fields: u8, + /// The boxed instance size, when measured. + pub instance_size: Option, + /// The number of fields declared by the class. + pub field_count: u16, } -// Il2CppGenericClass keeps the class an instantiation resolved to. -pub(super) struct GenericOffsets { - pub(super) cached_class: Option, +/// Offsets within `Il2CppGenericClass`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct GenericOffsets { + /// The class cached for a resolved generic instantiation, when measured. + pub cached_class: Option, } -// Il2CppType's own words: the data pointer and the element kind byte. -pub(super) struct TypeOffsets { - pub(super) data: Option, - pub(super) kind: Option, +/// Offsets within `Il2CppType`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TypeOffsets { + /// The type-data pointer, when measured. + pub data: Option, + /// The byte describing the element kind, when measured. + pub kind: Option, } -pub(super) struct FieldInfoOffsets { - pub(super) name: u8, - pub(super) type_: Option, // Where a field keeps its Il2CppType - - pub(super) offset: u8, - pub(super) struct_size: u8, +/// Offsets and size of `FieldInfo`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct FieldInfoOffsets { + /// The field-name pointer. + pub name: u8, + /// The field's `Il2CppType` pointer, when measured. + pub type_: Option, + /// The field offset within its object or static storage. + pub offset: u8, + /// The size of one `FieldInfo` entry. + pub size: u8, } diff --git a/src/game_engine/unity/il2cpp/profiles.rs b/src/game_engine/unity/il2cpp/profiles.rs new file mode 100644 index 00000000..50253966 --- /dev/null +++ b/src/game_engine/unity/il2cpp/profiles.rs @@ -0,0 +1,147 @@ +//! Complete IL2CPP layouts measured from specific Unity players. +//! +//! Each constant describes one architecture of one measured player. Pass a +//! profile directly to [`Module::attach`](super::Module::attach) or +//! [`Module::wait_attach`](super::Module::wait_attach) when the target game is +//! known. This avoids linking the automatic lookup table and its other +//! profiles into the final auto splitter. + +use super::{builds::BUILDS, Profile}; + +macro_rules! profiles { + ($(#[$meta:meta] $name:ident = $index:literal;)+) => { + $( + #[$meta] + pub const $name: Profile = BUILDS[$index].profile; + )+ + }; +} + +profiles! { + /// Unity 2018.4.36f1, file version `2018.4.36.54151`, x86-64. + UNITY_2018_4_36F1_X86_64 = 0; + /// Unity 2018.4.36f1, file version `2018.4.36.54151`, x86. + UNITY_2018_4_36F1_X86 = 1; + /// Unity 2019.4.41f2, file version `2019.4.41.9172`, x86-64. + UNITY_2019_4_41F2_X86_64 = 2; + /// Unity 2019.4.41f2, file version `2019.4.41.9172`, x86. + UNITY_2019_4_41F2_X86 = 3; + /// Unity 2020.1.18f1, file version `2020.1.18.38512`, x86-64. + UNITY_2020_1_18F1_X86_64 = 4; + /// Unity 2020.1.18f1, file version `2020.1.18.38512`, x86. + UNITY_2020_1_18F1_X86 = 5; + /// Unity 2021.3.11f1, file version `2021.3.11.23713`, x86-64. + UNITY_2021_3_11F1_X86_64 = 6; + /// Unity 2021.3.11f1, file version `2021.3.11.23713`, x86. + UNITY_2021_3_11F1_X86 = 7; + /// Unity 2022.3.0f1, file version `2022.3.0.4507`, x86-64. + UNITY_2022_3_0F1_X86_64 = 8; + /// Unity 2022.3.0f1, file version `2022.3.0.4507`, x86. + UNITY_2022_3_0F1_X86 = 9; + /// Unity 2023.1.0f1, file version `2023.1.0.2298`, x86-64. + UNITY_2023_1_0F1_X86_64 = 10; + /// Unity 2023.1.0f1, file version `2023.1.0.2298`, x86. + UNITY_2023_1_0F1_X86 = 11; + /// Unity 2023.1.22f1, file version `2023.1.22.16744`, x86-64. + UNITY_2023_1_22F1_X86_64 = 12; + /// Unity 2023.1.22f1, file version `2023.1.22.16744`, x86. + UNITY_2023_1_22F1_X86 = 13; + /// Unity 6000.2.12f1, file version `6000.2.12.40285`, x86-64. + UNITY_6000_2_12F1_X86_64 = 14; + /// Unity 6000.2.12f1, file version `6000.2.12.40285`, x86. + UNITY_6000_2_12F1_X86 = 15; + /// Unity 6000.3.21f1, file version `6000.3.21.9777`, x86-64. + UNITY_6000_3_21F1_X86_64 = 16; + /// Unity 6000.3.21f1, file version `6000.3.21.9777`, x86. + UNITY_6000_3_21F1_X86 = 17; + /// Unity 6000.5.10f1, file version `6000.5.10.54518`, x86-64. + UNITY_6000_5_10F1_X86_64 = 18; + /// Unity 6000.5.10f1, file version `6000.5.10.54518`, x86. + UNITY_6000_5_10F1_X86 = 19; + /// Unity 6000.7.0a3, file version `6000.7.0.5476`, x86-64. + UNITY_6000_7_0A3_X86_64 = 20; + /// Unity 6000.7.0a3, file version `6000.7.0.5476`, x86. + UNITY_6000_7_0A3_X86 = 21; +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + use crate::PointerSize; + + #[test] + fn constants_name_the_expected_profiles() { + let profiles = [ + ( + (2018, 4, 36, 54151), + UNITY_2018_4_36F1_X86_64, + UNITY_2018_4_36F1_X86, + ), + ( + (2019, 4, 41, 9172), + UNITY_2019_4_41F2_X86_64, + UNITY_2019_4_41F2_X86, + ), + ( + (2020, 1, 18, 38512), + UNITY_2020_1_18F1_X86_64, + UNITY_2020_1_18F1_X86, + ), + ( + (2021, 3, 11, 23713), + UNITY_2021_3_11F1_X86_64, + UNITY_2021_3_11F1_X86, + ), + ( + (2022, 3, 0, 4507), + UNITY_2022_3_0F1_X86_64, + UNITY_2022_3_0F1_X86, + ), + ( + (2023, 1, 0, 2298), + UNITY_2023_1_0F1_X86_64, + UNITY_2023_1_0F1_X86, + ), + ( + (2023, 1, 22, 16744), + UNITY_2023_1_22F1_X86_64, + UNITY_2023_1_22F1_X86, + ), + ( + (6000, 2, 12, 40285), + UNITY_6000_2_12F1_X86_64, + UNITY_6000_2_12F1_X86, + ), + ( + (6000, 3, 21, 9777), + UNITY_6000_3_21F1_X86_64, + UNITY_6000_3_21F1_X86, + ), + ( + (6000, 5, 10, 54518), + UNITY_6000_5_10F1_X86_64, + UNITY_6000_5_10F1_X86, + ), + ( + (6000, 7, 0, 5476), + UNITY_6000_7_0A3_X86_64, + UNITY_6000_7_0A3_X86, + ), + ]; + + for (unity, x86_64, x86) in profiles { + assert_eq!( + super::super::builds::nearest(unity, PointerSize::Bit64) + .unwrap() + .profile, + x86_64, + ); + assert_eq!( + super::super::builds::nearest(unity, PointerSize::Bit32) + .unwrap() + .profile, + x86, + ); + } + } +} diff --git a/src/game_engine/unity/il2cpp/readers_tests.rs b/src/game_engine/unity/il2cpp/readers_tests.rs index b3ea61af..b51d2e3d 100644 --- a/src/game_engine/unity/il2cpp/readers_tests.rs +++ b/src/game_engine/unity/il2cpp/readers_tests.rs @@ -2,7 +2,7 @@ //! implementation is shared, so this pins the mirror surface and the one //! behavior whose rationale is IL2CPP's: the full-width length judgment. -use super::{IL2CPPOffsets, Module, Version}; +use super::Module; use crate::runtime::mock::with_process; use crate::{Address, PointerSize, Process}; @@ -52,8 +52,9 @@ fn on_fixture(test: impl FnOnce(&Process, &Module)) { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, - offsets: IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + profile: super::builds::nearest((2022, 3, 0, 4507), PointerSize::Bit64) + .unwrap() + .profile, pointer_size: PointerSize::Bit64, }; test(process, &module); diff --git a/src/game_engine/unity/il2cpp/version.rs b/src/game_engine/unity/il2cpp/version.rs deleted file mode 100644 index b3a29efb..00000000 --- a/src/game_engine/unity/il2cpp/version.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{file_format::pe, PointerSize, Process}; - -/// The version of IL2CPP that was used for the game. -#[non_exhaustive] -#[derive(Copy, Clone, PartialEq, Hash, Debug)] -pub enum Version { - /// The base version. - Base, - /// The version used starting from Unity 2019.0 - V2019, - /// The version used starting from Unity 2020.2 - V2020, - /// The version used starting from Unity 2022.2 - V2022, -} - -impl Version { - pub(crate) fn detect(process: &Process) -> Option { - let unity_module = process.get_module_address("UnityPlayer.dll").ok()?; - - if pe::MachineType::pointer_size(pe::MachineType::read(process, unity_module)?)? - == PointerSize::Bit32 - { - return Some(Self::Base); - } - - let file_version = pe::FileVersion::read(process, unity_module)?; - - return Some( - if file_version.major_version > 2022 - || (file_version.major_version == 2022 && file_version.minor_version >= 2) - { - Self::V2022 - } else if (file_version.major_version >= 2021 && file_version.major_version < 2023) - || (file_version.major_version == 2020 && file_version.minor_version >= 2) - { - Self::V2020 - } else if (file_version.major_version == 2020 && file_version.minor_version < 2) - || file_version.major_version == 2019 - { - Self::V2019 - } else { - Self::Base - }, - ); - } -} diff --git a/src/game_engine/unity/il2cpp/walk_tests.rs b/src/game_engine/unity/il2cpp/walk_tests.rs index 3f31d9aa..b7b7bfa9 100644 --- a/src/game_engine/unity/il2cpp/walk_tests.rs +++ b/src/game_engine/unity/il2cpp/walk_tests.rs @@ -4,7 +4,7 @@ //! the literal numbers of the Unity 2019.4 and 6000.3 layouts, copied by hand, //! so the walk is checked against the layout rather than against itself. -use super::{IL2CPPOffsets, Module, UnityPointer, Version}; +use super::{Module, Profile, UnityPointer}; use crate::runtime::mock::{poll_once, with_process}; use crate::{Address, PointerSize, Process}; @@ -21,42 +21,37 @@ fn put(image: &mut [u8], at: u64, bytes: &[u8]) { } // A player as `attach_auto_detect` sees it: `GameAssembly.dll` with a PE -// header and the x64 code that points at both globals, `UnityPlayer.dll` with a -// PE header and a version resource, and the mapped metadata file when it is -// there yet. +// header and the x64 code that points at both globals, and `UnityPlayer.dll` +// with a PE header and a version resource. struct Player { unity: (u16, u16, u16, u16), - metadata: Option, } const GAME_ASSEMBLY: u64 = 0x1_8000_0000; const UNITY_PLAYER: u64 = 0x1900_0000; -const METADATA: u64 = 0x2000_0000; impl Player { fn attach(&self) -> Option { + self.with_process(Module::attach_auto_detect) + } + + fn attach_profile(&self, profile: Profile) -> Option { + self.with_process(|process| Module::attach(process, profile)) + } + + fn with_process(&self, f: impl FnOnce(&Process) -> T) -> T { let game_assembly = Self::game_assembly(); let unity_player = Self::unity_player(self.unity); - let metadata = self.metadata.map(|version| { - let mut i = vec![0; 0x100]; - put(&mut i, 0x0, &0xFAB1_1BAF_u32.to_le_bytes()); - put(&mut i, 0x4, &version.to_le_bytes()); - i - }); - let mut regions = vec![ - (GAME_ASSEMBLY, &game_assembly[..]), - (UNITY_PLAYER, &unity_player[..]), - ]; - if let Some(metadata) = &metadata { - regions.push((METADATA, &metadata[..])); - } crate::runtime::mock::with_modules( - ®ions, + &[ + (GAME_ASSEMBLY, &game_assembly[..]), + (UNITY_PLAYER, &unity_player[..]), + ], &[ ("GameAssembly.dll", GAME_ASSEMBLY, 0x1000), ("UnityPlayer.dll", UNITY_PLAYER, 0x1000), ], - Module::attach_auto_detect, + f, ) } @@ -127,15 +122,20 @@ impl Player { } } +const MEASURED_2019: (u16, u16, u16, u16) = (2019, 4, 41, 9172); +const MEASURED_2022: (u16, u16, u16, u16) = (2022, 3, 0, 4507); const MEASURED_6000_5: (u16, u16, u16, u16) = (6000, 5, 10, 54518); -// A game on a measured player, with its metadata mapped, attaches with the -// offsets measured on that player. +fn measured(unity: (u16, u16, u16, u16), pointer_size: PointerSize) -> Profile { + super::builds::nearest(unity, pointer_size).unwrap().profile +} + +// A game on a measured player attaches with the offsets measured on that +// player. #[test] fn attach_auto_detect_uses_a_measured_build() { let module = Player { unity: MEASURED_6000_5, - metadata: Some(107), } .attach() .unwrap(); @@ -144,47 +144,33 @@ fn attach_auto_detect_uses_a_measured_build() { module.type_info_definition_table, Address::new(GAME_ASSEMBLY + 0x910) ); - assert_eq!(module.offsets.class.static_fields, 0xA0); + assert_eq!(module.profile.class.static_fields, 0xA0); } -// The metadata file is mapped after `GameAssembly.dll`. Until it is, a game -// on a measured player has to wait rather than attach with the version -// table's offsets and keep them. #[test] -fn attach_auto_detect_waits_for_the_metadata_of_a_measured_player() { - let module = Player { +fn attach_uses_the_explicit_profile_and_checks_its_width() { + let player = Player { unity: MEASURED_6000_5, - metadata: None, - } - .attach(); - assert!(module.is_none()); -} - -// A player nobody measured takes the version table, whether or not its -// metadata is mapped yet. -#[test] -fn attach_auto_detect_falls_back_for_an_unmeasured_player() { - for metadata in [None, Some(107)] { - let module = Player { - unity: (6000, 5, 11, 1), - metadata, - } - .attach() + }; + let module = player + .attach_profile(super::profiles::UNITY_6000_5_10F1_X86_64) .unwrap(); - assert_eq!(module.offsets.class.static_fields, 0xB8); - } + assert_eq!(module.profile.class.static_fields, 0xa0); + assert!(player + .attach_profile(super::profiles::UNITY_6000_5_10F1_X86) + .is_none()); } -// A measured player whose metadata says another version is not that build. +// A player nobody measured takes the nearest build. #[test] -fn attach_auto_detect_falls_back_when_the_metadata_disagrees() { +fn attach_auto_detect_takes_the_nearest_build_for_an_unmeasured_player() { let module = Player { - unity: MEASURED_6000_5, - metadata: Some(110), + unity: (2021, 3, 5, 1), } .attach() .unwrap(); - assert_eq!(module.offsets.class.static_fields, 0xB8); + let nearest = super::builds::nearest((2021, 3, 11, 23713), PointerSize::Bit64).unwrap(); + assert_eq!(module.profile, nearest.profile); } // The x86 code that points at both globals. The assemblies loop reads the vector's @@ -238,16 +224,13 @@ fn x86_image(store: &'static [u8], operand_at: u64) -> vec::Vec { X86Image::with_store(store, operand_at).lay() } -// The scan reads no offsets, and the version tables carry no 32 bit arm, so -// the x64 offsets stand in. The module is the first 0x1000 bytes of the +// The scan reads no offsets. The module is the first 0x1000 bytes of the // image, so the image can hold bytes past the module's end. fn attach_x86(process: &crate::Process) -> Option { Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit32, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit32), ) } @@ -384,9 +367,7 @@ fn x64_globals_resolve_from_a_mapped_image() { let module = Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit64, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit64), ) .unwrap(); assert_eq!(module.assemblies, Address::new(BASE + 0x900)); @@ -405,22 +386,20 @@ fn x64_scanner_refuses_an_x86_image() { assert!(Module::attach_with( process, (Address::new(BASE), 0x1000), - PointerSize::Bit64, - Version::V2022, - IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + measured(MEASURED_6000_5, PointerSize::Bit64), ) .is_none()); }); } // Every measured build names an assembly through its image, at the image's -// own name field, where the version tables read it off the assembly. +// own name field. #[test] fn assembly_names_resolve_through_the_image() { - let build = super::builds::find(107, (6000, 5, 10, 54518), PointerSize::Bit64).unwrap(); - let offsets = &build.offsets; - assert!(offsets.assembly.aname.is_none()); - let name_at = offsets.image.assembly_name.unwrap() as u64; + let build = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); + let profile = build.profile; + assert!(profile.assembly.name.is_none()); + let name_at = profile.image.assembly_name.unwrap() as u64; let mut i = vec![0; 0x1000]; let ptr = |i: &mut [u8], at: u64, target: u64| { @@ -437,8 +416,7 @@ fn assembly_names_resolve_through_the_image() { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, - offsets, + profile, pointer_size: PointerSize::Bit64, }; let image = module.get_default_image(process).unwrap(); @@ -453,12 +431,12 @@ fn ptr(image: &mut [u8], at: u64, target: u64) { // The target's structures, hand-laid: the assemblies vector, the type info // definition table sliced by the image's handle, a parent chain reaching a // UnityEngine class, a static table, and a live object heading with its class. -fn image(version: Version) -> Vec { - let (type_count_at, handle_at, field_count_at) = match version { - Version::V2019 => (0x1C, 0x18, 0x11C), - Version::V2020 => (0x18, 0x28, 0x120), - _ => (0x18, 0x28, 0x124), - }; +fn image(unity: (u16, u16, u16, u16)) -> Vec { + use super::TypeStart; + + let offsets = measured(unity, PointerSize::Bit64); + let type_count_at = offsets.image.type_count as u64; + let field_count_at = offsets.class.field_count as u64; let mut i = vec![0; 0x5000]; @@ -496,19 +474,26 @@ fn image(version: Version) -> Vec { ptr(&mut i, 0x40, BASE + 0x80); ptr(&mut i, 0x48, BASE + 0xC0); - // Il2CppAssembly: the image at 0x0, the name at 0x18. - ptr(&mut i, 0x80, BASE + 0x140); - ptr(&mut i, 0x80 + 0x18, BASE + 0x2000); - ptr(&mut i, 0xC0, BASE + 0x300); - ptr(&mut i, 0xC0 + 0x18, BASE + 0x2080); + // Each assembly points at its image. Depending on the measured player, + // the assembly name lives either on the assembly or on that image. + let assembly = |i: &mut [u8], at: u64, image: u64, name: u64| { + ptr(i, at + offsets.assembly.image as u64, BASE + image); + if let Some(name_at) = offsets.assembly.name { + ptr(i, at + name_at as u64, BASE + name); + } else if let Some(name_at) = offsets.image.assembly_name { + ptr(i, image + name_at as u64, BASE + name); + } + }; + assembly(&mut i, 0x80, 0x140, 0x2000); + assembly(&mut i, 0xC0, 0x300, 0x2080); // The default image: three classes, reached through the handle. The older // lineage stores the handle inline where the newer one points at it. put(&mut i, 0x300 + type_count_at, &5_u32.to_le_bytes()); - match version { - Version::V2019 => put(&mut i, 0x300 + handle_at, &5_u32.to_le_bytes()), - _ => { - ptr(&mut i, 0x300 + handle_at, BASE + 0x400); + match offsets.image.type_start { + TypeStart::Inline(at) => put(&mut i, 0x300 + at as u64, &5_u32.to_le_bytes()), + TypeStart::Handle(at) => { + ptr(&mut i, 0x300 + at as u64, BASE + 0x400); put(&mut i, 0x400, &5_u32.to_le_bytes()); } } @@ -659,26 +644,25 @@ fn image(version: Version) -> Vec { i } -fn module(version: Version) -> Module { +fn module(unity: (u16, u16, u16, u16)) -> Module { Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version, - offsets: IL2CPPOffsets::new(version, PointerSize::Bit64).unwrap(), + profile: measured(unity, PointerSize::Bit64), pointer_size: PointerSize::Bit64, } } -fn on_fixture(version: Version, test: impl FnOnce(&Process, &Module)) { - with_process(&[(BASE, &image(version))], |process| { - test(process, &module(version)); +fn on_fixture(unity: (u16, u16, u16, u16), test: impl FnOnce(&Process, &Module)) { + with_process(&[(BASE, &image(unity))], |process| { + test(process, &module(unity)); }); } #[test] fn images_resolve_by_name_in_both_lineages() { - for version in [Version::V2019, Version::V2022] { - on_fixture(version, |process, module| { + for unity in [MEASURED_2019, MEASURED_2022] { + on_fixture(unity, |process, module| { assert!(module.get_default_image(process).is_some()); assert!(module.get_image(process, "mscorlib").is_some()); assert!(module.get_image(process, "Assembly-DoesNotExist").is_none()); @@ -688,8 +672,8 @@ fn images_resolve_by_name_in_both_lineages() { #[test] fn classes_resolve_by_name_and_namespace() { - for version in [Version::V2019, Version::V2022] { - on_fixture(version, |process, module| { + for unity in [MEASURED_2019, MEASURED_2022] { + on_fixture(unity, |process, module| { let image = module.get_default_image(process).unwrap(); assert!(image.get_class(process, module, "GameManager").is_some()); assert!(image.get_class(process, module, "Game.Boss").is_some()); @@ -702,7 +686,7 @@ fn classes_resolve_by_name_and_namespace() { #[test] fn field_offsets_resolve_declared_and_inherited() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert_eq!( @@ -718,7 +702,7 @@ fn field_offsets_resolve_declared_and_inherited() { #[test] fn nested_classes_resolve_by_their_written_name() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); assert!(image .get_class(process, module, "Game.Outer+Inner") @@ -735,25 +719,11 @@ fn nested_classes_resolve_by_their_written_name() { }); } -// V2020's table never measured where a class keeps its declaring type, so a -// nested lookup on it must miss cleanly rather than answer with whichever -// class carries the leaf name. -#[test] -fn nested_lookups_without_a_measured_offset_answer_nothing() { - on_fixture(Version::V2020, |process, module| { - let image = module.get_default_image(process).unwrap(); - assert!(image - .get_class(process, module, "Game.Outer+Inner") - .is_none()); - assert!(image.get_class(process, module, "GameManager").is_some()); - }); -} - // The climb stops at UnityEngine's namespace, so an engine field never // resolves. #[test] fn field_climbs_stop_at_the_engine() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert!(game_manager @@ -764,7 +734,7 @@ fn field_climbs_stop_at_the_engine() { #[test] fn statics_resolve_from_the_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let game_manager = image.get_class(process, module, "GameManager").unwrap(); assert_eq!( @@ -778,7 +748,7 @@ fn statics_resolve_from_the_class() { // table, not the table of the class the lookup started at. #[test] fn static_instances_resolve_through_the_declaring_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let boss = image.get_class(process, module, "Boss").unwrap(); assert_eq!( @@ -798,7 +768,7 @@ fn static_instances_resolve_through_the_declaring_class() { // and the read returns the live count's elements, never the backing capacity's. #[test] fn lists_resolve_through_their_own_class() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x18); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module.read_list::(process, offsets, at).unwrap(); @@ -808,7 +778,7 @@ fn lists_resolve_through_their_own_class() { #[test] fn lists_derived_from_corlibs_list_resolve() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x20); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module.read_list::(process, offsets, at).unwrap(); @@ -818,7 +788,7 @@ fn lists_derived_from_corlibs_list_resolve() { #[test] fn list_shaped_objects_are_not_lists() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { assert!(module .get_list_offsets(process, Address::new(BASE + 0x28)) .is_none()); @@ -829,7 +799,7 @@ fn list_shaped_objects_are_not_lists() { // element preserved at its position. #[test] fn reference_lists_resolve_their_element_addresses() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let at = Address::new(BASE + 0x30); let offsets = module.get_list_offsets(process, at).unwrap(); let read = module @@ -846,7 +816,7 @@ fn reference_lists_resolve_their_element_addresses() { // resolved against the object's own class read off its head. #[test] fn pointers_dereference_through_a_static_root() { - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let pointer = UnityPointer::<2>::new("GameManager", 0, &["instance", "points"]); assert_eq!(pointer.deref::(process, module, &image).unwrap(), 888); @@ -866,7 +836,7 @@ fn public_types_keep_their_properties() { is_copy::(); is_copy::(); - on_fixture(Version::V2022, |process, module| { + on_fixture(MEASURED_2022, |process, module| { let image = module.get_default_image(process).unwrap(); let _ = double_ended(image.classes(process, module)); }); @@ -875,6 +845,8 @@ fn public_types_keep_their_properties() { // A 32 bit target lays the assemblies vector and its pointers at four bytes. #[test] fn images_resolve_on_32_bit_targets() { + let profile = measured(MEASURED_6000_5, PointerSize::Bit32); + let name_at = profile.image.assembly_name.unwrap() as u64; let mut i = vec![0; 0x1000]; let narrow = |i: &mut [u8], at: u64, target: u64| { put(i, at, &(target as u32).to_le_bytes()); @@ -885,16 +857,76 @@ fn images_resolve_on_32_bit_targets() { narrow(&mut i, 0x4, BASE + 0x44); // and end, one assembly along narrow(&mut i, 0x40, BASE + 0x80); narrow(&mut i, 0x80, BASE + 0x100); // Il2CppAssembly.image - narrow(&mut i, 0x80 + 0x18, BASE + 0x800); // Il2CppAssembly.aname + narrow(&mut i, 0x100 + name_at, BASE + 0x800); // Il2CppImage.nameNoExt with_process(&[(BASE, &i)], |process| { let module = Module { assemblies: Address::new(BASE), type_info_definition_table: Address::new(BASE + 0x10), - version: Version::V2022, - offsets: IL2CPPOffsets::new(Version::V2022, PointerSize::Bit64).unwrap(), + profile, pointer_size: PointerSize::Bit32, }; assert!(module.get_default_image(process).is_some()); }); } + +// An image says where its first type sits in the type table. Players up to +// 2020.1 keep that index in the image. Later players keep a handle there, +// and the index sits behind it. +#[test] +fn class_walk_reads_the_type_start_where_the_build_keeps_it() { + use super::TypeStart; + + let walk = |build: &'static super::builds::Build| { + let profile = build.profile; + let mut i = vec![0; 0x1000]; + let ptr = |i: &mut [u8], at: u64, target: u64| { + put(i, at, &target.to_le_bytes()); + }; + put(&mut i, 0x800, b"Timer"); + ptr(&mut i, 0x10, BASE + 0x200); // the type table + ptr(&mut i, 0x210, BASE + 0x300); // its entry 2 + ptr(&mut i, 0x300 + profile.class.name as u64, BASE + 0x800); + put( + &mut i, + 0x100 + profile.image.type_count as u64, + &1_u32.to_le_bytes(), + ); + match profile.image.type_start { + TypeStart::Inline(at) => put(&mut i, 0x100 + at as u64, &2_u32.to_le_bytes()), + TypeStart::Handle(at) => { + ptr(&mut i, 0x100 + at as u64, BASE + 0x180); + put(&mut i, 0x180, &2_u32.to_le_bytes()); + } + } + + with_process(&[(BASE, &i)], |process| { + let module = Module { + assemblies: Address::new(BASE), + type_info_definition_table: Address::new(BASE + 0x10), + profile, + pointer_size: PointerSize::Bit64, + }; + let image = super::Image { + image: Address::new(BASE + 0x100), + }; + image + .get_class(process, &module, "Timer") + .map(|class| class.class) + }) + }; + + let inline = super::builds::nearest((2018, 4, 36, 54151), PointerSize::Bit64).unwrap(); + assert!(matches!( + inline.profile.image.type_start, + TypeStart::Inline(0x18) + )); + assert_eq!(walk(inline), Some(Address::new(BASE + 0x300))); + + let handle = super::builds::nearest(MEASURED_6000_5, PointerSize::Bit64).unwrap(); + assert!(matches!( + handle.profile.image.type_start, + TypeStart::Handle(0x28) + )); + assert_eq!(walk(handle), Some(Address::new(BASE + 0x300))); +} diff --git a/src/game_engine/unity/mod.rs b/src/game_engine/unity/mod.rs index 820e763f..b452c3f9 100644 --- a/src/game_engine/unity/mod.rs +++ b/src/game_engine/unity/mod.rs @@ -6,12 +6,12 @@ //! # async fn example(process: asr::Process) { //! use asr::{ //! future::retry, -//! game_engine::unity::il2cpp::{Module, Version}, +//! game_engine::unity::il2cpp::Module, //! Address, Address64, //! }; //! -//! // We first attach to the Mono module. Here we know that the game is using IL2CPP 2020. -//! let module = Module::wait_attach(&process, Version::V2020).await; +//! // We first attach to the IL2CPP module. +//! let module = Module::wait_attach_auto_detect(&process).await; //! // We access the .NET DLL that the game code is in. //! let image = module.wait_get_default_image(&process).await; //!