From 340daa6a4cef655ad75919329d042526036ada68 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 2 Jul 2024 22:56:12 +0200 Subject: [PATCH] Add Support for the Godot Engine (#98) This adds support for querying information and navigating the node tree in games built with the Godot Engine. This is initial version only supports games using Godot 4.2 without debug symbols and is missing many features such as accessing variables inside scripts. It is intentionally structured and documented in a way to match both the Godot documentation and the source code folder structure. --- Cargo.toml | 4 + src/game_engine/godot/core/mod.rs | 9 + src/game_engine/godot/core/object/mod.rs | 3 + src/game_engine/godot/core/object/object.rs | 23 +++ src/game_engine/godot/core/os/main_loop.rs | 11 ++ src/game_engine/godot/core/os/mod.rs | 3 + src/game_engine/godot/core/string/mod.rs | 5 + .../godot/core/string/string_name.rs | 47 +++++ src/game_engine/godot/core/string/ustring.rs | 34 ++++ .../godot/core/templates/hash_map.rs | 43 +++++ src/game_engine/godot/core/templates/mod.rs | 3 + src/game_engine/godot/cpp/mod.rs | 11 ++ src/game_engine/godot/cpp/ptr.rs | 70 +++++++ src/game_engine/godot/cpp/type_info.rs | 37 ++++ src/game_engine/godot/cpp/vtable.rs | 24 +++ src/game_engine/godot/mod.rs | 45 +++++ .../godot/scene/main/canvas_item.rs | 28 +++ src/game_engine/godot/scene/main/mod.rs | 11 ++ src/game_engine/godot/scene/main/node.rs | 180 ++++++++++++++++++ .../godot/scene/main/scene_tree.rs | 58 ++++++ src/game_engine/godot/scene/main/viewport.rs | 12 ++ src/game_engine/godot/scene/main/window.rs | 11 ++ src/game_engine/godot/scene/mod.rs | 5 + src/game_engine/godot/scene/two_d/mod.rs | 3 + src/game_engine/godot/scene/two_d/node_2d.rs | 41 ++++ src/game_engine/mod.rs | 2 + 26 files changed, 723 insertions(+) create mode 100644 src/game_engine/godot/core/mod.rs create mode 100644 src/game_engine/godot/core/object/mod.rs create mode 100644 src/game_engine/godot/core/object/object.rs create mode 100644 src/game_engine/godot/core/os/main_loop.rs create mode 100644 src/game_engine/godot/core/os/mod.rs create mode 100644 src/game_engine/godot/core/string/mod.rs create mode 100644 src/game_engine/godot/core/string/string_name.rs create mode 100644 src/game_engine/godot/core/string/ustring.rs create mode 100644 src/game_engine/godot/core/templates/hash_map.rs create mode 100644 src/game_engine/godot/core/templates/mod.rs create mode 100644 src/game_engine/godot/cpp/mod.rs create mode 100644 src/game_engine/godot/cpp/ptr.rs create mode 100644 src/game_engine/godot/cpp/type_info.rs create mode 100644 src/game_engine/godot/cpp/vtable.rs create mode 100644 src/game_engine/godot/mod.rs create mode 100644 src/game_engine/godot/scene/main/canvas_item.rs create mode 100644 src/game_engine/godot/scene/main/mod.rs create mode 100644 src/game_engine/godot/scene/main/node.rs create mode 100644 src/game_engine/godot/scene/main/scene_tree.rs create mode 100644 src/game_engine/godot/scene/main/viewport.rs create mode 100644 src/game_engine/godot/scene/main/window.rs create mode 100644 src/game_engine/godot/scene/mod.rs create mode 100644 src/game_engine/godot/scene/two_d/mod.rs create mode 100644 src/game_engine/godot/scene/two_d/node_2d.rs diff --git a/Cargo.toml b/Cargo.toml index c422ea1..64cc7d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ signature = ["memchr"] wasi-no-std = ["libm"] # Game Engines +godot = [] unity = ["signature", "asr-derive?/unity"] unreal = ["signature"] @@ -41,3 +42,6 @@ ps1 = ["flags", "signature"] ps2 = ["flags", "signature"] sms = ["flags", "signature"] wii = ["flags"] + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(doc_cfg)'] } diff --git a/src/game_engine/godot/core/mod.rs b/src/game_engine/godot/core/mod.rs new file mode 100644 index 0000000..3755250 --- /dev/null +++ b/src/game_engine/godot/core/mod.rs @@ -0,0 +1,9 @@ +mod object; +mod os; +mod string; +mod templates; + +pub use object::*; +pub use os::*; +pub use string::*; +pub use templates::*; diff --git a/src/game_engine/godot/core/object/mod.rs b/src/game_engine/godot/core/object/mod.rs new file mode 100644 index 0000000..75ce6bc --- /dev/null +++ b/src/game_engine/godot/core/object/mod.rs @@ -0,0 +1,3 @@ +mod object; + +pub use object::*; diff --git a/src/game_engine/godot/core/object/object.rs b/src/game_engine/godot/core/object/object.rs new file mode 100644 index 0000000..44aba8b --- /dev/null +++ b/src/game_engine/godot/core/object/object.rs @@ -0,0 +1,23 @@ +//! + +use crate::{ + game_engine::godot::{Ptr, VTable}, + Error, Process, +}; + +/// Base class for all other classes in the engine. +/// +/// [`Object`](https://docs.godotengine.org/en/4.2/classes/class_object.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can call +/// on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +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()) + } +} diff --git a/src/game_engine/godot/core/os/main_loop.rs b/src/game_engine/godot/core/os/main_loop.rs new file mode 100644 index 0000000..e5624d6 --- /dev/null +++ b/src/game_engine/godot/core/os/main_loop.rs @@ -0,0 +1,11 @@ +//! + +use crate::game_engine::godot::Object; + +/// Abstract base class for the game's main loop. +/// +/// [`MainLoop`](https://docs.godotengine.org/en/4.2/classes/class_mainloop.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct MainLoop; +extends!(MainLoop: Object); diff --git a/src/game_engine/godot/core/os/mod.rs b/src/game_engine/godot/core/os/mod.rs new file mode 100644 index 0000000..06fd1c1 --- /dev/null +++ b/src/game_engine/godot/core/os/mod.rs @@ -0,0 +1,3 @@ +mod main_loop; + +pub use main_loop::*; diff --git a/src/game_engine/godot/core/string/mod.rs b/src/game_engine/godot/core/string/mod.rs new file mode 100644 index 0000000..d424232 --- /dev/null +++ b/src/game_engine/godot/core/string/mod.rs @@ -0,0 +1,5 @@ +mod string_name; +mod ustring; + +pub use string_name::*; +pub use ustring::*; diff --git a/src/game_engine/godot/core/string/string_name.rs b/src/game_engine/godot/core/string/string_name.rs new file mode 100644 index 0000000..ad89053 --- /dev/null +++ b/src/game_engine/godot/core/string/string_name.rs @@ -0,0 +1,47 @@ +//! + +use core::mem::MaybeUninit; + +use arrayvec::ArrayVec; +use bytemuck::{Pod, Zeroable}; + +use crate::{ + game_engine::godot::{KnownSize, Ptr}, + Address64, Error, Process, +}; + +use super::String; + +/// 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); + +impl KnownSize for StringName {} + +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +#[repr(transparent)] +struct StringNameData(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)?; + + // Only on 4.2 or before. + let len = process + .read::(cow_data + -0x4)? + .checked_sub(1) + .ok_or(Error {})?; + let mut buf = [MaybeUninit::uninit(); N]; + let buf = buf.get_mut(..len as usize).ok_or(Error {})?; + let buf = process.read_into_uninit_slice(cow_data, buf)?; + + let mut out = ArrayVec::new(); + out.extend(buf.iter().copied()); + + Ok(String(out)) + } +} diff --git a/src/game_engine/godot/core/string/ustring.rs b/src/game_engine/godot/core/string/ustring.rs new file mode 100644 index 0000000..663b92e --- /dev/null +++ b/src/game_engine/godot/core/string/ustring.rs @@ -0,0 +1,34 @@ +//! + +use arrayvec::{ArrayString, ArrayVec}; + +/// 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 String { + /// Returns an iterator over the characters in this string. + pub fn chars(&self) -> impl Iterator + '_ { + self.0 + .iter() + .copied() + .map(|c| char::from_u32(c).unwrap_or(char::REPLACEMENT_CHARACTER)) + } + + /// Converts this string to an [`ArrayString`]. If the string is too long to + /// fit in the array, the excess characters are truncated. + pub fn to_array_string(&self) -> ArrayString { + let mut buf = ArrayString::::new(); + for c in self.chars() { + let _ = buf.try_push(c); + } + buf + } + + /// Checks if this string matches the given string. + pub fn matches_str(&self, text: &str) -> bool { + self.chars().eq(text.chars()) + } +} diff --git a/src/game_engine/godot/core/templates/hash_map.rs b/src/game_engine/godot/core/templates/hash_map.rs new file mode 100644 index 0000000..633e94b --- /dev/null +++ b/src/game_engine/godot/core/templates/hash_map.rs @@ -0,0 +1,43 @@ +//! + +use core::{iter, mem::size_of}; + +use crate::{game_engine::godot::Ptr, Address64, Error, Process}; + +/// A type that we know the size of in the target process. +pub trait KnownSize {} + +/// 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 +/// [`Dictionary`](https://docs.godotengine.org/en/4.2/classes/class_dictionary.html). +/// +/// 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)>); + +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, + { + let mut current: Address64 = self.read_at_offset(0x18, 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) + }) + } + + /// Returns the number of elements in this hash map. + pub fn size(self, process: &Process) -> Result { + self.read_at_offset(0x2C, process) + } +} diff --git a/src/game_engine/godot/core/templates/mod.rs b/src/game_engine/godot/core/templates/mod.rs new file mode 100644 index 0000000..e411060 --- /dev/null +++ b/src/game_engine/godot/core/templates/mod.rs @@ -0,0 +1,3 @@ +mod hash_map; + +pub use hash_map::*; diff --git a/src/game_engine/godot/cpp/mod.rs b/src/game_engine/godot/cpp/mod.rs new file mode 100644 index 0000000..50cb912 --- /dev/null +++ b/src/game_engine/godot/cpp/mod.rs @@ -0,0 +1,11 @@ +//! This module is not Godot specific and instead provides generic utilities for +//! working with processes written in C++. It could be moved outside at some +//! point in the future. + +mod ptr; +mod type_info; +mod vtable; + +pub use ptr::*; +pub use type_info::*; +pub use vtable::*; diff --git a/src/game_engine/godot/cpp/ptr.rs b/src/game_engine/godot/cpp/ptr.rs new file mode 100644 index 0000000..697771e --- /dev/null +++ b/src/game_engine/godot/cpp/ptr.rs @@ -0,0 +1,70 @@ +use core::{any::type_name, fmt, marker::PhantomData, ops::Add}; + +use bytemuck::{CheckedBitPattern, Pod, Zeroable}; + +use crate::{Address64, Error, Process}; + +/// A pointer is an address in the target process that knows the type that it's +/// targeting. +#[repr(transparent)] +pub struct Ptr(Address64, PhantomData T>); + +impl fmt::Debug for Ptr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}*: {}", type_name::(), self.0) + } +} + +impl Copy for Ptr {} + +impl Clone for Ptr { + fn clone(&self) -> Self { + *self + } +} + +// SAFETY: The type is transparent over an `Address64`, which is `Pod`. +unsafe impl Pod for Ptr {} + +// SAFETY: The type is transparent over an `Address64`, which is `Zeroable`. +unsafe impl Zeroable for Ptr {} + +impl Ptr { + /// Creates a new pointer from the given address. + pub fn new(addr: Address64) -> Self { + Self(addr, PhantomData) + } + + /// Checks whether the pointer is null. + pub fn is_null(self) -> bool { + self.0.is_null() + } + + /// Reads the value that this pointer points to from the target process. + pub fn deref(self, process: &Process) -> Result + where + T: CheckedBitPattern, + { + process.read(self.0) + } + + /// 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 + where + U: CheckedBitPattern, + Address64: Add, + { + process.read(self.0 + offset) + } + + /// Casts this pointer to a pointer of a different type without any checks. + pub fn unchecked_cast(self) -> Ptr { + Ptr::new(self.0) + } + + /// Returns the address that this pointer points to. + pub fn addr(self) -> Address64 { + self.0 + } +} diff --git a/src/game_engine/godot/cpp/type_info.rs b/src/game_engine/godot/cpp/type_info.rs new file mode 100644 index 0000000..fa4716f --- /dev/null +++ b/src/game_engine/godot/cpp/type_info.rs @@ -0,0 +1,37 @@ +use crate::{string::ArrayCString, Address64, Error, Process}; + +use super::Ptr; + +/// The class `TypeInfo` holds implementation-specific information about a +/// type, including the name of the type and means to compare two types for +/// equality or collating order. This is the class returned by +/// [`Ptr::get_type_info`]. +/// +/// [`std::type_info`](https://en.cppreference.com/w/cpp/types/type_info) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct TypeInfo; + +impl Ptr { + /// Returns a GCC/Clang mangled null-terminated character string containing + /// the name of the type. No guarantees are given; in particular, the + /// returned string can be identical for several types. + /// + /// [`std::type_info::name`](https://en.cppreference.com/w/cpp/types/type_info/name) + pub fn get_mangled_name( + self, + process: &Process, + ) -> Result, Error> { + let name_ptr: Address64 = self.read_at_offset(0x8, process)?; + process.read(name_ptr) + } + + /// Checks if the mangled name of the type matches the given string. + pub fn matches_mangled_name( + self, + mangled_name: &[u8; N], + process: &Process, + ) -> Result { + Ok(self.get_mangled_name::(process)?.matches(mangled_name)) + } +} diff --git a/src/game_engine/godot/cpp/vtable.rs b/src/game_engine/godot/cpp/vtable.rs new file mode 100644 index 0000000..a7b2b77 --- /dev/null +++ b/src/game_engine/godot/cpp/vtable.rs @@ -0,0 +1,24 @@ +use crate::{Error, Process}; + +use super::{Ptr, TypeInfo}; + +/// A C++ virtual method table. +/// +/// This can be used to look up virtual functions and type information for the +/// object. A pointer to a vtable is unique for each type, so comparing pointers +/// is enough to check for type equality. +/// +/// [Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct VTable; + +impl Ptr { + /// Queries information of a type. Used where the dynamic type of a + /// polymorphic object must be known and for static type identification. + /// + /// [`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) + } +} diff --git a/src/game_engine/godot/mod.rs b/src/game_engine/godot/mod.rs new file mode 100644 index 0000000..8adb75c --- /dev/null +++ b/src/game_engine/godot/mod.rs @@ -0,0 +1,45 @@ +//! Support for games using the Godot engine. +//! +//! The support is still very experimental. Currently only games using Godot 4.2 +//! without any debug symbols are supported. +//! +//! 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. +//! +//! # Example +//! +//! ```no_run +//! # async fn example(process: asr::Process, main_module_address: asr::Address) { +//! use asr::game_engine::godot::SceneTree; +//! +//! // We first locate the SceneTree instance. +//! let scene_tree = SceneTree::wait_locate(&process, main_module_address).await; +//! +//! // We access the root node of the SceneTree. +//! let root = scene_tree.wait_get_root(&process).await; +//! +//! // We print the tree of nodes starting from the root. +//! asr::print_limited::<4096>(&root.print_tree::<64>(&process)); +//! # } + +macro_rules! extends { + ($Sub:ident: $Base:ident) => { + impl core::ops::Deref for crate::game_engine::godot::Ptr<$Sub> { + type Target = crate::game_engine::godot::Ptr<$Base>; + + fn deref(&self) -> &Self::Target { + bytemuck::cast_ref(self) + } + } + }; +} + +mod core; +mod scene; + +pub use core::*; +pub use scene::*; + +mod cpp; +pub use cpp::*; diff --git a/src/game_engine/godot/scene/main/canvas_item.rs b/src/game_engine/godot/scene/main/canvas_item.rs new file mode 100644 index 0000000..f4c1381 --- /dev/null +++ b/src/game_engine/godot/scene/main/canvas_item.rs @@ -0,0 +1,28 @@ +//! + +use crate::{game_engine::godot::Ptr, Error, Process}; + +use super::Node; + +/// Abstract base class for everything in 2D space. +/// +/// [`CanvasItem`](https://docs.godotengine.org/en/4.2/classes/class_canvasitem.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can +/// call on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CanvasItem; +extends!(CanvasItem: Node); + +impl Ptr { + /// Returns the global transform matrix of this item, i.e. the combined + /// transform up to the topmost **CanvasItem** node. The topmost item is a + /// **CanvasItem** that either has no parent, has non-**CanvasItem** parent + /// 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) + } +} diff --git a/src/game_engine/godot/scene/main/mod.rs b/src/game_engine/godot/scene/main/mod.rs new file mode 100644 index 0000000..e01dd7a --- /dev/null +++ b/src/game_engine/godot/scene/main/mod.rs @@ -0,0 +1,11 @@ +mod canvas_item; +mod node; +mod scene_tree; +mod viewport; +mod window; + +pub use canvas_item::*; +pub use node::*; +pub use scene_tree::*; +pub use viewport::*; +pub use window::*; diff --git a/src/game_engine/godot/scene/main/node.rs b/src/game_engine/godot/scene/main/node.rs new file mode 100644 index 0000000..f5794c7 --- /dev/null +++ b/src/game_engine/godot/scene/main/node.rs @@ -0,0 +1,180 @@ +//! + +use core::fmt; + +use crate::{ + game_engine::godot::{HashMap, Object, Ptr, String, StringName}, + Error, Process, +}; + +use super::SceneTree; + +/// Base class for all scene objects. +/// +/// [`Node`](https://docs.godotengine.org/en/4.2/classes/class_node.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can call +/// on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Node; +extends!(Node: Object); + +impl Ptr { + /// Returns this node's parent node, or [`None`] if the node doesn't have a parent. + /// + /// [`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( + |ptr: Ptr| { + if ptr.is_null() { + None + } else { + Some(ptr) + } + }, + ) + } + + /// The owner of this node. The owner must be an ancestor of this node. When + /// packing the owner node in a `PackedScene`, all the nodes it owns are + /// also saved with it. + /// + /// [`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( + |ptr: Ptr| { + if ptr.is_null() { + None + } else { + Some(ptr) + } + }, + ) + } + + /// 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 + /// error. + /// + /// [`Node.get_child`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-child) + /// + /// # Warning + /// + /// Prefer not using this function in loops, it has to iterate over a linked + /// list and is not actually O(1). Iterate over the children directly + /// instead in that case. Only use this function if you know the specific + /// index of the child node. + pub fn get_child(self, idx: usize, process: &Process) -> Result, Error> { + self.get_children() + .iter(process) + .nth(idx) + .ok_or(Error {})? + .1 + .deref(process) + } + + /// Returns the number of children of this node. + /// + /// [`Node.get_child_count`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-child-count) + pub fn get_child_count(self, process: &Process) -> Result { + self.get_children().size(process) + } + + /// Returns all children of this node inside a [`HashMap`]. + /// + /// [`Node.get_children`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-get-children) + pub fn get_children(self) -> Ptr>> { + Ptr::new(self.addr() + 0x138) + } + + /// Returns this node's order among its siblings. The first node's index is + /// `0`. See also [`get_child`](Self::get_child). + /// + /// [`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) + } + + /// The name of the node. This name must be unique among the siblings (other + /// child nodes from the same parent). When set to an existing sibling's + /// name, the node is automatically renamed. + /// + /// [`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)?; + string_name.read(process) + } + + /// Prints the node and its children, recursively. The node does not have to + /// be inside the tree. + /// + /// [`Node.print_tree`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-print-tree) + #[must_use] + pub fn print_tree(self, process: &Process) -> PrintTree<'_, N> { + PrintTree(self, process) + } + + /// Returns [`true`] if this node is currently inside [`SceneTree`]. See + /// also [`get_tree`](Self::get_tree). + /// + /// [`Node.is_inside_tree`](https://docs.godotengine.org/en/4.2/classes/class_node.html#class-node-method-is-inside-tree) + pub fn is_inside_tree(self, process: &Process) -> Result { + self.get_tree(process).map(|tree| tree.is_some()) + } + + /// Returns the [`SceneTree`] that contains this node. If this node is not + /// inside the tree, returns [`None`]. See also + /// [`is_inside_tree`](Self::is_inside_tree). + /// + /// [`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( + |ptr: Ptr| { + if ptr.is_null() { + None + } else { + Some(ptr) + } + }, + ) + } +} + +/// A recursive tree printer. +pub struct PrintTree<'p, const N: usize>(Ptr, &'p Process); + +impl<'p, const N: usize> fmt::Debug for PrintTree<'p, N> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug_map = f.debug_map(); + for (name, node) in self.0.get_children().iter(self.1) { + self.print_key(&mut debug_map, name); + match node.deref(self.1) { + Ok(node) => debug_map.value(&PrintTree::(node, self.1)), + Err(_) => debug_map.value(&""), + }; + } + debug_map.finish() + } +} + +impl<'p, const N: usize> fmt::Display for PrintTree<'p, N> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#?}", self) + } +} + +impl<'p, const N: usize> PrintTree<'p, N> { + #[inline(never)] + fn print_key(&self, debug_map: &mut fmt::DebugMap<'_, '_>, name: Ptr) { + debug_map.key( + &name + .deref(self.1) + .ok() + .and_then(|name| Some(name.read::(self.1).ok()?.to_array_string::())) + .as_deref() + .unwrap_or(""), + ); + } +} diff --git a/src/game_engine/godot/scene/main/scene_tree.rs b/src/game_engine/godot/scene/main/scene_tree.rs new file mode 100644 index 0000000..3ce99f4 --- /dev/null +++ b/src/game_engine/godot/scene/main/scene_tree.rs @@ -0,0 +1,58 @@ +//! + +use crate::{ + future::retry, + game_engine::godot::{MainLoop, Ptr}, + Address, Address64, Error, Process, +}; + +use super::Window; + +/// Manages the game loop via a hierarchy of nodes. +/// +/// [`SceneTree`](https://docs.godotengine.org/en/4.2/classes/class_scenetree.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can call +/// on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct SceneTree; +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)?; + if addr.is_null() { + return Err(Error {}); + } + Ok(Ptr::new(addr)) + } + + /// 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 + } +} + +impl Ptr { + /// The `SceneTree`'s root [`Window`]. + /// + /// [`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) + } + + /// Waits for the `SceneTree`'s root [`Window`] to be available. + pub async fn wait_get_root(self, process: &Process) -> Ptr { + retry(|| self.get_root(process)).await + } + + /// Returns the current frame number, i.e. the total frame count since the + /// application started. + /// + /// [`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) + } +} diff --git a/src/game_engine/godot/scene/main/viewport.rs b/src/game_engine/godot/scene/main/viewport.rs new file mode 100644 index 0000000..18b8efe --- /dev/null +++ b/src/game_engine/godot/scene/main/viewport.rs @@ -0,0 +1,12 @@ +//! + +use super::Node; + +/// Abstract base class for viewports. Encapsulates drawing and interaction with +/// a game world. +/// +/// [`Viewport`](https://docs.godotengine.org/en/4.2/classes/class_viewport.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Viewport; +extends!(Viewport: Node); diff --git a/src/game_engine/godot/scene/main/window.rs b/src/game_engine/godot/scene/main/window.rs new file mode 100644 index 0000000..77fc3c1 --- /dev/null +++ b/src/game_engine/godot/scene/main/window.rs @@ -0,0 +1,11 @@ +//! + +use super::Viewport; + +/// Base class for all windows, dialogs, and popups. +/// +/// [`Window`](https://docs.godotengine.org/en/4.2/classes/class_window.html) +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Window; +extends!(Window: Viewport); diff --git a/src/game_engine/godot/scene/mod.rs b/src/game_engine/godot/scene/mod.rs new file mode 100644 index 0000000..271e5b7 --- /dev/null +++ b/src/game_engine/godot/scene/mod.rs @@ -0,0 +1,5 @@ +mod main; +mod two_d; + +pub use main::*; +pub use two_d::*; diff --git a/src/game_engine/godot/scene/two_d/mod.rs b/src/game_engine/godot/scene/two_d/mod.rs new file mode 100644 index 0000000..c20c17c --- /dev/null +++ b/src/game_engine/godot/scene/two_d/mod.rs @@ -0,0 +1,3 @@ +mod node_2d; + +pub use node_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 new file mode 100644 index 0000000..9e22f28 --- /dev/null +++ b/src/game_engine/godot/scene/two_d/node_2d.rs @@ -0,0 +1,41 @@ +//! + +use crate::{ + game_engine::godot::{CanvasItem, Ptr}, + Error, Process, +}; + +/// A 2D game object, inherited by all 2D-related nodes. Has a position, +/// rotation, scale, and Z index. +/// +/// [`Node2D`](https://docs.godotengine.org/en/4.2/classes/class_node2d.html) +/// +/// Check the [`Ptr`] documentation to see all the methods you can call +/// on it. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct Node2D; +extends!(Node2D: CanvasItem); + +impl Ptr { + /// Position, relative to the node's parent. + /// + /// [`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) + } + + /// 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) + } + + /// 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) + } +} diff --git a/src/game_engine/mod.rs b/src/game_engine/mod.rs index 26f693d..751b02d 100644 --- a/src/game_engine/mod.rs +++ b/src/game_engine/mod.rs @@ -1,5 +1,7 @@ //! Support for attaching to various game engines. +#[cfg(feature = "godot")] +pub mod godot; #[cfg(feature = "unity")] pub mod unity; #[cfg(feature = "unreal")]