Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 8 commits into from

Conversation

Jujstme
Copy link
Contributor

@Jujstme Jujstme commented Sep 24, 2023

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
Copy link
Contributor

AlexKnauth commented Sep 26, 2023

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
Copy link
Contributor Author

Jujstme commented Sep 27, 2023

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
Copy link
Contributor

AlexKnauth commented Sep 27, 2023

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
Copy link
Contributor

AlexKnauth commented Sep 27, 2023

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
Copy link
Contributor

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
Copy link
Contributor Author

Jujstme commented Sep 27, 2023

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.

@Jujstme Jujstme marked this pull request as draft September 27, 2023 06:33
@Jujstme Jujstme marked this pull request as ready for review September 27, 2023 20:19
@AlexKnauth
Copy link
Contributor

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: Jujstme/asr@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
Copy link
Contributor Author

Jujstme commented Oct 8, 2023

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 Jujstme marked this pull request as draft October 8, 2023 06:06
@Jujstme
Copy link
Contributor Author

Jujstme commented Oct 8, 2023

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 Jujstme marked this pull request as ready for review October 8, 2023 16:19
@Jujstme Jujstme marked this pull request as draft October 15, 2023 09:48
@Jujstme
Copy link
Contributor Author

Jujstme commented Oct 15, 2023

Closing this as the changes are included in #67

@Jujstme Jujstme closed this Oct 15, 2023
@Jujstme Jujstme deleted the unity branch October 15, 2023 13:58
@Jujstme Jujstme restored the unity branch October 19, 2023 21:32
@Jujstme Jujstme deleted the unity branch December 25, 2023 13:04
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