LiveSplit / asr

Helper crate to write auto splitters for LiveSplit One's auto splitting runtime.
https://livesplit.org/asr/asr
Apache License 2.0
10 stars 10 forks source link

[Unity] Automatic pointer path resolution for Mono games #60

Closed Jujstme closed 11 months ago

Jujstme commented 1 year ago

This commit adds a new Pointer struct that allows for automatic resolution of pointer paths in Unity games, instead of relying to calling get_class(), get_field(), get_parent() or get_static_table() separatedly, simplifying the writing of autosplitters.

It is needed to specify the depth when creating a new instance. For example, Pointer<2> will dereference the path up to the 2nd offset.

The try_find() function is safe to leave in the main update loop, as it will immediately return if a pointer path has been found already. This serves as a workaround to solve issues with some Mono classes not getting instantiated when the game starts, which might block the execution of the autosplitter.

Example:

game.until_closes(async {
    let unity = &unity::mono::Module::wait_attach(&game, unity::mono::Version::V3).await;
    asr::print_message("Attached Unity module");

    let image = &unity.wait_get_default_image(&game).await;
    asr::print_message("Attached Assembly-CSharp.dll");

    let mut loaded_was_success = unity::mono::Pointer::<2>::new();

    loop {
        loaded_was_success.try_find(game, unity, image, "SystemManager", 1, &["_instance", "InitWasLoaded"]);

        if let Ok(val) = loaded_was_success.read::<u8>(game) {
            asr::print_limited::<8>(&"Success!");
        }

        next_tick().await;
    }
})
.await;
AlexKnauth commented 1 year ago

For the purposes of using functions like this with ? question-mark notation, a return type of Option<()> would be more convenient for me than a return type of bool. This isn't a big deal since if !boolfn() { return None; } isn't that much worse than optfn()?;, and bools have better support for more complex conditions with &&, ||, etc. than options currently do.

Edit: and if I'm using .read with ?, then I could just ignore the bool result and use .read(...).ok()? just fine

Jujstme commented 1 year ago

Either way should probably be fine. I'm just not sure which is the most sound approach. Ideally I guess we should just include the parameters in the new() and just call update() as a function with no return type. But storing strings can be a pain if we want to stay in a no_std noalloc environment.

I'll see what I can do later.

AlexKnauth commented 1 year ago

To stay within no alloc, would it require a second const type parameter for the string capacity of an ArrayString that it can use to store it?

A new function like that could take &str for user convenience, then internally use ArrayString::from to store them. Though I suppose the user would still have to specify the capacity.

Edit: another idea: could &'static str work? it would be less flexible, but the most common use case would be to use string literals anyway

AlexKnauth commented 1 year ago

A new(...parameters...) + update() design like that could also make an improved version of read possible which can just call update if the static_table is None, instead of giving up.

AlexKnauth commented 1 year ago

For storing multiple Pointer values in an array or a map, it would be helpful if the const N type parameter were a capacity, not a length. If some of the Pointer values I want to store in that array are 2 offsets long, and others I want to store in the same array are 3 offsets long, it would be nice if I could just make them all Pointer<3>, with 3 as the capacity, and some would have length 2 and some length 3.

Jujstme commented 1 year ago

To stay within no alloc, would it require a second const type parameter for the string capacity of an ArrayString that it can use to store it?

A new function like that could take &str for user convenience, then internally use ArrayString::from to store them. Though I suppose the user would still have to specify the capacity.

Edit: another idea: could &'static str work? it would be less flexible, but the most common use case would be to use string literals anyway

No, A second const isn't really required, as all the code so far assumes ArrayCString<128>. It should be possible to take the &str directly though, provided we specify a lifetime. It should not be necessary to specify &'static, a normal generic lifetime parameter should be enough.

Also the idea of using a normal constructor with new() and then automatically resolving the offsets (if required) with read() seems a much more sound idea. I will implement these changes later today.

AlexKnauth commented 11 months ago

I think this would be better if it didn't require a lifetime parameter 'a in the type.

I see 2 ways to avoid it, either using alloc, or storing in fixed-capacity structures like array strings. I've explored one way to store with fixed-capacity in a diff here: https://github.com/Jujstme/asr/compare/unity...AlexKnauth:asr:unity-2

I also think it would be better if the N in the type acted as a capacity, not a length. Currently fields.len() must be exactly equal to N as a length, but if N is a capacity then fields.len() could be less than or equal to N.

That would allow a pointer with a type like Pointer<4> to be created that only accessed 3 fields deep, for example, to put in an array or other homogeneous data structure next to other Pointer<4> instances that need to access 4 fields deep.

Jujstme commented 11 months ago

Although I believe the lifetime parameter is not a big issue in this specific case (they're never gonna change anyway, as long as the game isn't closed), you're right, but I didn't have time to think a lot about it. Actually I'll be reverting this to draft in the meantime.

I REALLY don't want to use alloc unless absolutely needed, as all the rest of the stuff we implement for Unity is not using it.

Jujstme commented 11 months ago

The code has been rewritten again. This time N is just a capacity, not a length, with the only limitation that it must be higher than the number of offsets we are dereferencing.

I'm kinda abusing ArrayString in order to store the path without needing to reference anything. The final read function will still need to include module and image but it doesn't look that bad to be fair.

image

Jujstme commented 11 months ago

Closing this as the changes are included in https://github.com/LiveSplit/asr/pull/67