From db99a4d26193bf74b4326859ee5999f56d224e2b Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Fri, 5 Jul 2024 17:42:21 +0200 Subject: [PATCH] Allow querying the scripts of nodes in Godot This allows you to get access to the underlying values stored in the scripts, which likely contain a lot of important information for the auto splitter. --- .github/workflows/build.yml | 42 ++-- .github/workflows/docs.yml | 8 +- src/game_engine/godot/core/mod.rs | 2 + src/game_engine/godot/core/object/mod.rs | 4 + src/game_engine/godot/core/object/object.rs | 69 +++++- .../godot/core/object/script_instance.rs | 12 ++ .../godot/core/object/script_language.rs | 12 ++ .../godot/core/string/string_name.rs | 63 +++++- src/game_engine/godot/core/string/ustring.rs | 6 + .../godot/core/templates/cowdata.rs | 30 +++ .../godot/core/templates/hash_map.rs | 197 ++++++++++++++++-- .../godot/core/templates/hash_set.rs | 15 ++ .../godot/core/templates/hashfuncs.rs | 105 ++++++++++ src/game_engine/godot/core/templates/list.rs | 14 ++ src/game_engine/godot/core/templates/mod.rs | 10 + .../godot/core/templates/vector.rs | 43 ++++ src/game_engine/godot/core/variant/mod.rs | 3 + src/game_engine/godot/core/variant/variant.rs | 164 +++++++++++++++ src/game_engine/godot/cpp/mod.rs | 6 + src/game_engine/godot/cpp/ptr.rs | 8 +- src/game_engine/godot/cpp/type_info.rs | 2 +- src/game_engine/godot/cpp/vtable.rs | 2 +- src/game_engine/godot/mod.rs | 36 +++- .../godot/modules/gdscript/gdscript.rs | 115 ++++++++++ src/game_engine/godot/modules/gdscript/mod.rs | 3 + src/game_engine/godot/modules/mod.rs | 5 + .../godot/modules/mono/csharp_script.rs | 131 ++++++++++++ src/game_engine/godot/modules/mono/mod.rs | 3 + .../godot/scene/main/canvas_item.rs | 4 +- src/game_engine/godot/scene/main/node.rs | 27 ++- .../godot/scene/main/scene_tree.rs | 57 ++++- src/game_engine/godot/scene/mod.rs | 2 + .../scene/three_d/collision_object_3d.rs | 11 + src/game_engine/godot/scene/three_d/mod.rs | 7 + .../godot/scene/three_d/node_3d.rs | 38 ++++ .../godot/scene/three_d/physics_body_3d.rs | 39 ++++ .../godot/scene/two_d/collision_object_2d.rs | 11 + src/game_engine/godot/scene/two_d/mod.rs | 4 + src/game_engine/godot/scene/two_d/node_2d.rs | 6 +- .../godot/scene/two_d/physics_body_2d.rs | 39 ++++ 40 files changed, 1282 insertions(+), 73 deletions(-) create mode 100644 src/game_engine/godot/core/object/script_instance.rs create mode 100644 src/game_engine/godot/core/object/script_language.rs create mode 100644 src/game_engine/godot/core/templates/cowdata.rs create mode 100644 src/game_engine/godot/core/templates/hash_set.rs create mode 100644 src/game_engine/godot/core/templates/hashfuncs.rs create mode 100644 src/game_engine/godot/core/templates/list.rs create mode 100644 src/game_engine/godot/core/templates/vector.rs create mode 100644 src/game_engine/godot/core/variant/mod.rs create mode 100644 src/game_engine/godot/core/variant/variant.rs create mode 100644 src/game_engine/godot/modules/gdscript/gdscript.rs create mode 100644 src/game_engine/godot/modules/gdscript/mod.rs create mode 100644 src/game_engine/godot/modules/mod.rs create mode 100644 src/game_engine/godot/modules/mono/csharp_script.rs create mode 100644 src/game_engine/godot/modules/mono/mod.rs create mode 100644 src/game_engine/godot/scene/three_d/collision_object_3d.rs create mode 100644 src/game_engine/godot/scene/three_d/mod.rs create mode 100644 src/game_engine/godot/scene/three_d/node_3d.rs create mode 100644 src/game_engine/godot/scene/three_d/physics_body_3d.rs create mode 100644 src/game_engine/godot/scene/two_d/collision_object_2d.rs create mode 100644 src/game_engine/godot/scene/two_d/physics_body_2d.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b9d21f..30b351d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,24 +10,12 @@ on: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - label: - - WebAssembly Unknown - - WebAssembly WASI - - include: - - label: WebAssembly Unknown - target: wasm32-unknown-unknown - os: ubuntu-latest - install_target: true - - label: WebAssembly WASI - target: wasm32-wasi - os: ubuntu-latest - install_target: true - + target: [wasm32-unknown-unknown, wasm32-wasip1] + toolchain: [stable, nightly] steps: - name: Checkout Commit uses: actions/checkout@v4 @@ -35,11 +23,8 @@ jobs: - name: Install Rust uses: hecrj/setup-rust-action@v2 with: - rust-version: ${{ matrix.toolchain || 'stable' }} - - - name: Install Target - if: matrix.install_target != '' - run: rustup target add ${{ matrix.target }} + targets: ${{ matrix.target }} + rust-version: ${{ matrix.toolchain }} - name: Build (No Default Features) run: | @@ -53,14 +38,19 @@ jobs: run: | cargo build --all-features --target ${{ matrix.target }} - - name: Test (Target, All Features) - run: | - cargo test --all-features + test: + name: Test (Host) + runs-on: ubuntu-latest + steps: + - name: Checkout Commit + uses: actions/checkout@v4 - # Test on the host to also run the doc tests - - name: Test (Host, All Features) + - name: Install Rust + uses: hecrj/setup-rust-action@v2 + + - name: Test (All Features) run: | - cargo test --target x86_64-unknown-linux-gnu --all-features + cargo test --all-features clippy: name: Check clippy lints diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 833ad38..4c07ff6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,10 +37,10 @@ jobs: with: components: rust-docs rust-version: nightly - targets: wasm32-wasi + targets: wasm32-wasip1 - name: Build docs - run: RUSTDOCFLAGS="--cfg doc_cfg" cargo doc --all-features --target wasm32-wasi + run: RUSTDOCFLAGS="--cfg doc_cfg" cargo doc --all-features --target wasm32-wasip1 - name: Setup Pages uses: actions/configure-pages@v3 @@ -61,12 +61,12 @@ jobs: --exclude=.github \ . env: - INPUT_PATH: target/wasm32-wasi/doc + INPUT_PATH: target/wasm32-wasip1/doc - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: - path: target/wasm32-wasi/doc + path: target/wasm32-wasip1/doc - name: Deploy to GitHub Pages id: deployment diff --git a/src/game_engine/godot/core/mod.rs b/src/game_engine/godot/core/mod.rs index 3755250..35bc8e9 100644 --- a/src/game_engine/godot/core/mod.rs +++ b/src/game_engine/godot/core/mod.rs @@ -2,8 +2,10 @@ mod object; mod os; mod string; mod templates; +mod variant; pub use object::*; pub use os::*; pub use string::*; pub use templates::*; +pub use variant::*; diff --git a/src/game_engine/godot/core/object/mod.rs b/src/game_engine/godot/core/object/mod.rs index 75ce6bc..e6c26ba 100644 --- a/src/game_engine/godot/core/object/mod.rs +++ b/src/game_engine/godot/core/object/mod.rs @@ -1,3 +1,7 @@ mod object; +mod script_instance; +mod script_language; pub use object::*; +pub use script_instance::*; +pub use script_language::*; diff --git a/src/game_engine/godot/core/object/object.rs b/src/game_engine/godot/core/object/object.rs index 44aba8b..bcece9a 100644 --- a/src/game_engine/godot/core/object/object.rs +++ b/src/game_engine/godot/core/object/object.rs @@ -1,10 +1,62 @@ //! use crate::{ - game_engine::godot::{Ptr, VTable}, + game_engine::godot::{Ptr, VTable, VariantType}, Error, Process, }; +use super::ScriptInstance; + +#[allow(unused)] +mod offsets { + // *const VTable + pub const VTABLE_PTR: u64 = 0x0; + // *const ObjectGDExtension + pub const EXTENSION: u64 = 0x8; + // GDExtensionClassInstancePtr + pub const EXTENSION_INSTANCE: u64 = 0x10; + // HashMap + pub const SIGNAL_MAP: u64 = 0x18; + // List + pub const CONNECTIONS: u64 = 0x48; + // bool + pub const BLOCK_SIGNALS: u64 = 0x50; + // i32 + pub const PREDELETE_OK: u64 = 0x54; + // ObjectID + pub const INSTANCE_ID: u64 = 0x58; + // bool + pub const CAN_TRANSLATE: u64 = 0x60; + // bool + pub const EMITTING: u64 = 0x61; + // *const ScriptInstance + pub const SCRIPT_INSTANCE: u64 = 0x68; + // Variant + pub const SCRIPT: u64 = 0x70; + // HashMap + pub const METADATA: u64 = 0x88; + // HashMap + pub const METADATA_PROPERTIES: u64 = 0xb8; + // *const StringName + pub const CLASS_NAME_PTR: u64 = 0xe8; +} + +/// Information about a property of a script. This is not publicly exposed in +/// Godot. +/// +/// Check the [`Ptr`] documentation to see all the methods you can +/// call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct PropertyInfo; + +impl Ptr { + /// Returns the type of the property as a [`VariantType`]. + pub fn get_variant_type(self, process: &Process) -> Result { + self.read_at_byte_offset(0x0, process) + } +} + /// Base class for all other classes in the engine. /// /// [`Object`](https://docs.godotengine.org/en/4.2/classes/class_object.html) @@ -18,6 +70,19 @@ pub struct Object; impl Ptr { /// Returns a pointer to the object's virtual method table. pub fn get_vtable(self, process: &Process) -> Result, Error> { - process.read(self.addr()) + self.read_at_byte_offset(offsets::VTABLE_PTR, process) + } + + /// Returns the object's Script instance, or [`None`] if no script is + /// attached. + /// + /// [`Object.get_script`](https://docs.godotengine.org/en/4.2/classes/class_object.html#class-object-method-get-script) + pub fn get_script_instance( + self, + process: &Process, + ) -> Result>, Error> { + let ptr: Ptr = + self.read_at_byte_offset(offsets::SCRIPT_INSTANCE, process)?; + Ok(if ptr.is_null() { None } else { Some(ptr) }) } } diff --git a/src/game_engine/godot/core/object/script_instance.rs b/src/game_engine/godot/core/object/script_instance.rs new file mode 100644 index 0000000..6dce7a6 --- /dev/null +++ b/src/game_engine/godot/core/object/script_instance.rs @@ -0,0 +1,12 @@ +//! + +/// An instance of a [`Script`](super::Script). +/// +/// You need to cast this to a +/// [`GDScriptInstance`](crate::game_engine::godot::GDScriptInstance) or +/// [`CSharpScriptInstance`](crate::game_engine::godot::CSharpScriptInstance) to +/// do anything meaningful with it. Make sure to verify the script language +/// before casting. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct ScriptInstance; diff --git a/src/game_engine/godot/core/object/script_language.rs b/src/game_engine/godot/core/object/script_language.rs new file mode 100644 index 0000000..e523238 --- /dev/null +++ b/src/game_engine/godot/core/object/script_language.rs @@ -0,0 +1,12 @@ +//! + +/// A class stored as a resource. +/// +/// [`Script`](https://docs.godotengine.org/en/4.2/classes/class_script.html) +/// +/// You need to cast this to a [`GDScript`](crate::game_engine::godot::GDScript) +/// or [`CSharpScript`](crate::game_engine::godot::CSharpScript) to do anything +/// meaningful with it. Make sure to verify the script language before casting. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Script; diff --git a/src/game_engine/godot/core/string/string_name.rs b/src/game_engine/godot/core/string/string_name.rs index ad89053..f49ef98 100644 --- a/src/game_engine/godot/core/string/string_name.rs +++ b/src/game_engine/godot/core/string/string_name.rs @@ -6,29 +6,82 @@ use arrayvec::ArrayVec; use bytemuck::{Pod, Zeroable}; use crate::{ - game_engine::godot::{KnownSize, Ptr}, + game_engine::godot::{Hash, Ptr, SizeInTargetProcess}, Address64, Error, Process, }; use super::String; +#[allow(unused)] +mod offsets { + pub mod data { + use super::super::{SizeInTargetProcess, String}; + + pub const REFCOUNT: u64 = 0x00; + pub const STATIC_COUNT: u64 = 0x04; + pub const CNAME: u64 = 0x08; + pub const NAME: u64 = 0x10; + pub const IDX: u64 = NAME + String::<0>::SIZE; + pub const HASH: u64 = IDX + 0x4; + } +} + /// A built-in type for unique strings. /// /// [`StringName`](https://docs.godotengine.org/en/4.2/classes/class_stringname.html) #[derive(Debug, Copy, Clone, Pod, Zeroable)] #[repr(transparent)] -pub struct StringName(Ptr); +pub struct StringName { + data: Ptr, +} + +impl Hash<[u8; N]> for StringName { + fn hash_of_lookup_key(lookup_key: &[u8; N]) -> u32 { + // String::hash + let mut hashv: u32 = 5381; + + for c in lossy_chars(lookup_key) { + hashv = hashv.wrapping_mul(33).wrapping_add(c as u32); + } + + hashv + } + + fn eq(&self, lookup_key: &[u8; N], process: &Process) -> bool { + let Ok(name) = self.read::(process) else { + return false; + }; + name.chars().eq(lossy_chars(lookup_key)) + } +} -impl KnownSize for StringName {} +fn lossy_chars(lookup_key: &[u8]) -> impl Iterator + '_ { + lookup_key.utf8_chunks().flat_map(|chunk| { + chunk.valid().chars().chain(if chunk.invalid().is_empty() { + None + } else { + Some(char::REPLACEMENT_CHARACTER) + }) + }) +} + +impl SizeInTargetProcess for StringName { + const SIZE: u64 = 0x8; +} #[derive(Debug, Copy, Clone, Pod, Zeroable)] #[repr(transparent)] -struct StringNameData(Address64); +struct Data(Address64); impl StringName { /// Reads the string from the target process. pub fn read(self, process: &Process) -> Result, Error> { - let cow_data: Address64 = self.0.read_at_offset(0x10, process)?; + // FIXME: This skips cname entirely atm. + + // FIXME: Use CowData + let cow_data: Address64 = self + .data + .read_at_byte_offset(offsets::data::NAME, process)?; // Only on 4.2 or before. let len = process diff --git a/src/game_engine/godot/core/string/ustring.rs b/src/game_engine/godot/core/string/ustring.rs index 663b92e..195e9cc 100644 --- a/src/game_engine/godot/core/string/ustring.rs +++ b/src/game_engine/godot/core/string/ustring.rs @@ -2,12 +2,18 @@ use arrayvec::{ArrayString, ArrayVec}; +use crate::game_engine::godot::SizeInTargetProcess; + /// A built-in type for strings. /// /// [`String`](https://docs.godotengine.org/en/4.2/classes/class_string.html) #[derive(Clone)] pub struct String(pub(super) ArrayVec); +impl SizeInTargetProcess for String { + const SIZE: u64 = 0x8; +} + impl String { /// Returns an iterator over the characters in this string. pub fn chars(&self) -> impl Iterator + '_ { diff --git a/src/game_engine/godot/core/templates/cowdata.rs b/src/game_engine/godot/core/templates/cowdata.rs new file mode 100644 index 0000000..e7b764b --- /dev/null +++ b/src/game_engine/godot/core/templates/cowdata.rs @@ -0,0 +1,30 @@ +//! + +use bytemuck::{Pod, Zeroable}; + +use crate::game_engine::godot::Ptr; + +/// A copy-on-write data type. This is not publicly exposed in Godot. +#[repr(transparent)] +pub struct CowData(Ptr); + +impl Copy for CowData {} + +impl Clone for CowData { + fn clone(&self) -> Self { + *self + } +} + +// SAFETY: The type is transparent over a `Ptr`, which is `Pod`. +unsafe impl Pod for CowData {} + +// SAFETY: The type is transparent over a `Ptr`, which is `Zeroable`. +unsafe impl Zeroable for CowData {} + +impl CowData { + /// Returns the pointer to the underlying data. + pub fn ptr(self) -> Ptr { + self.0 + } +} diff --git a/src/game_engine/godot/core/templates/hash_map.rs b/src/game_engine/godot/core/templates/hash_map.rs index 633e94b..35bfcfb 100644 --- a/src/game_engine/godot/core/templates/hash_map.rs +++ b/src/game_engine/godot/core/templates/hash_map.rs @@ -1,11 +1,38 @@ //! -use core::{iter, mem::size_of}; +use core::{iter, marker::PhantomData, num::NonZeroU32}; -use crate::{game_engine::godot::Ptr, Address64, Error, Process}; +use crate::{ + game_engine::godot::{Ptr, SizeInTargetProcess}, + Address64, Error, Process, +}; -/// A type that we know the size of in the target process. -pub trait KnownSize {} +use super::{ + hashfuncs::{fastmod, HASH_TABLE_SIZE_PRIMES, HASH_TABLE_SIZE_PRIMES_INV}, + Hash, +}; + +#[allow(unused)] +mod offsets { + pub const ELEMENTS: u32 = 0x8; + pub const HASHES: u32 = 0x10; + pub const HEAD_ELEMENT: u32 = 0x18; + pub const TAIL_ELEMENT: u32 = 0x20; + pub const CAPACITY_INDEX: u32 = 0x28; + pub const NUM_ELEMENTS: u32 = 0x2C; + + pub mod element { + pub const NEXT: u32 = 0x00; + pub const PREV: u32 = 0x08; + pub const KEY: u32 = 0x10; + } +} + +impl SizeInTargetProcess for HashMap { + const SIZE: u64 = 0x30; +} + +const EMPTY_HASH: u32 = 0; /// A hash map that maps keys to values. This is not publicly exposed as such in /// Godot, because it's a template class. The closest equivalent is the general @@ -14,30 +41,170 @@ pub trait KnownSize {} /// Check the [`Ptr`] documentation to see all the methods you can call on it. #[derive(Debug, Copy, Clone)] #[repr(transparent)] -pub struct HashMap(core::marker::PhantomData<(K, V)>); +pub struct HashMap(PhantomData (K, V)>); + +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +struct HashMapElement(PhantomData (K, V)>); + +impl Ptr> { + fn next(self, process: &Process) -> Result { + self.read_at_byte_offset(offsets::element::NEXT, process) + } + + fn prev(self, process: &Process) -> Result { + self.read_at_byte_offset(offsets::element::PREV, process) + } + + fn key(self) -> Ptr { + Ptr::new(self.addr() + offsets::element::KEY) + } + + fn value(self) -> Ptr + where + K: SizeInTargetProcess, + { + Ptr::new(self.addr() + offsets::element::KEY + K::SIZE) + } +} -impl Ptr> { +impl Ptr> { /// Returns an iterator over the key-value pairs in this hash map. pub fn iter<'a>(&'a self, process: &'a Process) -> impl Iterator, Ptr)> + 'a where - K: KnownSize, + K: SizeInTargetProcess, + { + let mut current: Ptr> = Ptr::new( + self.read_at_byte_offset(offsets::HEAD_ELEMENT, process) + .unwrap_or_default(), + ); + iter::from_fn(move || { + if current.is_null() { + return None; + } + let pair = (current.key(), current.value()); + current = current.next(process).ok()?; + Some(pair) + }) + } + + /// Returns a backwards iterator over the key-value pairs in this hash map. + pub fn iter_back<'a>( + &'a self, + process: &'a Process, + ) -> impl Iterator, Ptr)> + 'a + where + K: SizeInTargetProcess, { - let mut current: Address64 = self.read_at_offset(0x18, process).unwrap_or_default(); + let mut current: Ptr> = Ptr::new( + self.read_at_byte_offset(offsets::TAIL_ELEMENT, process) + .unwrap_or_default(), + ); iter::from_fn(move || { if current.is_null() { return None; } - let ret = ( - Ptr::new(current + 0x10), - Ptr::new(current + 0x10 + size_of::() as u64), - ); - current = process.read(current).ok()?; - Some(ret) + let pair = (current.key(), current.value()); + current = current.prev(process).ok()?; + Some(pair) }) } + /// Returns the value associated with the given key, or [`None`] if the key + /// is not in the hash map. + pub fn get(self, key: &Q, process: &Process) -> Result>, Error> + where + K: Hash + SizeInTargetProcess, + { + match self.lookup_pos(key, process)? { + Some(element) => Ok(Some(element.value())), + None => Ok(None), + } + } + /// Returns the number of elements in this hash map. pub fn size(self, process: &Process) -> Result { - self.read_at_offset(0x2C, process) + self.read_at_byte_offset(offsets::NUM_ELEMENTS, process) + } + + fn get_capacity_index(self, process: &Process) -> Result { + self.read_at_byte_offset(offsets::CAPACITY_INDEX, process) + } + + fn lookup_pos( + self, + key: &Q, + process: &Process, + ) -> Result>>, Error> + where + K: Hash, + { + let capacity_index = self.get_capacity_index(process)?; + + let capacity = *HASH_TABLE_SIZE_PRIMES + .get(capacity_index as usize) + .ok_or(Error {})?; + + let capacity_inv = *HASH_TABLE_SIZE_PRIMES_INV + .get(capacity_index as usize) + .ok_or(Error {})?; + + let hash = Self::hash(key); + let mut pos = fastmod(hash, capacity_inv, capacity); + let mut distance = 0; + + let [elements_ptr, hashes_ptr]: [Address64; 2] = + self.read_at_byte_offset(offsets::ELEMENTS, process)?; + + for _ in 0..10000 { + let current_hash: u32 = + process.read(hashes_ptr + pos.checked_mul(4).ok_or(Error {})?)?; + + if current_hash == EMPTY_HASH { + return Ok(None); + } + + if distance > get_probe_length(pos, current_hash, capacity, capacity_inv) { + return Ok(None); + } + + if current_hash == hash { + let element_ptr: Ptr> = + process.read(elements_ptr + pos.checked_mul(8).ok_or(Error {})?)?; + let element_key = element_ptr.key().deref(process)?; + if K::eq(&element_key, key, process) { + return Ok(Some(element_ptr)); + } + } + + pos = fastmod(pos.wrapping_add(1), capacity_inv, capacity); + distance += 1; + } + + Err(Error {}) } + + fn hash(key: &Q) -> u32 + where + K: Hash, + { + let hash = K::hash_of_lookup_key(key); + + if hash == EMPTY_HASH { + EMPTY_HASH + 1 + } else { + hash + } + } +} + +fn get_probe_length(p_pos: u32, p_hash: u32, p_capacity: NonZeroU32, p_capacity_inv: u64) -> u32 { + let original_pos = fastmod(p_hash, p_capacity_inv, p_capacity); + fastmod( + p_pos + .wrapping_sub(original_pos) + .wrapping_add(p_capacity.get()), + p_capacity_inv, + p_capacity, + ) } diff --git a/src/game_engine/godot/core/templates/hash_set.rs b/src/game_engine/godot/core/templates/hash_set.rs new file mode 100644 index 0000000..e58d354 --- /dev/null +++ b/src/game_engine/godot/core/templates/hash_set.rs @@ -0,0 +1,15 @@ +//! + +use core::marker::PhantomData; + +use crate::game_engine::godot::SizeInTargetProcess; + +impl SizeInTargetProcess for HashSet { + const SIZE: u64 = 40; +} + +/// A hash set that uniquely stores each element. This is not publicly exposed +/// in Godot. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct HashSet(PhantomData K>); diff --git a/src/game_engine/godot/core/templates/hashfuncs.rs b/src/game_engine/godot/core/templates/hashfuncs.rs new file mode 100644 index 0000000..36684b4 --- /dev/null +++ b/src/game_engine/godot/core/templates/hashfuncs.rs @@ -0,0 +1,105 @@ +//! + +use core::num::NonZeroU32; + +use bytemuck::CheckedBitPattern; + +use crate::Process; + +/// A trait for looking up a key in a hash table. The type of the key to look up +/// does not need to match the type in the target process. However, it needs to +/// hash and compare equally in the same way. +pub trait Hash: CheckedBitPattern { + /// Hashes the lookup key. + fn hash_of_lookup_key(lookup_key: &Q) -> u32; + /// Compares the lookup key with the key in the target process. Errors are + /// meant to be ignored and instead treated as comparing unequal. + fn eq(&self, lookup_key: &Q, process: &Process) -> bool; +} + +const HASH_TABLE_SIZE_MAX: usize = 29; + +#[track_caller] +pub(super) const fn n32(x: u32) -> NonZeroU32 { + match NonZeroU32::new(x) { + Some(x) => x, + None => panic!(), + } +} + +pub(super) const HASH_TABLE_SIZE_PRIMES: [NonZeroU32; HASH_TABLE_SIZE_MAX] = [ + n32(5), + n32(13), + n32(23), + n32(47), + n32(97), + n32(193), + n32(389), + n32(769), + n32(1543), + n32(3079), + n32(6151), + n32(12289), + n32(24593), + n32(49157), + n32(98317), + n32(196613), + n32(393241), + n32(786433), + n32(1572869), + n32(3145739), + n32(6291469), + n32(12582917), + n32(25165843), + n32(50331653), + n32(100663319), + n32(201326611), + n32(402653189), + n32(805306457), + n32(1610612741), +]; + +pub(super) const HASH_TABLE_SIZE_PRIMES_INV: [u64; HASH_TABLE_SIZE_MAX] = [ + 3689348814741910324, + 1418980313362273202, + 802032351030850071, + 392483916461905354, + 190172619316593316, + 95578984837873325, + 47420935922132524, + 23987963684927896, + 11955116055547344, + 5991147799191151, + 2998982941588287, + 1501077717772769, + 750081082979285, + 375261795343686, + 187625172388393, + 93822606204624, + 46909513691883, + 23456218233098, + 11728086747027, + 5864041509391, + 2932024948977, + 1466014921160, + 733007198436, + 366503839517, + 183251896093, + 91625960335, + 45812983922, + 22906489714, + 11453246088, +]; + +pub(super) fn fastmod(n: u32, _c: u64, d: NonZeroU32) -> u32 { + #[cfg(not(target_family = "wasm"))] + { + let lowbits = _c.wrapping_mul(n as u64); + // TODO: `widening_mul` + ((lowbits as u128 * d.get() as u128) >> 64) as u32 + } + #[cfg(target_family = "wasm")] + { + n % d.get() + } +} diff --git a/src/game_engine/godot/core/templates/list.rs b/src/game_engine/godot/core/templates/list.rs new file mode 100644 index 0000000..e343476 --- /dev/null +++ b/src/game_engine/godot/core/templates/list.rs @@ -0,0 +1,14 @@ +//! + +use core::marker::PhantomData; + +use crate::game_engine::godot::SizeInTargetProcess; + +impl SizeInTargetProcess for List { + const SIZE: u64 = 0x8; +} + +/// A linked list of elements. This is not publicly exposed in Godot. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct List(PhantomData T>); diff --git a/src/game_engine/godot/core/templates/mod.rs b/src/game_engine/godot/core/templates/mod.rs index e411060..75fca0b 100644 --- a/src/game_engine/godot/core/templates/mod.rs +++ b/src/game_engine/godot/core/templates/mod.rs @@ -1,3 +1,13 @@ +mod cowdata; mod hash_map; +mod hash_set; +mod hashfuncs; +mod list; +mod vector; +pub use cowdata::*; pub use hash_map::*; +pub use hash_set::*; +pub use hashfuncs::*; +pub use list::*; +pub use vector::*; diff --git a/src/game_engine/godot/core/templates/vector.rs b/src/game_engine/godot/core/templates/vector.rs new file mode 100644 index 0000000..9901ab0 --- /dev/null +++ b/src/game_engine/godot/core/templates/vector.rs @@ -0,0 +1,43 @@ +//! + +use core::mem::size_of; + +use bytemuck::{Pod, Zeroable}; + +use crate::game_engine::godot::{Ptr, SizeInTargetProcess}; + +use super::CowData; + +/// A contiguous vector of elements. This is not publicly exposed in Godot. +#[repr(C)] +pub struct Vector { + // lol this is pure padding, they messed up + write_proxy: [u8; 0x8], + cowdata: CowData, +} + +impl SizeInTargetProcess for Vector { + const SIZE: u64 = size_of::>() as u64; +} + +impl Copy for Vector {} + +impl Clone for Vector { + fn clone(&self) -> Self { + *self + } +} + +// SAFETY: The type is transparent over a `CowData` and a byte array, which is `Pod`. +unsafe impl Pod for Vector {} + +// SAFETY: The type is transparent over a `CowData` and a byte array, which is `Zeroable`. +unsafe impl Zeroable for Vector {} + +impl Vector { + /// Returns the pointer to the underlying data at the given index. This does + /// not perform bounds checking. + pub fn unchecked_at(&self, index: u64) -> Ptr { + Ptr::new(self.cowdata.ptr().addr() + index.wrapping_mul(T::SIZE)) + } +} diff --git a/src/game_engine/godot/core/variant/mod.rs b/src/game_engine/godot/core/variant/mod.rs new file mode 100644 index 0000000..97aab76 --- /dev/null +++ b/src/game_engine/godot/core/variant/mod.rs @@ -0,0 +1,3 @@ +mod variant; + +pub use variant::*; diff --git a/src/game_engine/godot/core/variant/variant.rs b/src/game_engine/godot/core/variant/variant.rs new file mode 100644 index 0000000..b302d40 --- /dev/null +++ b/src/game_engine/godot/core/variant/variant.rs @@ -0,0 +1,164 @@ +//! + +use core::{fmt, mem::size_of}; + +use bytemuck::{Pod, Zeroable}; + +use crate::game_engine::godot::SizeInTargetProcess; + +/// The type of a [`Variant`]. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Pod, Zeroable)] +#[repr(transparent)] +pub struct VariantType(u8); + +impl fmt::Debug for VariantType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::NIL => "NIL", + Self::BOOL => "BOOL", + Self::INT => "INT", + Self::FLOAT => "FLOAT", + Self::STRING => "STRING", + Self::VECTOR2 => "VECTOR2", + Self::VECTOR2I => "VECTOR2I", + Self::RECT2 => "RECT2", + Self::RECT2I => "RECT2I", + Self::VECTOR3 => "VECTOR3", + Self::VECTOR3I => "VECTOR3I", + Self::TRANSFORM2D => "TRANSFORM2D", + Self::VECTOR4 => "VECTOR4", + Self::VECTOR4I => "VECTOR4I", + Self::PLANE => "PLANE", + Self::QUATERNION => "QUATERNION", + Self::AABB => "AABB", + Self::BASIS => "BASIS", + Self::TRANSFORM3D => "TRANSFORM3D", + Self::PROJECTION => "PROJECTION", + Self::COLOR => "COLOR", + Self::STRING_NAME => "STRING_NAME", + Self::NODE_PATH => "NODE_PATH", + Self::RID => "RID", + Self::OBJECT => "OBJECT", + Self::CALLABLE => "CALLABLE", + Self::SIGNAL => "SIGNAL", + Self::DICTIONARY => "DICTIONARY", + Self::ARRAY => "ARRAY", + Self::PACKED_BYTE_ARRAY => "PACKED_BYTE_ARRAY", + Self::PACKED_INT32_ARRAY => "PACKED_INT32_ARRAY", + Self::PACKED_INT64_ARRAY => "PACKED_INT64_ARRAY", + Self::PACKED_FLOAT32_ARRAY => "PACKED_FLOAT32_ARRAY", + Self::PACKED_FLOAT64_ARRAY => "PACKED_FLOAT64_ARRAY", + Self::PACKED_STRING_ARRAY => "PACKED_STRING_ARRAY", + Self::PACKED_VECTOR2_ARRAY => "PACKED_VECTOR2_ARRAY", + Self::PACKED_VECTOR3_ARRAY => "PACKED_VECTOR3_ARRAY", + Self::PACKED_COLOR_ARRAY => "PACKED_COLOR_ARRAY", + _ => "", + }) + } +} + +#[allow(missing_docs)] +impl VariantType { + pub const NIL: Self = Self(0); + + // atomic types + pub const BOOL: Self = Self(1); + pub const INT: Self = Self(2); + pub const FLOAT: Self = Self(3); + pub const STRING: Self = Self(4); + + // math types + pub const VECTOR2: Self = Self(5); + pub const VECTOR2I: Self = Self(6); + pub const RECT2: Self = Self(7); + pub const RECT2I: Self = Self(8); + pub const VECTOR3: Self = Self(9); + pub const VECTOR3I: Self = Self(10); + pub const TRANSFORM2D: Self = Self(11); + pub const VECTOR4: Self = Self(12); + pub const VECTOR4I: Self = Self(13); + pub const PLANE: Self = Self(14); + pub const QUATERNION: Self = Self(15); + pub const AABB: Self = Self(16); + pub const BASIS: Self = Self(17); + pub const TRANSFORM3D: Self = Self(18); + pub const PROJECTION: Self = Self(19); + + // misc types + pub const COLOR: Self = Self(20); + pub const STRING_NAME: Self = Self(21); + pub const NODE_PATH: Self = Self(22); + pub const RID: Self = Self(23); + pub const OBJECT: Self = Self(24); + pub const CALLABLE: Self = Self(25); + pub const SIGNAL: Self = Self(26); + pub const DICTIONARY: Self = Self(27); + pub const ARRAY: Self = Self(28); + + // typed arrays + pub const PACKED_BYTE_ARRAY: Self = Self(29); + pub const PACKED_INT32_ARRAY: Self = Self(30); + pub const PACKED_INT64_ARRAY: Self = Self(31); + pub const PACKED_FLOAT32_ARRAY: Self = Self(32); + pub const PACKED_FLOAT64_ARRAY: Self = Self(33); + pub const PACKED_STRING_ARRAY: Self = Self(34); + pub const PACKED_VECTOR2_ARRAY: Self = Self(35); + pub const PACKED_VECTOR3_ARRAY: Self = Self(36); + pub const PACKED_COLOR_ARRAY: Self = Self(37); +} + +/// The most important data type in Godot. +/// +/// [`Variant`](https://docs.godotengine.org/en/4.2/classes/class_variant.html) +#[derive(Copy, Clone, PartialEq, Eq, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct Variant { + /// The type of the variant. + pub ty: VariantType, + _padding: [u8; 7], + /// The data of the variant. Use one of the accessors to get the data, based + /// on the type. + pub data: [u8; 16], +} + +impl Variant { + /// Assume the variant is a boolean and returns its value. Make sure this is + /// the correct type beforehand. + pub fn get_bool(&self) -> bool { + self.data[0] != 0 + } + + /// Assume the variant is an integer and returns its value. Make sure this + /// is the correct type beforehand. + pub fn get_int(&self) -> i32 { + let [i, _, _, _]: &[i32; 4] = bytemuck::cast_ref(&self.data); + *i + } + + /// Assume the variant is a float and returns its value. Make sure this is + /// the correct type beforehand. + pub fn get_float(&self) -> f32 { + let [f, _, _, _]: &[f32; 4] = bytemuck::cast_ref(&self.data); + *f + } +} + +impl fmt::Debug for Variant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.ty { + VariantType::NIL => write!(f, "Variant::NIL"), + VariantType::BOOL => write!(f, "Variant::BOOL({})", self.get_bool()), + VariantType::INT => write!(f, "Variant::INT({})", self.get_int()), + VariantType::FLOAT => write!(f, "Variant::FLOAT({})", self.get_float()), + _ => f + .debug_struct("Variant") + .field("ty", &self.ty) + .field("data", &self.data) + .finish(), + } + } +} + +impl SizeInTargetProcess for Variant { + const SIZE: u64 = size_of::() as u64; +} diff --git a/src/game_engine/godot/cpp/mod.rs b/src/game_engine/godot/cpp/mod.rs index 50cb912..c829a2b 100644 --- a/src/game_engine/godot/cpp/mod.rs +++ b/src/game_engine/godot/cpp/mod.rs @@ -9,3 +9,9 @@ mod vtable; pub use ptr::*; pub use type_info::*; pub use vtable::*; + +/// The size of a type in the target process. +pub trait SizeInTargetProcess { + /// The size of the type in the target process. + const SIZE: u64; +} diff --git a/src/game_engine/godot/cpp/ptr.rs b/src/game_engine/godot/cpp/ptr.rs index 697771e..983789a 100644 --- a/src/game_engine/godot/cpp/ptr.rs +++ b/src/game_engine/godot/cpp/ptr.rs @@ -23,6 +23,12 @@ impl Clone for Ptr { } } +impl Default for Ptr { + fn default() -> Self { + Self(Address64::NULL, PhantomData) + } +} + // SAFETY: The type is transparent over an `Address64`, which is `Pod`. unsafe impl Pod for Ptr {} @@ -50,7 +56,7 @@ impl Ptr { /// Reads the value that this pointer points to from the target process at /// the given offset. - pub fn read_at_offset(self, offset: O, process: &Process) -> Result + pub fn read_at_byte_offset(self, offset: O, process: &Process) -> Result where U: CheckedBitPattern, Address64: Add, diff --git a/src/game_engine/godot/cpp/type_info.rs b/src/game_engine/godot/cpp/type_info.rs index fa4716f..72bc537 100644 --- a/src/game_engine/godot/cpp/type_info.rs +++ b/src/game_engine/godot/cpp/type_info.rs @@ -22,7 +22,7 @@ impl Ptr { self, process: &Process, ) -> Result, Error> { - let name_ptr: Address64 = self.read_at_offset(0x8, process)?; + let name_ptr: Address64 = self.read_at_byte_offset(0x8, process)?; process.read(name_ptr) } diff --git a/src/game_engine/godot/cpp/vtable.rs b/src/game_engine/godot/cpp/vtable.rs index a7b2b77..5e6ebf5 100644 --- a/src/game_engine/godot/cpp/vtable.rs +++ b/src/game_engine/godot/cpp/vtable.rs @@ -19,6 +19,6 @@ impl Ptr { /// /// [`typeid`](https://en.cppreference.com/w/cpp/language/typeid) pub fn get_type_info(self, process: &Process) -> Result, Error> { - self.read_at_offset(-8, process) + self.read_at_byte_offset(-8, process) } } diff --git a/src/game_engine/godot/mod.rs b/src/game_engine/godot/mod.rs index 8adb75c..ee8378d 100644 --- a/src/game_engine/godot/mod.rs +++ b/src/game_engine/godot/mod.rs @@ -5,7 +5,8 @@ //! //! The main entry point is [`SceneTree::locate`], which locates the //! [`SceneTree`] instance in the game's memory. From there you can find the -//! root node and all its child nodes. +//! root node and all its child nodes. Nodes may also have attached scripts, +//! which can also be accessed and queried for their members. //! //! # Example //! @@ -22,6 +23,37 @@ //! // We print the tree of nodes starting from the root. //! asr::print_limited::<4096>(&root.print_tree::<64>(&process)); //! # } +//! ``` +//! +//! # Extensibility +//! +//! The types and the code are closely matching the Godot source code. If there +//! is anything missing, chances are that it can easily be added. Feel free to +//! open an issue or contribute the missing parts yourself. +//! +//! # Copyright Notice +//! +//! Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). +//! +//! Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. +//! +//! Permission is hereby granted, free of charge, to any person obtaining a copy +//! of this software and associated documentation files (the "Software"), to +//! deal in the Software without restriction, including without limitation the +//! rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +//! sell copies of the Software, and to permit persons to whom the Software is +//! furnished to do so, subject to the following conditions: +//! +//! The above copyright notice and this permission notice shall be included in +//! all copies or substantial portions of the Software. +//! +//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +//! FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +//! IN THE SOFTWARE. macro_rules! extends { ($Sub:ident: $Base:ident) => { @@ -36,9 +68,11 @@ macro_rules! extends { } mod core; +mod modules; mod scene; pub use core::*; +pub use modules::*; pub use scene::*; mod cpp; diff --git a/src/game_engine/godot/modules/gdscript/gdscript.rs b/src/game_engine/godot/modules/gdscript/gdscript.rs new file mode 100644 index 0000000..7f35cae --- /dev/null +++ b/src/game_engine/godot/modules/gdscript/gdscript.rs @@ -0,0 +1,115 @@ +//! + +use crate::{ + game_engine::godot::{HashMap, Ptr, Script, ScriptInstance, StringName, Variant, Vector}, + Error, Process, +}; + +#[allow(unused)] +mod offsets { + pub mod script_instance { + // ObjectId + pub const OWNER_ID: u64 = 0x8; + // *const Object + pub const OWNER: u64 = 0x10; + // Ref + pub const SCRIPT: u64 = 0x18; + // Vector + pub const MEMBERS: u64 = 0x20; + } + + pub mod script { + // bool + pub const TOOL: u64 = 0x178; + // bool + pub const VALID: u64 = 0x179; + // bool + pub const RELOADING: u64 = 0x17A; + // Ref + pub const NATIVE: u64 = 0x180; + // Ref + pub const BASE: u64 = 0x188; + // *const GDScript + pub const BASE_PTR: u64 = 0x190; + // *const GDScript + pub const OWNER_PTR: u64 = 0x198; + // HashMap + pub const MEMBER_INDICES: u64 = 0x1A0; + } + + pub mod member_info { + // i32 + pub const INDEX: u64 = 0x0; + // StringName + pub const SETTER: u64 = 0x8; + // StringName + pub const GETTER: u64 = 0x10; + // GDScriptDataType + pub const DATA_TYPE: u64 = 0x18; + } +} + +/// A script implemented in the GDScript programming language. +/// +/// [`GDScript`](https://docs.godotengine.org/en/4.2/classes/class_gdscript.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can +/// call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct GDScript; +extends!(GDScript: Script); + +impl Ptr { + /// Returns a [`HashMap`] that maps the name of each member to a + /// [`MemberInfo`] object. This object contains information about the + /// member, such as the index it occupies in the `members` array of a + /// [`GDScriptInstance`]. This can then be used to read the actual values of + /// the members, by indexing into the `members` array returned by + /// [`Ptr::get_members`]. + pub fn get_member_indices(self) -> Ptr> { + Ptr::new(self.addr() + offsets::script::MEMBER_INDICES) + } +} + +/// An instance of a script implemented in the GDScript programming language. +/// This is not publicly exposed in Godot. +/// +/// Check the [`Ptr`] documentation to see all the methods you +/// can call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct GDScriptInstance; +extends!(GDScriptInstance: ScriptInstance); + +impl Ptr { + /// Returns the [`GDScript`] that this instance is an instance of. This can + /// be used to query information about the script, such as the names of its + /// members and their indices. + pub fn get_script(self, process: &Process) -> Result, Error> { + self.read_at_byte_offset(offsets::script_instance::SCRIPT, process) + } + + /// Returns the values of all the members of this script instance. To figure + /// out the index of a member, use [`Ptr::get_member_indices`]. + pub fn get_members(self, process: &Process) -> Result, Error> { + self.read_at_byte_offset(offsets::script_instance::MEMBERS, process) + } +} + +/// Information about a member of a script implemented in the GDScript +/// programming language. This is not publicly exposed in Godot. +/// +/// Check the [`Ptr`] documentation to see all the methods you can +/// call on it. +pub struct MemberInfo; + +impl Ptr { + /// Returns the index of the member in the `members` array of a + /// [`GDScriptInstance`]. This can then be used to read the actual values of + /// the members, by indexing into the `members` array returned by + /// [`Ptr::get_members`]. + pub fn get_index(self, process: &Process) -> Result { + self.read_at_byte_offset(offsets::member_info::INDEX, process) + } +} diff --git a/src/game_engine/godot/modules/gdscript/mod.rs b/src/game_engine/godot/modules/gdscript/mod.rs new file mode 100644 index 0000000..d4c49b5 --- /dev/null +++ b/src/game_engine/godot/modules/gdscript/mod.rs @@ -0,0 +1,3 @@ +mod gdscript; + +pub use gdscript::*; diff --git a/src/game_engine/godot/modules/mod.rs b/src/game_engine/godot/modules/mod.rs new file mode 100644 index 0000000..92ccd37 --- /dev/null +++ b/src/game_engine/godot/modules/mod.rs @@ -0,0 +1,5 @@ +mod gdscript; +mod mono; + +pub use gdscript::*; +pub use mono::*; diff --git a/src/game_engine/godot/modules/mono/csharp_script.rs b/src/game_engine/godot/modules/mono/csharp_script.rs new file mode 100644 index 0000000..40a8852 --- /dev/null +++ b/src/game_engine/godot/modules/mono/csharp_script.rs @@ -0,0 +1,131 @@ +//! + +use crate::{ + game_engine::godot::{HashMap, PropertyInfo, Ptr, Script, ScriptInstance, StringName}, + Error, Process, +}; + +#[allow(unused)] +mod offsets { + pub mod script_instance { + // *const Object + pub const OWNER: u64 = 0x8; + // bool + pub const BASE_REF_COUNTED: u64 = 0x10; + // bool + pub const REF_DYING: u64 = 0x11; + // bool + pub const UNSAFE_REFERENCED: u64 = 0x12; + // bool + pub const PREDELETE_NOTIFIED: u64 = 0x13; + // Ref + pub const SCRIPT: u64 = 0x18; + // MonoGCHandleData + pub const GCHANDLE: u64 = 0x20; + } + + pub mod script { + use crate::game_engine::godot::{HashSet, SizeInTargetProcess, String, Vector}; + + // bool + pub const TOOL: u64 = 0x178; + // bool + pub const GLOBAL_CLASS: u64 = 0x179; + // bool + pub const ABSTRACT_CLASS: u64 = 0x17A; + // bool + pub const VALID: u64 = 0x17B; + // bool + pub const RELOAD_INVALIDATED: u64 = 0x17C; + // Ref + pub const BASE_SCRIPT: u64 = 0x180; + // HashSet<*const Object> + pub const INSTANCES: u64 = 0x188; + // String + pub const SOURCE: u64 = (INSTANCES + HashSet::<()>::SIZE).next_multiple_of(8); + // String + pub const CLASS_NAME: u64 = (SOURCE + String::<0>::SIZE).next_multiple_of(8); + // String + pub const ICON_PATH: u64 = (CLASS_NAME + String::<0>::SIZE).next_multiple_of(8); + // SelfList (4 pointers) + pub const SCRIPT_LIST: u64 = (ICON_PATH + String::<0>::SIZE).next_multiple_of(8); + // Dictionary (1 pointer) + pub const RPC_CONFIG: u64 = (SCRIPT_LIST + 4 * 8).next_multiple_of(8); + // Vector + pub const EVENT_SIGNALS: u64 = (RPC_CONFIG + 8).next_multiple_of(8); + // Vector + pub const METHODS: u64 = (EVENT_SIGNALS + Vector::<()>::SIZE).next_multiple_of(8); + // HashMap + pub const MEMBER_INFO: u64 = (METHODS + Vector::<()>::SIZE).next_multiple_of(8); + } +} + +/// A script implemented in the C# programming language, saved with the `.cs` +/// extension (Mono-enabled builds only). +/// +/// [`CSharpScript`](https://docs.godotengine.org/en/4.2/classes/class_csharpscript.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can +/// call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CSharpScript; +extends!(CSharpScript: Script); + +impl Ptr { + /// Returns a [`HashMap`] that maps the name of each member to a + /// [`PropertyInfo`] object. This object contains information about the + /// member, such as its type. Notably this is not the type on the C# side, + /// but a [`VariantType`](crate::game_engine::godot::VariantType). Unlike + /// with [`GDScript`](crate::game_engine::godot::GDScript), there is + /// currently no way to figure out where the member is stored in memory. + pub fn get_member_info(self) -> Ptr> { + Ptr::new(self.addr() + offsets::script::MEMBER_INFO) + } +} + +/// An instance of a script implemented in the C# programming language. This is +/// not publicly exposed in Godot. +/// +/// Check the [`Ptr`] documentation to see all the methods +/// you can call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CSharpScriptInstance; +extends!(CSharpScriptInstance: ScriptInstance); + +impl Ptr { + /// Returns the [`CSharpScript`] that this instance is an instance of. This + /// can be used to query information about the script, such as the names of + /// its members and their + /// [`VariantType`](crate::game_engine::godot::VariantType)s. + pub fn get_script(self, process: &Process) -> Result, Error> { + self.read_at_byte_offset(offsets::script_instance::SCRIPT, process) + } + + /// Returns the [`CSharpGCHandle`], which allows you to access the members of + /// the script instance. + pub fn get_gc_handle(self, process: &Process) -> Result, Error> { + self.read_at_byte_offset(offsets::script_instance::GCHANDLE, process) + } +} + +/// A handle to a C# object. This is not publicly exposed in Godot. +/// +/// Check the [`Ptr`] documentation to see all the methods you +/// can call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CSharpGCHandle; + +impl Ptr { + /// Returns a pointer to the start of the raw data of the instance. This is + /// where all the members are stored. You can use the `.Net Info` + /// functionality in Cheat Engine to figure out the offset of a member from + /// this pointer. Note that the garbage collector can move objects around in + /// memory, so this pointer should be queried in each tick of the auto + /// splitter. + pub fn get_instance_data(self, process: &Process) -> Result, Error> { + self.read_at_byte_offset(0x0, process) + } +} diff --git a/src/game_engine/godot/modules/mono/mod.rs b/src/game_engine/godot/modules/mono/mod.rs new file mode 100644 index 0000000..887239a --- /dev/null +++ b/src/game_engine/godot/modules/mono/mod.rs @@ -0,0 +1,3 @@ +mod csharp_script; + +pub use csharp_script::*; diff --git a/src/game_engine/godot/scene/main/canvas_item.rs b/src/game_engine/godot/scene/main/canvas_item.rs index f4c1381..8ac2c91 100644 --- a/src/game_engine/godot/scene/main/canvas_item.rs +++ b/src/game_engine/godot/scene/main/canvas_item.rs @@ -22,7 +22,7 @@ impl Ptr { /// or it has `top_level` enabled. /// /// [`CanvasItem.get_global_transform`](https://docs.godotengine.org/en/4.2/classes/class_canvasitem.html#class-canvasitem-method-get-global-transform) - pub fn get_global_transform(self, process: &Process) -> Result<[f32; 6], Error> { - self.read_at_offset(0x450, process) + pub fn get_global_transform(self, process: &Process) -> Result<[[f32; 2]; 3], Error> { + self.read_at_byte_offset(0x450, process) } } diff --git a/src/game_engine/godot/scene/main/node.rs b/src/game_engine/godot/scene/main/node.rs index f5794c7..b4889e6 100644 --- a/src/game_engine/godot/scene/main/node.rs +++ b/src/game_engine/godot/scene/main/node.rs @@ -25,7 +25,7 @@ impl Ptr { /// /// [`Node.get_parent`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-parent) pub fn get_parent(self, process: &Process) -> Result>, Error> { - self.read_at_offset(0x128, process).map( + self.read_at_byte_offset(0x128, process).map( |ptr: Ptr| { if ptr.is_null() { None @@ -42,7 +42,7 @@ impl Ptr { /// /// [`Node.get_owner`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-property-owner) pub fn get_owner(self, process: &Process) -> Result>, Error> { - self.read_at_offset(0x130, process).map( + self.read_at_byte_offset(0x130, process).map( |ptr: Ptr| { if ptr.is_null() { None @@ -53,6 +53,23 @@ impl Ptr { ) } + /// Finds the first descendant of this node whose name matches the name + /// provided, returning [`None`] if no match is found. The matching is done + /// against node names, not their paths. As such, it is case-sensitive. + /// Unlike the Godot API, not wildcards are supported. + /// + /// [`Node.find_child`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-find-child) + pub fn find_child( + self, + name: &[u8; N], + process: &Process, + ) -> Result>, Error> { + self.get_children() + .get(name, process)? + .map(|node| node.deref(process)) + .transpose() + } + /// Fetches a child node by its index. Each child node has an index relative /// its siblings (see [`get_index`](Self::get_index)). The first child is at /// index 0. If no child exists at the given index, this method returns an @@ -94,7 +111,7 @@ impl Ptr { /// /// [`Node.get_index`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-index) pub fn get_index(self, process: &Process) -> Result { - self.read_at_offset(0x1C4, process) + self.read_at_byte_offset(0x1C4, process) } /// The name of the node. This name must be unique among the siblings (other @@ -103,7 +120,7 @@ impl Ptr { /// /// [`Node.get_name`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-property-name) pub fn get_name(self, process: &Process) -> Result, Error> { - let string_name: StringName = self.read_at_offset(0x1D0, process)?; + let string_name: StringName = self.read_at_byte_offset(0x1D0, process)?; string_name.read(process) } @@ -130,7 +147,7 @@ impl Ptr { /// /// [`Node.get_tree`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-tree) pub fn get_tree(self, process: &Process) -> Result>, Error> { - self.read_at_offset(0x1D8, process).map( + self.read_at_byte_offset(0x1D8, process).map( |ptr: Ptr| { if ptr.is_null() { None diff --git a/src/game_engine/godot/scene/main/scene_tree.rs b/src/game_engine/godot/scene/main/scene_tree.rs index 3ce99f4..5588b6e 100644 --- a/src/game_engine/godot/scene/main/scene_tree.rs +++ b/src/game_engine/godot/scene/main/scene_tree.rs @@ -6,7 +6,41 @@ use crate::{ Address, Address64, Error, Process, }; -use super::Window; +use super::{Node, Window}; + +#[allow(unused)] +mod offsets { + use crate::{ + game_engine::godot::{HashMap, HashSet, List, SizeInTargetProcess}, + Address64, + }; + + // *const Window + pub const ROOT: u64 = 0x2B0; + // i64 + pub const CURRENT_FRAME: u64 = 0x330; + // i32 + pub const NODES_IN_TREE_COUNT: u64 = 0x338; + // bool + pub const PROCESSING: u64 = 0x33C; + // i32 + pub const NODES_REMOVED_ON_GROUP_CALL_LOCK: u64 = 0x340; + // HashSet<*const Node> + pub const NODES_REMOVED_ON_GROUP_CALL: u64 = 0x348; + // List + pub const DELETE_QUEUE: u64 = + (NODES_REMOVED_ON_GROUP_CALL + HashSet::<()>::SIZE).next_multiple_of(8); + /// HashMap, UGCall> + pub const UNIQUE_GROUP_CALLS: u64 = (DELETE_QUEUE + List::<()>::SIZE).next_multiple_of(8); + // bool + pub const UGC_LOCKED: u64 = UNIQUE_GROUP_CALLS + HashMap::<(), ()>::SIZE; + // *const Node + pub const CURRENT_SCENE: u64 = (UGC_LOCKED + 1).next_multiple_of(8); + // *const Node + pub const PREV_SCENE: u64 = CURRENT_SCENE + 8; + // *const Node + pub const PENDING_NEW_SCENE: u64 = PREV_SCENE + 8; +} /// Manages the game loop via a hierarchy of nodes. /// @@ -21,8 +55,8 @@ extends!(SceneTree: MainLoop); impl SceneTree { /// Locates the `SceneTree` instance in the given process. - pub fn locate(process: &Process, module: Address) -> Result, Error> { - let addr: Address64 = process.read(module + 0x0424BE40)?; + pub fn locate(process: &Process, main_module: Address) -> Result, Error> { + let addr: Address64 = process.read(main_module + 0x0424BE40)?; if addr.is_null() { return Err(Error {}); } @@ -30,8 +64,8 @@ impl SceneTree { } /// Waits for the `SceneTree` instance to be located in the given process. - pub async fn wait_locate(process: &Process, module: Address) -> Ptr { - retry(|| Self::locate(process, module)).await + pub async fn wait_locate(process: &Process, main_module: Address) -> Ptr { + retry(|| Self::locate(process, main_module)).await } } @@ -40,7 +74,7 @@ impl Ptr { /// /// [`SceneTree.get_root`](https://docs.godotengine.org/en/4.2/classes/class_scenetree.html#class-scenetree-property-root) pub fn get_root(self, process: &Process) -> Result, Error> { - self.read_at_offset(0x2B0, process) + self.read_at_byte_offset(offsets::ROOT, process) } /// Waits for the `SceneTree`'s root [`Window`] to be available. @@ -53,6 +87,15 @@ impl Ptr { /// /// [`SceneTree.get_frame`](https://docs.godotengine.org/en/4.2/classes/class_scenetree.html#class-scenetree-method-get-frame) pub fn get_frame(self, process: &Process) -> Result { - self.read_at_offset(0x330, process) + self.read_at_byte_offset(offsets::CURRENT_FRAME, process) + } + + /// Returns the root node of the currently running scene, regardless of its + /// structure. + /// + /// [`SceneTree.get_current_scene`](https://docs.godotengine.org/en/4.2/classes/class_scenetree.html#class-scenetree-property-current-scene) + pub fn get_current_scene(self, process: &Process) -> Result>, Error> { + let scene: Ptr = self.read_at_byte_offset(offsets::CURRENT_SCENE, process)?; + Ok(if scene.is_null() { None } else { Some(scene) }) } } diff --git a/src/game_engine/godot/scene/mod.rs b/src/game_engine/godot/scene/mod.rs index 271e5b7..ef735ba 100644 --- a/src/game_engine/godot/scene/mod.rs +++ b/src/game_engine/godot/scene/mod.rs @@ -1,5 +1,7 @@ mod main; +mod three_d; mod two_d; pub use main::*; +pub use three_d::*; pub use two_d::*; diff --git a/src/game_engine/godot/scene/three_d/collision_object_3d.rs b/src/game_engine/godot/scene/three_d/collision_object_3d.rs new file mode 100644 index 0000000..a1cbab5 --- /dev/null +++ b/src/game_engine/godot/scene/three_d/collision_object_3d.rs @@ -0,0 +1,11 @@ +//! + +use super::Node3D; + +/// Abstract base class for 3D physics objects. +/// +/// [`CollisionObject3D`](https://docs.godotengine.org/en/4.2/classes/class_collisionobject3d.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CollisionObject3D; +extends!(CollisionObject3D: Node3D); diff --git a/src/game_engine/godot/scene/three_d/mod.rs b/src/game_engine/godot/scene/three_d/mod.rs new file mode 100644 index 0000000..61a8550 --- /dev/null +++ b/src/game_engine/godot/scene/three_d/mod.rs @@ -0,0 +1,7 @@ +mod collision_object_3d; +mod node_3d; +mod physics_body_3d; + +pub use collision_object_3d::*; +pub use node_3d::*; +pub use physics_body_3d::*; diff --git a/src/game_engine/godot/scene/three_d/node_3d.rs b/src/game_engine/godot/scene/three_d/node_3d.rs new file mode 100644 index 0000000..e901e91 --- /dev/null +++ b/src/game_engine/godot/scene/three_d/node_3d.rs @@ -0,0 +1,38 @@ +//! + +use crate::{ + game_engine::godot::{Node, Ptr}, + Error, Process, +}; + +mod offsets { + // Transform3D + pub const GLOBAL_TRANSFORM: u64 = 0x3C8; + // Transform3D + pub const LOCAL_TRANSFORM: u64 = 0x3F8; +} + +/// Most basic 3D game object, parent of all 3D-related nodes. +/// +/// [`Node3D`](https://docs.godotengine.org/en/4.2/classes/class_node3d.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can call +/// on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Node3D; +extends!(Node3D: Node); + +impl Ptr { + /// World3D space (global) Transform3D of this node. + /// + /// [`Node3D.global_transform`](https://docs.godotengine.org/en/4.2/classes/class_node3d.html#class-node3d-property-global-transform) + pub fn get_global_transform(self, process: &Process) -> Result<[[f32; 3]; 4], Error> { + self.read_at_byte_offset(offsets::GLOBAL_TRANSFORM, process) + } + + /// Local Transform3D of this node. This is not exposed in Godot. + pub fn get_local_transform(self, process: &Process) -> Result<[[f32; 3]; 4], Error> { + self.read_at_byte_offset(offsets::LOCAL_TRANSFORM, process) + } +} diff --git a/src/game_engine/godot/scene/three_d/physics_body_3d.rs b/src/game_engine/godot/scene/three_d/physics_body_3d.rs new file mode 100644 index 0000000..4974ced --- /dev/null +++ b/src/game_engine/godot/scene/three_d/physics_body_3d.rs @@ -0,0 +1,39 @@ +//! + +use crate::{game_engine::godot::Ptr, Error, Process}; + +use super::CollisionObject3D; + +mod offsets { + // Vector3 + pub const VELOCITY: u64 = 0x5D8; +} + +/// Abstract base class for 3D game objects affected by physics. +/// +/// [`PhysicsBody3D`](https://docs.godotengine.org/en/4.2/classes/class_physicsbody3d.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct PhysicsBody3D; +extends!(PhysicsBody3D: CollisionObject3D); + +/// A 3D physics body specialized for characters moved by script. +/// +/// [`CharacterBody3D`](https://docs.godotengine.org/en/4.2/classes/class_characterbody3d.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you +/// can call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CharacterBody3D; +extends!(CharacterBody3D: PhysicsBody3D); + +impl Ptr { + /// Current velocity vector (typically meters per second), used and modified + /// during calls to `move_and_slide`. + /// + /// [`CharacterBody3D.velocity`](https://docs.godotengine.org/en/4.2/classes/class_characterbody3d.html#class-characterbody3d-property-velocity) + pub fn get_velocity(self, process: &Process) -> Result<[f32; 3], Error> { + self.read_at_byte_offset(offsets::VELOCITY, process) + } +} diff --git a/src/game_engine/godot/scene/two_d/collision_object_2d.rs b/src/game_engine/godot/scene/two_d/collision_object_2d.rs new file mode 100644 index 0000000..92ea2c0 --- /dev/null +++ b/src/game_engine/godot/scene/two_d/collision_object_2d.rs @@ -0,0 +1,11 @@ +//! + +use super::Node2D; + +/// Abstract base class for 2D physics objects. +/// +/// [`CollisionObject2D`](https://docs.godotengine.org/en/4.2/classes/class_collisionobject2d.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CollisionObject2D; +extends!(CollisionObject2D: Node2D); diff --git a/src/game_engine/godot/scene/two_d/mod.rs b/src/game_engine/godot/scene/two_d/mod.rs index c20c17c..fa32e7a 100644 --- a/src/game_engine/godot/scene/two_d/mod.rs +++ b/src/game_engine/godot/scene/two_d/mod.rs @@ -1,3 +1,7 @@ +mod collision_object_2d; mod node_2d; +mod physics_body_2d; +pub use collision_object_2d::*; pub use node_2d::*; +pub use physics_body_2d::*; diff --git a/src/game_engine/godot/scene/two_d/node_2d.rs b/src/game_engine/godot/scene/two_d/node_2d.rs index 9e22f28..399e252 100644 --- a/src/game_engine/godot/scene/two_d/node_2d.rs +++ b/src/game_engine/godot/scene/two_d/node_2d.rs @@ -22,20 +22,20 @@ impl Ptr { /// /// [`Node2D.get_position`](https://docs.godotengine.org/en/4.2/classes/class_node2d.html#class-node2d-property-position) pub fn get_position(self, process: &Process) -> Result<[f32; 2], Error> { - self.read_at_offset(0x48C, process) + self.read_at_byte_offset(0x48C, process) } /// Rotation in radians, relative to the node's parent. /// /// [`Node2D.get_rotation`](https://docs.godotengine.org/en/4.2/classes/class_node2d.html#class-node2d-property-rotation) pub fn get_rotation(self, process: &Process) -> Result { - self.read_at_offset(0x494, process) + self.read_at_byte_offset(0x494, process) } /// The node's scale. Unscaled value: `[1.0, 1.0]`. /// /// [`Node2D.get_scale`](https://docs.godotengine.org/en/4.2/classes/class_node2d.html#class-node2d-property-scale) pub fn get_scale(self, process: &Process) -> Result<[f32; 2], Error> { - self.read_at_offset(0x498, process) + self.read_at_byte_offset(0x498, process) } } diff --git a/src/game_engine/godot/scene/two_d/physics_body_2d.rs b/src/game_engine/godot/scene/two_d/physics_body_2d.rs new file mode 100644 index 0000000..06f262a --- /dev/null +++ b/src/game_engine/godot/scene/two_d/physics_body_2d.rs @@ -0,0 +1,39 @@ +//! + +use crate::{game_engine::godot::Ptr, Error, Process}; + +use super::CollisionObject2D; + +mod offsets { + // Vector2 + pub const VELOCITY: u64 = 0x5C4; +} + +/// Abstract base class for 2D game objects affected by physics. +/// +/// [`PhysicsBody2D`](https://docs.godotengine.org/en/4.2/classes/class_physicsbody2d.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct PhysicsBody2D; +extends!(PhysicsBody2D: CollisionObject2D); + +/// A 2D physics body specialized for characters moved by script. +/// +/// [`CharacterBody2D`](https://docs.godotengine.org/en/4.2/classes/class_characterbody2d.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you +/// can call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CharacterBody2D; +extends!(CharacterBody2D: PhysicsBody2D); + +impl Ptr { + /// Current velocity vector in pixels per second, used and modified during + /// calls to `move_and_slide`. + /// + /// [`CharacterBody2D.velocity`](https://docs.godotengine.org/en/4.2/classes/class_characterbody2d.html#class-characterbody2d-property-velocity) + pub fn get_velocity(self, process: &Process) -> Result<[f32; 2], Error> { + self.read_at_byte_offset(offsets::VELOCITY, process) + } +}