Skip to content

Add explicit static game registration for Skyrim - #698

Open
Illustar0 wants to merge 3 commits into
Mutagen-Modding:devfrom
Illustar0:feat/aot-skyrim-static-registration
Open

Illustar0 wants to merge 3 commits into
Mutagen-Modding:devfrom
Illustar0:feat/aot-skyrim-static-registration

Conversation

@Illustar0

@Illustar0 Illustar0 commented Sep 30, 2026 •

Copy link
Copy Markdown

Related to #696

This is the first step toward Native AOT support. It introduces an explicit static registration path for Skyrim.

For now, consumers need to call GameRegistration.Register(); before using APIs that rely on game registration or mappings.

It passes my local Native AOT tests as well as tests against a real-world use case.

The three DynamicDependency annotations are currently required, though it may be possible to remove them once the later stages of the Native AOT work are complete.

I’d also like to add proper Native AOT smoke-test infrastructure, but I think the way that should be organized is something that should be decided by the maintainers. Personally, I’d probably lean toward using tools such as NUKE / Fallout to orchestrate the tests.

@Illustar0 Illustar0 changed the title feat(skyrim): add explicit static game registration Add explicit static game registration for Skyrim Oct 2, 2026
@Illustar0

Illustar0 commented Oct 2, 2026 •

Copy link
Copy Markdown
Author

@Noggog I think this one is probably worth taking a closer look at when you have time.

GameRegistrations is likely going to be used by quite a few of the follow-up AOT changes, so I'd rather make sure the overall approach makes sense before building too much on top of it.

@Noggog Noggog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For now, consumers need to call GameRegistration.Register(); before using APIs that rely on game registration or mappings.

Not sure i'd like to put that burden on without some serious discussion that there aren't ways to avoid this. If we're gonna do this, i think we do it right so that it's seamless to existing users of the library.

Is this our endgame plan to require this call? Or is there an idea in mind to avoid it we're planning on doing as a followup? Little bit of research suggests we can maybe make use of [ModuleInitializer] concepts?

internal static class GameRegistrationInit
{
    [ModuleInitializer]
    internal static void Init() => GameRegistration.Register();
}

Sounds like AOT apps call this function automatically somehow? Not too familiar.

Comment thread Mutagen.Bethesda.Core/Plugins/Cache/Internals/OverrideMaskRegistrations.cs Outdated
@Illustar0

Illustar0 commented Oct 6, 2026 •

Copy link
Copy Markdown
Author

Thanks for the review!

Is this our endgame plan to require this call? Or is there an idea in mind to avoid it we're planning on doing as a followup?

No, I don't consider an explicit GameRegistration.Register() call to be the endgame if the goal is for the AOT-safe path to eventually replace the reflection path seamlessly. That said, given how significantly the two paths differ, I doubt we can actually achieve a seamless migration. If we can't, the explicit GameRegistration.Register() call would be my preferred approach.

The key difference is that the reflection path auto-discovers games at runtime, whereas the AOT-safe path inherently requires static reachability, so runtime discovery is heavily constrained under Native AOT. This is a real behavioral change, not just an implementation detail. It's also why I wanted to explicitly separate the two paths at the current stage: the AOT-safe path doesn't fully work yet, so this lets us tell consumers that AOT support is still incomplete and that, if they opt in, they need to call GameRegistration.Register() explicitly.

Little bit of research suggests we can maybe make use of [ModuleInitializer] concepts?

As for hiding the call, I did consider a library-side [ModuleInitializer], and it would certainly work. However, Microsoft says it should not be used in libraries (CA2255) and recommends explicit registration instead. It's also not friendly to trimming. So I'm honestly not sure it's a good idea, lol.

Another possibility, once AOT compatibility is further along, is a source generator that emits the registration call in the consuming application. I haven't verified this yet. A generator can only emit code rather than invoke it, so it would most likely have to emit a [ModuleInitializer] into the consumer's own assembly.

That's arguably more acceptable than having the library do it (CA2255 targets libraries). It would also have an extra benefit: the registration would happen at an appropriate time during the consumer's startup, without them having to think about it (unless the consumer has multiple [ModuleInitializer] methods and some of them depend on Mutagen, since their relative order is unspecified).

It would still need to be idempotent, though, and its interaction with trimming would need testing. It also raises a new problem: if the consumer is itself a library, we would just push CA2255 onto them.

Happy to hear your thoughts on whether a seamless route seems worth pursuing.

@Illustar0
Illustar0 requested a review from Noggog October 6, 2026 05:09

@Noggog Noggog left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, if GameRegistration.Register() is just needed for people opting into AOT, then that's okay. As long as typical users like Synthesis patchers not using AOT still work out of the box.

If that's the case, then we can just punt for now and solve the seamlessness for AOT users as a followup

@Illustar0

Copy link
Copy Markdown
Author

Ah, that’s exactly what I meant! So, are we good to merge this PR?

That said, I think the current API works fine as an internal implementation detail, but feels a bit awkward as a public API. Would you like me to follow up with another PR that adds a public registration/initialization API (in Mutagen.Bethesda) along these lines and makes the game-specific GameRegistration types internal?

MutagenInitializer.Initialize(Game.Skyrim);
MutagenInitializer.Initialize(Game.Starfield);
MutagenInitializer.Initialize(Game.Skyrim, Game.Starfield);
MutagenInitializer.Initialize(Game.All);

The idea is for each Game.* value to be a handle backed by a statically bound Action delegate. Unlike a switch over GameCategory, each individual handle would only statically reference its own game’s registration code, allowing unused registration paths to be trimmed away. Game.All would explicitly reference all supported games.

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