Skip to content

Commit

Permalink
Add Support for the Godot Engine (#98)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
CryZe authored Jul 2, 2024
1 parent 5eb4619 commit 340daa6
Show file tree
Hide file tree
Showing 26 changed files with 723 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ signature = ["memchr"]
wasi-no-std = ["libm"]

# Game Engines
godot = []
unity = ["signature", "asr-derive?/unity"]
unreal = ["signature"]

Expand All @@ -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)'] }
9 changes: 9 additions & 0 deletions src/game_engine/godot/core/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mod object;
mod os;
mod string;
mod templates;

pub use object::*;
pub use os::*;
pub use string::*;
pub use templates::*;
3 changes: 3 additions & 0 deletions src/game_engine/godot/core/object/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod object;

pub use object::*;
23 changes: 23 additions & 0 deletions src/game_engine/godot/core/object/object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! <https://github.com/godotengine/godot/blob/07cf36d21c9056fb4055f020949fb90ebd795afb/core/object/object.h>
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<Object>`] documentation to see all the methods you can call
/// on it.
#[derive(Debug, Copy, Clone)]
#[repr(transparent)]
pub struct Object;

impl Ptr<Object> {
/// Returns a pointer to the object's virtual method table.
pub fn get_vtable(self, process: &Process) -> Result<Ptr<VTable>, Error> {
process.read(self.addr())
}
}
11 changes: 11 additions & 0 deletions src/game_engine/godot/core/os/main_loop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! <https://github.com/godotengine/godot/blob/07cf36d21c9056fb4055f020949fb90ebd795afb/core/os/main_loop.h>
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);
3 changes: 3 additions & 0 deletions src/game_engine/godot/core/os/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod main_loop;

pub use main_loop::*;
5 changes: 5 additions & 0 deletions src/game_engine/godot/core/string/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod string_name;
mod ustring;

pub use string_name::*;
pub use ustring::*;
47 changes: 47 additions & 0 deletions src/game_engine/godot/core/string/string_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! <https://github.com/godotengine/godot/blob/07cf36d21c9056fb4055f020949fb90ebd795afb/core/string/string_name.h>
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<StringNameData>);

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<const N: usize>(self, process: &Process) -> Result<String<N>, Error> {
let cow_data: Address64 = self.0.read_at_offset(0x10, process)?;

// Only on 4.2 or before.
let len = process
.read::<u32>(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))
}
}
34 changes: 34 additions & 0 deletions src/game_engine/godot/core/string/ustring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! <https://github.com/godotengine/godot/blob/07cf36d21c9056fb4055f020949fb90ebd795afb/core/string/ustring.h>
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<const N: usize>(pub(super) ArrayVec<u32, N>);

impl<const N: usize> String<N> {
/// Returns an iterator over the characters in this string.
pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
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<const UTF8_SIZE: usize>(&self) -> ArrayString<UTF8_SIZE> {
let mut buf = ArrayString::<UTF8_SIZE>::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())
}
}
43 changes: 43 additions & 0 deletions src/game_engine/godot/core/templates/hash_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! <https://github.com/godotengine/godot/blob/07cf36d21c9056fb4055f020949fb90ebd795afb/core/templates/hash_map.h>
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<K, V>(core::marker::PhantomData<(K, V)>);

impl<K, V> Ptr<HashMap<K, V>> {
/// Returns an iterator over the key-value pairs in this hash map.
pub fn iter<'a>(&'a self, process: &'a Process) -> impl Iterator<Item = (Ptr<K>, Ptr<V>)> + '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::<K>() 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<u32, Error> {
self.read_at_offset(0x2C, process)
}
}
3 changes: 3 additions & 0 deletions src/game_engine/godot/core/templates/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod hash_map;

pub use hash_map::*;
11 changes: 11 additions & 0 deletions src/game_engine/godot/cpp/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
70 changes: 70 additions & 0 deletions src/game_engine/godot/cpp/ptr.rs
Original file line number Diff line number Diff line change
@@ -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<T>(Address64, PhantomData<fn() -> T>);

impl<T> fmt::Debug for Ptr<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}*: {}", type_name::<T>(), self.0)
}
}

impl<T> Copy for Ptr<T> {}

impl<T> Clone for Ptr<T> {
fn clone(&self) -> Self {
*self
}
}

// SAFETY: The type is transparent over an `Address64`, which is `Pod`.
unsafe impl<T: 'static> Pod for Ptr<T> {}

// SAFETY: The type is transparent over an `Address64`, which is `Zeroable`.
unsafe impl<T> Zeroable for Ptr<T> {}

impl<T> Ptr<T> {
/// 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<T, Error>
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<U, O>(self, offset: O, process: &Process) -> Result<U, Error>
where
U: CheckedBitPattern,
Address64: Add<O, Output = Address64>,
{
process.read(self.0 + offset)
}

/// Casts this pointer to a pointer of a different type without any checks.
pub fn unchecked_cast<U>(self) -> Ptr<U> {
Ptr::new(self.0)
}

/// Returns the address that this pointer points to.
pub fn addr(self) -> Address64 {
self.0
}
}
37 changes: 37 additions & 0 deletions src/game_engine/godot/cpp/type_info.rs
Original file line number Diff line number Diff line change
@@ -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<VTable>::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<TypeInfo> {
/// 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<const N: usize>(
self,
process: &Process,
) -> Result<ArrayCString<N>, 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<const N: usize>(
self,
mangled_name: &[u8; N],
process: &Process,
) -> Result<bool, Error> {
Ok(self.get_mangled_name::<N>(process)?.matches(mangled_name))
}
}
24 changes: 24 additions & 0 deletions src/game_engine/godot/cpp/vtable.rs
Original file line number Diff line number Diff line change
@@ -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<VTable> {
/// 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<Ptr<TypeInfo>, Error> {
self.read_at_offset(-8, process)
}
}
45 changes: 45 additions & 0 deletions src/game_engine/godot/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Loading

0 comments on commit 340daa6

Please sign in to comment.