Skip to content

Refactor SceneManager / Transform; fix a number of bugs in scene/game object traversal - #133

Draft
mitchell-merry wants to merge 42 commits into
LiveSplit:masterfrom
mitchell-merry:transform-refactor
Draft

mitchell-merry wants to merge 42 commits into
LiveSplit:masterfrom
mitchell-merry:transform-refactor

Conversation

@mitchell-merry

@mitchell-merry mitchell-merry commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Another one of the changes I made to asr that I need to upstream...

Why?

I'm now more and more often traversing the gameobject tree in my autosplitters. This has culminated in being able to get a field on a component of a gameobject in a scene: https://github.com/mitchell-merry/autosplitters-wasm/blob/94fbcef38134317b3a5bd7e549a483f18bed17c4/cuphead/src/memory.rs#L170 which does wonders for what's possible.

I have been maintaining a fork with a bunch of tweaks to asr to make this possible, and it's about time I upstream some of it.

  • Concepts of Scene/SceneManager and Transform/GameObject are a little too interwoven
    • I wanted to access GameObject#activeInHierarchy, but it doesn't really make sense to expose that on the Transform directly to me. It's a feature of GameObjects - what if you don't have a transform?
    • get_game_object_from_dont_destroy_on_load doesn't really make sense as a specific function, since we can already get the DDoL scene + game objects on arbitrary scenes
  • Existing logic has some bugs
    • We have klass_name in the SceneManager offsets, but this doesn't really make sense there, since that's strictly to do with the version of Mono/IL2CPP
    • When traversing root game objects, it actually traverses in reverse

What?

  • Split operations on a Scene into the Scene struct rather than being on SceneManager
    • root_game_objects, get_root_game_object
    • Removed get_game_object_from_dont_destroy_on_load (replacement is to get the DDoL scene then call get_root_game_object, like any other scene)
  • Scene#root_game_objects now reads next instead of prev, so the objects are read in the correct order
  • Add Transform#find_transform which traverses the game object tree for you via a path of game object names
  • Split Transform into GameObject & Transform
    • Move Transform#get_name logic to GameObject#get_name, add Transform#get_game_object to Transform and make Transform#get_name use the above two
    • Move Transform#classes and Transform#get_class to GameObject
  • Transform#get_class split into GameObject#get_class_mono and GameObject#get_class_il2cpp since the offset of klass.name depends on the mono version / il2cpp version, same way as it does in the regular path dereferencing code
  • Add GameObject#is_active_in_hierarchy and GameObject#is_active_self, which are self-explanatory

And then a couple things to make this work for Cuphead:

  • Support getting the SceneManager from the main module (for old versions of Cuphead)
  • Logic to get the mono object changed between some versions of mono (? or unity), new switch supports this - needs more testing
  • Allow manual injecting of offsets, this is intended to be temporary while we properly support everything

Testing

TODO:

  • Test on Mono versions
  • Test on IL2CPP versions
    • since the offsets change here for class name especially.


impl Class {
pub(super) fn get_name<const N: usize>(
pub fn get_from_component(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"component" isn't accurate here, but there's something in between the component and the class. not exactly sure what it is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ ] TODO: figure out what it is

}))
}

// TODO it's really dumb i have to split this by mono/il2cpp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we really need to implement some sort of strategy pattern here. The amount of code duplication is getting ridiculous

game_object_activeself: 0x5E,
game_object_activeinhierarchy: 0x5F,
klass: 0x28,
klass_name: 0x48,

@mitchell-merry mitchell-merry May 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes depending on the mono version, which, of course it does.

Now we just store the offset to the object handle, and you have to use mono/il2cpp specific code to get the component

/// (and so on), as well as a list of `Component`s, which are classes (eg.
/// `MonoBehaviour`) containing data we might want to retrieve for the auto
/// splitter logic.
fn root_game_objects<'a>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more conceptual sense to live on the Scene rather than the SceneManager


