Skip to content

Instantly share code, notes, and snippets.

@SamboyCoding
Last active April 16, 2024 06:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SamboyCoding/bc1c75ca2d81ea38f3137285f23f9bcc to your computer and use it in GitHub Desktop.
Save SamboyCoding/bc1c75ca2d81ea38f3137285f23f9bcc to your computer and use it in GitHub Desktop.
Some upcoming Core (CLR) changes to MelonLoader

MelonLoader, but CoreCLR

CoreWhoLR?

What if I told you that you could use .NET 6 features in your MelonLoader mods? Even if the game itself wasn't .NET 6 (as most aren't)?

Well, now you (potentially) can! An upcoming MelonLoader release (probably 0.6.0?) will have the runtime for IL2CPP swapped out from Mono to CoreCLR, also known as dotnet/runtime, "the Microsoft Runtime", or "the .NET Runtime".

Basically, it's the official, open source .NET runtime that you can download from https://dot.net.

Debugging!

One big advantage of the new runtime is proper debugger/profiler/etc support.

There are two ways of debugging. First, you can just launch the game via your tool of choice (Rider, dotTrace, dotMemory, VS).

Alternatively, you can now run MelonLoader with --melonloader.launchdebugger, which will call Debugger.Launch() very early in the initialization process.

This should pop up a window allowing you to attach the game to an IDE instance with your project open. Or, you can attach any debugger/tool manually, then click cancel in the dialog to continue loading, now with the tool in question attached.

  • Note, if you want to launch JetBrains Rider from this dialog, you need to first go to Settings -> Debugger -> Set rider as a default debugger in the IDE.

However, regardless of which IDE you choose to use, once you have it set up, breakpoints, and even hot reload (link to a video) should work as you'd expect. Note, for best results with hot reload, you should be using the Debug configuration when building your mod, and disable code optimizations. Also, hot reload of unity managed methods (Update, etc), may not work, but if you call a method from these methods, and hot reload that method, it should work.

And that's not all! If you ship a PDB file with your mod (or even better, set <DebugType>embedded</DebugType> in your csproj, which will embed the PDB inside the DLL file) then you'll get full line numbers in almost any stacktrace that is logged.

Nuget!

MelonLoader now has NuGet packages! This means you can reference MelonLoader assemblies without having to manually download new versions, and tools like Dependabot can alert you to updates. It can be obtained from https://www.nuget.org/packages/LavaGang.MelonLoader

Modern APIs!

Of course, the biggest advantage of switching from .NET framework targeting to .NET 6 targeting, is that you get all the benefits from the last 5 years of development. This includes much wider async support, better zero-allocation data management via the Span family of types, and the string interpolation improvements debuted in .NET 6.

New APIs in MelonLoader

As part of the process of enabling the new runtime, a lot of stuff internally has had to be shifted around, which gave some opportunities for cleanup. As an example, the new MelonEnvironment class gives you convenient access to a bunch of key information, such as what runtime your mod is running in, and key game/ML paths. In addition, the Log APIs now have full RGB colour support, so you can log in any colour you like, not just the predefined ones.

A stricter runtime

The first rule of the new runtime is that it is a LOT stricter than Mono about everything being nice and tidy. This means that some changes in generated DLLs have had to be made (primarily, methods on enums are gone), but in almost every case, this shouldn't affect your development process.

In fact, in most cases (about 95% of mods, from our tests, if not more), everything will be backwards compatible, as long as you recompile your mod - more on this later. There are mechanisms in place to fix up cases where you do something that the new runtime doesn't like (like hook an il2cpp method directly to a managed one without having a delegate to handle the re-enabling of the Garbage Collector - more on this, too, will come later).

A few growing pains

Unfortunately as part of the big shift, you'll almost certainly have to make some hopefully-fairly-painless changes to compile your mods. Namely:

  • The biggest change - we're no longer using Il2CppAssemblyUnhollower, but have switched to a fork called Il2CppInterop which provides better compatibility with both newer .NET runtimes, and newer unity versions. This means you'll have to update all references to unhollower base/runtime lib. Class names haven't changed, though, so referencing the new interop DLLs from the net6 folder, and removing all the old using statements, should be enough for your IDE to pick up and quick-fix the references.
  • As part of this, all non-unity namespaces that come from il2cpp are now prefixed with Il2cpp, like System used to be with Il2cppSystem. This includes game-specific classes from Assembly-CSharp. Again, removing all your using statements, updating the referenced DLLs, and invoking your IDEs quick-fix should be enough to find the new namespaces.
    • This change is primarily for two reasons - clarity as to what is coming from IL2CPP and what isn't, and allowing usage of libraries that were used in the game (e.g. Newtonsoft.Json) without conflicts.
  • You'll have to update your mod to use the new csproj format. Microsoft have a tool to automate this process, in most cases.
  • You'll have to update your target framework to net6.
  • shproj projects are no longer supported. This probably won't affect most people. There are alternatives, though (the <Link> tag)
  • The game folder structure has changed a bit. MelonLoader DLLs are now in a dedicated net6 folder inside the MelonLoader one.
    • Newtonsoft.Json is now in there too, instead of Managed, because it has two versions (one for the old runtime, still used on mono games, and one for the new runtime).
  • Generated IL2CPP assemblies have been moved to a separate folder next to the Managed folder, to reduce confusion between what is actually the game vs MelonLoader itself.
  • Some dependencies may need updating in order for your mod to load. These largely fall into 3 categories.
    • Dependencies that were prevously part of the framework but are not included in the runtime anymore (e.g. System.Windows.Extensions). These can be got from nuget, and packaged in your mod or dropped into UserLibs.
    • 3rd-party dependencies that support newer runtimes, but need to be re-compiled against to include the correct version (e.g. Ionic.Zip)
    • 3rd-party dependencies that do not support newer runtimes. In a lot of cases, if the library was popular enough, it can be simply switched to a fork (e.g. WebSocketSharp).

