diff --git a/src/game_engine/unity/mono/class.rs b/src/game_engine/unity/mono/class.rs
index 2ffe93c6..2d302eb5 100644
--- a/src/game_engine/unity/mono/class.rs
+++ b/src/game_engine/unity/mono/class.rs
@@ -135,7 +135,56 @@ impl Class {
.await
}
- fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option
{
+ /// Reads the class of an object found in memory.
+ ///
+ /// Every managed object begins with a pointer to its class's vtable, and a
+ /// vtable begins with a pointer to its class, so an object can be resolved
+ /// back to a [`Class`] whose fields are then available by name.
+ ///
+ /// This is the only way to reach a generic instantiation such as
+ /// `HashSet`. Each instantiation is a distinct class with its own
+ /// field offsets, and none of them can be looked up in an
+ /// [`Image`](super::Image) by name -- but any instance points at its own.
+ ///
+ /// # Inflated generics may have no field names
+ ///
+ /// Mono fills a class's field table in lazily, and for an inflated generic
+ /// nothing necessarily has. [`get_field_offset`](Self::get_field_offset)
+ /// can therefore return [`None`] for every field of a class resolved this
+ /// way, against an object that is plainly an instance of it -- and the
+ /// same lookup may succeed against another process running the same build,
+ /// so it is runtime state rather than anything a version check could
+ /// predict. Code that must not fail on a collection needs a fallback to
+ /// the known layout, guarded by a check that the object agrees with it.
+ pub fn of_object(process: &Process, module: &Module, object: Address) -> Option {
+ // `MonoVTable::klass` is the first member in every version Mono has
+ // shipped, so unlike the other offsets in this module it needs no
+ // version table. It is the same invariant an object's own header
+ // relies on.
+ let vtable = process
+ .read_pointer(object, module.pointer_size)
+ .ok()
+ .filter(|addr| !addr.is_null())?;
+ let class = process
+ .read_pointer(vtable, module.pointer_size)
+ .ok()
+ .filter(|addr| !addr.is_null())?;
+ Some(Self { class })
+ }
+
+ /// Returns the address of this class's `MonoVTable` in the first domain.
+ ///
+ /// Every managed object begins with a pointer to the vtable of its class,
+ /// so this doubles as an identity handle for the class: an object at
+ /// `addr` is an instance of this exact class if and only if the pointer at
+ /// `addr` equals this value.
+ ///
+ /// This is useful for games where the object of interest cannot be reached
+ /// by walking static fields. Games built around constructor-injection
+ /// dependency injection often have no static roots at all, which makes
+ /// [`UnityPointer`](super::UnityPointer) inapplicable, and the only way to
+ /// find a service is to scan the heap for the instance of its class.
+ pub fn get_vtable(&self, process: &Process, module: &Module) -> Option {
let runtime_info = process
.read_pointer(
self.class + module.offsets.class.runtime_info,
@@ -144,10 +193,14 @@ impl Class {
.ok()
.filter(|addr| !addr.is_null())?;
- let mut vtables = process
+ process
.read_pointer(runtime_info + module.size_of_ptr(), module.pointer_size)
.ok()
- .filter(|addr| !addr.is_null())?;
+ .filter(|addr| !addr.is_null())
+ }
+
+ fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option {
+ let mut vtables = self.get_vtable(process, module)?;
// Mono V1 behaves differently when it comes to recover the static table
match module.version {