/// Tries to find the specified root [`Transform`] from the
/// `DontDestroyOnLoad` Unity scene.
pub fn get_game_object_from_dont_destroy_on_load(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no replacement for this - get the DDoL scene from SceneManager, then call get_root_game_object as with any other Scene


iter::from_fn(move || {
// TODO check if this is correct on other games
let [_prev, next, current]: [Address; 3] = match scene_manager.pointer_size {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: there's a change here, previously we were using [first, _, third] where first was our next element.

That's wrong: https://gist.github.com/just-ero/92457b51baf85bd1e5b8c87de8c9835e#file-list-hpp it actually causes us to go in the reverse order in the list. That caused me some issues in Cuphead

@mitchell-merry mitchell-merry changed the title Transform/GameObject refactors Refactor SceneManager / Transform; fix a number of bugs in scene/game object traversal May 11, 2026
v_table: MonoVTableOffsets { vtable: 0x48 },
}),
(BinaryFormat::ELF | BinaryFormat::MachO, Version::V3, PointerSize::Bit64) => {
Some(&Self {

@mitchell-merry mitchell-merry Jun 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this indented a level... I didn't change any offsets here.

const SIG_64_BIT_PE_1: Signature<13> =
Signature::new("48 83 EC 20 4C 8B ?5 ?? ?? ?? ?? 33 F6");
const SIG_64_BIT_PE_2: Signature<13> =
Signature::new("48 83 EC 20 48 8B 2D ?? ?? ?? ?? 33 F6");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: In my testing, newer versions of unity (Unity 6000.4.5f1 specifically) didn't work with the old sig, so I just added this new one

/// FIXME: This function is a bandaid while we don't have support for all
/// scene manager versions in asr. We should remove this by supporting
/// those versions without override.
pub fn attach_with_offsets(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Since getting all of the offsets on every unity version is quite a large task, I'm opting to just support passing in offsets manually for now.


(pointer_size, base_address)
} else {
// If it's not in UnityPlayer, then it's in the main module (old unity versions)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I'm also adding support for older versions of Unity in this PR (well except for the offsets, which have to be overridden, which I'll do in later PRs)

.ok_or(Error {})
}
// FIXME: Untested on V2
mono::Version::V2 | mono::Version::V3 => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: previous logic only ever did the equivalent of the V1 branch - I found that at least V3 (or like, newer versions, don't know if it's actually split on mono versions) have an extra dereference

@ero-qt

ero-qt commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Hey! Wanna flag some overlap with some of my open PRs:

#165 replaces the hand-kept scene manager offsets with a layout measured per Unity version. That covers 2 of the things this PR carries per version: where klass.name sits, and how a game object's managed object is reached. If #165 merges first, those switches go away here. Your split would then sit on top of the measured builds.

There's also a harness now (#168, #169). It runs the real Unity players from the fixtures repo and checks what asr reads against a known scene, over 19 versions, both 32-bit and 64-bit, Mono and IL2CPP. Two items on your list it could settle:

  • The reversed root order. Our fixture scene has only one root object, so there's no order to check. I'll add a second one, then a test can catch it.
  • activeSelf and activeInHierarchy. The fixture already has an object switched off with a child switched on, Hidden and Quux under Baz. Both flags are checkable the moment the API hands them over.

Let me know what you'd prefer. I'm happy to do the fixture side either way. The API split is yours.

@mitchell-merry

Copy link
Copy Markdown
Contributor Author

So was this comment written with AI? Can you speak to me like a human? I thought I'd only have to deal with that at work.

I don't think it matters much what I do here. I've had PRs open in this repo for ages that haven't been reviewed despite my incessance, and seeing the number you've raised recently I'm fairly sure they're all going to clobber mine anyways.

I think the last thing this change needed was checking all versions of unity, so if you've got all the offsets that's great and makes this a lot easier.

Is this work one that we would actually like to eventually merge, or is it all changing with your work?

@ero-qt

ero-qt commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Yes, this should be merged.

#165, #166 and #167 don't touch your API split: Scene owning its roots, GameObject apart from Transform, find_transform, active flags. They only replace the per-version offsets you're introducing here. Should #165 merge first, I'm happy to help with the conflicts.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants