Skip to content

Commit

Permalink
Add Support for Unity Games (#36)
Browse files Browse the repository at this point in the history
This adds support for easily reading values from Unity games. It has
support for both the Mono and IL2CPP backens. Support for Unity is
available via the `unity` crate feature.

# Example

```rust
use asr::{
    future::retry,
    game_engine::unity::il2cpp::{Module, Version},
    Address, Address64,
};

// We first attach to the Mono module. Here we know that the game is using IL2CPP 2020.
let module = Module::wait_attach(&process, Version::V2020).await;
// We access the .NET DLL that the game code is in.
let image = module.wait_get_default_image(&process).await;

// We access a class called "Timer" in that DLL.
let timer_class = image.wait_get_class(&process, &module, "Timer").await;
// We access a static field called "_instance" representing the singleton
// instance of the class.
let instance = timer_class.wait_get_static_instance(&process, &module, "_instance").await;

// Once we have the address of the instance, we want to access one of its
// fields, so we get the offset of the "currentTime" field.
let current_time_offset = timer_class.wait_get_field(&process, &module, "currentTime").await;

// Now we can add it to the address of the instance and read the current time.
if let Ok(current_time) = process.read::<f32>(instance + current_time_offset) {
    // Use the current time.
}
```

Alternatively you can use the `Class` derive macro to generate the
bindings for you. It is available via the `derive` crate feature. This
allows reading the contents of an instance of the class described by the
struct from a process. Each field must match the name of the field in
the class exactly and needs to be of a type that can be read from a
process.

```rust
#[derive(Class)]
struct Timer {
    currentLevelTime: f32,
    timerStopped: bool,
}
```

This will bind to a .NET class of the following shape:

```csharp
class Timer
{
    float currentLevelTime;
    bool timerStopped;
    // ...
}
```

The class can then be bound to the process like so:

```rust
let timer_class = Timer::bind(&process, &module, &image).await;
```

Once you have an instance, you can read the instance from the process
like so:

```rust
if let Ok(timer) = timer_class.read(&process, timer_instance) {
    // Do something with the instance.
}
```

References:

https://github.com/just-ero/asl-help/tree/4c87822df0125b027d1af75e8e348c485817592d/src/Unity
https://github.com/Unity-Technologies/mono
https://github.com/CryZe/lunistice-auto-splitter/blob/b8c01031991783f7b41044099ee69edd54514dba/asr-dotnet/src/lib.rs
  • Loading branch information
Jujstme committed Jul 17, 2023
1 parent c42e0d4 commit 2b0f32c
Show file tree
Hide file tree
Showing 16 changed files with 1,726 additions and 12 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ float-vars-small = ["float-vars", "ryu/small"]
integer-vars = ["itoa"]
signature = ["memchr"]

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

# Emulators
gba = []
genesis = ["flags", "signature"]
Expand Down
3 changes: 3 additions & 0 deletions asr-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ heck = "0.4.0"

[lib]
proc-macro = true

[features]
unity = []
95 changes: 95 additions & 0 deletions asr-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,98 @@ pub fn from_endian_macro(input: TokenStream) -> TokenStream {
}
.into()
}

#[cfg(feature = "unity")]
mod unity;

/// A derive macro that can be used to bind to a .NET class. This allows reading
/// the contents of an instance of the class described by the struct from a
/// process. Each field must match the name of the field in the class exactly
/// and needs to be of a type that can be read from a process.
///
/// # Example
///
/// ```no_run
/// #[derive(Class)]
/// struct Timer {
/// currentLevelTime: f32,
/// timerStopped: bool,
/// }
/// ```
///
/// This will bind to a .NET class of the following shape:
///
/// ```csharp
/// class Timer
/// {
/// float currentLevelTime;
/// bool timerStopped;
/// // ...
/// }
/// ```
///
/// The class can then be bound to the process like so:
///
/// ```no_run
/// let timer_class = Timer::bind(&process, &module, &image).await;
/// ```
///
/// Once you have an instance, you can read the instance from the process like
/// so:
///
/// ```no_run
/// if let Ok(timer) = timer_class.read(&process, timer_instance) {
/// // Do something with the instance.
/// }
/// ```
#[cfg(feature = "unity")]
#[proc_macro_derive(Il2cppClass)]
pub fn il2cpp_class_binding(input: TokenStream) -> TokenStream {
unity::process(input, quote! { asr::game_engine::unity::il2cpp })
}