If you really can't update your csproj, for whatever reason, or you are writing a universal mod that needs to work on mono too, you can use the melonloader assemblies in the net35 folder, which are fundamentally the same as the ones you've always used.

Do note, however, that you can convert your project to the new style, and still target net35 (as MelonLoader itself does), or any other version of .NET, so we recommend you convert if you can, even if you still have to target old frameworks.

Dodgy Native Hooks

One thing you may see in the console is a message like this:

Encountered a dodgy native hook to a managed method in melon [your mod's name]: [method name]. It has been wrapped in a proper unmanaged delegate, but please fix your mod! You also won't be able to detach this hook!

or, in the worse-case scenario, the more scary

=========================================
The mod [your mod] has attempted to detour a native method to a managed one.
The managed method target is [your method]
If this hadn't been stopped, the runtime would have crashed.
Modder: Either create an [UnmanagedFunctionPointer] delegate from your function, and use Marshal.GetFunctionPointerFromDelegate,
or annotate your patch function as [UnmanagedCallersOnly] (target net5.0), and then you can directly use &Method as the hook target.
=========================================

These occur if you call MelonUtils.NativeHookAttach with the second parameter as a direct method reference to a method in your mod class. Because this results in native code jumping straight into C# code, it doesn't do some key setup required to keep things running smoothly. Mono handled this case (either by ignoring the issue or injecting setup code into the start of every method, I'm not sure) but CoreCLR doesn't. As the second message hints, this would actually have just hard-crashed the game (i.e. it would just close with maybe a brief flicker of an obscure error in the console).

If you get the first message, your mod is fine, for now. MelonLoader has cleaned up your call to NativeHookAttach, and everything should just work. However, you really should update it, preferably to Harmony if possible, as it will be quicker - due to the checks required to prevent the runtime from crashing, native hooking now takes at least 500ms per call. If you have the second, more serious log entry, MelonLoader has NOT been able to clean up your call to NativeHookAttach, and has just blocked it, so your hook simply won't be called. You'll need to fix your mod.

As for fixing it, it's fairly simple. If you're able to target .NET 6 (i.e. your mod is not intended to run under mono too), simply annotate your hook method like this:

//You'll need to import `System.Runtime.CompilerServices` and `System.Runtime.InteropServices`
//And you won't be able to call this method from C#.
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]

Then just replace your call to NativeHookAttach with something like the following (your method will need to be unsafe, but it probably is anyway):

delegate* unmanaged[Cdecl]<ParameterType1, ParameterType2, ParameterType3, ReturnType> detourPtr = &myMethod;
MelonUtils.NativeHookAttach((IntPtr) (&targetMethodPtr), (IntPtr) detourPtr);

Alternatively, if that seems scary, or you can't target net6, you can do the following: You don't need any additonal attributes on the method, but you do need an appropriately-annotated delegate, like so:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] //System.Runtime.InteropServices namespace
private delegate ReturnType myCoolDelegate(ParamType1 param1, ParamType2 param2);

Then, replace your patching code with

var detourPtr = Marshal.GetFunctionPointerForDelegate((myCoolDelegate) myCoolHookFunction);
MelonUtils.NativeHookAttach((IntPtr) (&targetMethodPtr), detourPtr);

Additionally, please note that when detaching hooks, you now need to pass in &originalMethod not &trampoline.

Native Stack Walks

Also new in this version of ML is a basic, but functional, method to perform a native stack walk. This should allow you to find out, for example, where a call to a Harmony patch is coming from in an IL2CPP game. Note this is only currently supported on Windows.

This requires several steps of setup. First off, you'll need to put a copy of dbghelp.dll and symsrv.dll in your game directory (next to the executable). These can usually be found in your windows installation or alongside other software (e.g. the Oculus desktop app ships both), or downloaded in standalone form. The licensing on these is a little unclear so we're not shipping them out of the box.

Next you'll need to generate a PDB file for your GameAssembly.dll file. Il2CppAssemblyUnhollower can do this - I want to also support it in Cpp2IL eventually. This, too, should be placed in the game directory, next to the executable.

Finally, you can call NativeStackWalk.GetNativeStackFrames() or get the value of NativeStackWalk.NativeStackTrace from your mod. Note that the first time you call this, a bunch of PDB files (for both windows and unity) will need to be downloaded from various symbol servers, which may hang the process for a few minutes. Hopefully, subsequent calls should be significantly quicker.

Bear in mind this process is far from perfect but it might hopefully be of some use.

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