/// A derive macro that can be used to bind to a .NET class. This allows reading
/// the contents of an instance of the class described by the struct from a
/// process. Each field must match the name of the field in the class exactly
/// and needs to be of a type that can be read from a process.
///
/// # Example
///
/// ```no_run
/// #[derive(Class)]
/// struct Timer {
/// currentLevelTime: f32,
/// timerStopped: bool,
/// }
/// ```
///
/// This will bind to a .NET class of the following shape:
///
/// ```csharp
/// class Timer
/// {
/// float currentLevelTime;
/// bool timerStopped;
/// // ...
/// }
/// ```
///
/// The class can then be bound to the process like so:
///
/// ```no_run
/// let timer_class = Timer::bind(&process, &module, &image).await;
/// ```
///
/// Once you have an instance, you can read the instance from the process like
/// so:
///
/// ```no_run
/// if let Ok(timer) = timer_class.read(&process, timer_instance) {
/// // Do something with the instance.
/// }
/// ```
#[cfg(feature = "unity")]
#[proc_macro_derive(MonoClass)]
pub fn mono_class_binding(input: TokenStream) -> TokenStream {
unity::process(input, quote! { asr::game_engine::unity::mono })
}
71 changes: 71 additions & 0 deletions asr-derive/src/unity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{Data, DeriveInput, Ident};

pub fn process(input: TokenStream, mono_module: impl ToTokens) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();

let struct_data = match ast.data {
Data::Struct(s) => s,
_ => panic!("Only structs are supported"),
};

let struct_name = ast.ident;
let stuct_name_string = struct_name.to_string();

let binding_name = Ident::new(&format!("{struct_name}Binding"), struct_name.span());

let mut field_names = Vec::new();
let mut field_name_strings = Vec::new();
let mut field_types = Vec::new();
let mut field_reads = Vec::new();
for field in struct_data.fields {
let field_name = field.ident.clone().unwrap();
let span = field_name.span();
field_reads.push(quote_spanned! { span =>
process.read(instance + self.#field_name).map_err(drop)?
});
field_names.push(field_name);
field_name_strings.push(field.ident.clone().unwrap().to_string());
field_types.push(field.ty);
}

quote! {
struct #binding_name {
class: #mono_module::Class,
#(#field_names: u32,)*
}

impl #struct_name {
async fn bind(
process: &asr::Process,
module: &#mono_module::Module,
image: &#mono_module::Image,
) -> #binding_name {
let class = image.wait_get_class(process, module, #stuct_name_string).await;

#(
let #field_names = class.wait_get_field(process, module, #field_name_strings).await;
)*

#binding_name {
class,
#(#field_names,)*
}
}
}

impl #binding_name {
fn class(&self) -> &#mono_module::Class {
&self.class
}

fn read(&self, process: &asr::Process, instance: asr::Address) -> Result<#struct_name, ()> {
Ok(#struct_name {#(
#field_names: #field_reads,
)*})
}
}
}
.into()
}
6 changes: 3 additions & 3 deletions src/emulator/ps1/epsxe.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Address, Process, signature::Signature};
use crate::{signature::Signature, Address, Address32, Process};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct State;
Expand All @@ -14,10 +14,10 @@ impl State {

let ptr = SIG.scan_process_range(game, main_module_range)? + 5;

Some(game.read::<u32>(ptr).ok()?.into())
Some(game.read::<Address32>(ptr).ok()?.into())
}

pub const fn keep_alive(&self) -> bool {
true
}
}
}
2 changes: 1 addition & 1 deletion src/emulator/ps1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Emulator {
};

let Some(ram_base) = self.ram_base else {
return Err(Error {})
return Err(Error {});
};

let end_offset = offset.checked_sub(0x80000000).unwrap_or(offset);
Expand Down
8 changes: 4 additions & 4 deletions src/emulator/ps1/pcsx_redux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ impl State {

pub fn keep_alive(&self, game: &Process) -> bool {
if self.is_64_bit {
let Ok(addr) = game.read::<Address64>(self.addr_base) else { return false };
self.addr == addr.into()
game.read::<Address64>(self.addr_base)
.is_ok_and(|addr| self.addr == addr.into())
} else {
let Ok(addr) = game.read::<Address32>(self.addr_base) else { return false };
self.addr == addr.into()
game.read::<Address32>(self.addr_base)
.is_ok_and(|addr| self.addr == addr.into())
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/game_engine/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Support for attaching to various game engines.

#[cfg(feature = "unity")]
pub mod unity;
Loading

0 comments on commit 2b0f32c

Please sign in to comment.