Mono and IL2CPP
Unity compiles a game's C# code with one of two scripting backends, and each mod build targets one of them. The SDK detects the backend from the game folder: GameAssembly.dll next to the executable indicates IL2CPP, and a <game>_Data/Managed folder containing DLLs indicates Mono. Lustral's API is identical on both; the differences concern how mods interact with the game's own code.
| Mono | IL2CPP | |
|---|---|---|
| Game code | .NET assemblies in <game>_Data/Managed | Native code in GameAssembly.dll |
| Mods compile against | The game's assemblies | Interop assemblies generated by Lustral in Lustral/interop |
| Mods run on | The game's Mono runtime | .NET 10, shipped by Lustral in Lustral/dotnet |
| Target framework | .NET Standard 2.1 | .NET 10 |
| Library isolation | Shared by all mods (single application domain) | Per mod (one load context per mod) |
| Debugger | Mono soft debugger (Unity debugger in the IDE) | The IDE's .NET debugger |
The SDK configures all of the above. Do not set <TargetFramework> in the project. To build a mod for both backends from one project, see Multi-Backend Mods.
Mono Games
The game's assemblies are standard .NET assemblies: mods reference their types directly and use regular string, List<T> and array types. Use a decompiler such as ILSpy or dnSpy to browse the game's code.
All mods share the game's runtime, so if two mods ship different versions of the same library, only one version is loaded. Ship libraries only when necessary, or share them through a library mod.
IL2CPP Games
IL2CPP games contain no .NET code to reference. On its first launch with Lustral, the game's interop assemblies are generated: .NET assemblies with the same types and members as the game's code, whose methods call into the native code. They are generated by Il2CppInterop into Lustral/interop and regenerated after each game update. Mods compile against these assemblies. Opening Lustral/interop/Assembly-CSharp.dll in ILSpy shows every class, field and method signature of the game (method bodies only contain calls into native code).
Interop types behave much like the game's original types, with the differences described below.
Naming
- Unity types keep their names:
UnityEngine.GameObject,UnityEngine.Transform. - .NET types used by the game are in the
Il2CppSystemnamespace:Il2CppSystem.String,Il2CppSystem.Collections.Generic.List<T>,Il2CppSystem.Action. - The game's namespaces are prefixed with
Il2Cpp:Assets.Scripts.Unity.PlayerbecomesIl2CppAssets.Scripts.Unity.Player, and types without a namespace are placed inIl2Cpp.
using Il2Cpp; // game types without a namespace
using Il2CppAssets.Scripts.Unity; // the game's Assets.Scripts.Unity namespaceStrings and Primitive Types
Game strings are of type Il2CppSystem.String, and many members use Il2CppSystem.Int32, Il2CppSystem.Boolean and similar types. All of them convert implicitly to and from the corresponding .NET types:
var host = new GameObject("My mod"); // string to Il2CppSystem.String
string name = host.name; // Il2CppSystem.String to string
int count = transform.childCount; // Il2CppSystem.Int32 to intTo use .NET string methods (Contains, Split, formatting), assign the value to a string first.
Collections
Game collections are IL2CPP collections: Il2CppSystem.Collections.Generic.List<T>, Dictionary<TKey, TValue>, and arrays of type Il2CppArrayRank1<T>. They provide the usual members (Count, indexers, Add, enumeration), and arrays convert to and from .NET arrays with an explicit cast:
var vertices = (Vector3[])mesh.vertices;
mesh.vertices = (Il2CppArrayRank1<Vector3>)vertices;Type Checks and Casts
Objects returned by the game are wrapped according to their actual class, so is, as and casts work as usual:
if (component is Rigidbody body)
body.isKinematic = true;Delegates
Where the game expects a delegate (an event or callback), pass a .NET delegate; it is converted automatically:
SceneManager.add_sceneLoaded((Action<Scene, LoadSceneMode>)OnSceneLoaded);Events are exposed as add_ and remove_ methods on interop types.
Custom Classes
Classes that Unity must recognize as components, or that the game calls into, must also exist on the IL2CPP side. See Custom Components.
Stripped Code
IL2CPP builds only include the code the game uses; methods, constructors and classes the game never calls are absent. When generating the interop assemblies, Lustral restores as much as possible ("unstripping") from Unity's own libraries for the game's exact Unity version, which are downloaded once per Unity version from Lustral.UnityLibraries. This restores Unity's C# code removed by the build, such as Vector3, Color and Quaternion operations, Mathf, overloads and helper methods built on members the game includes. The restored members are identical on every user's machine.
The following remain unavailable and are reported by the compiler:
- Members implemented in the engine's native code that the game does not include, and C# members that depend on them. Il2CppInterop cannot yet restore these reliably, so Lustral omits them rather than generating members that fail at runtime. Lustral's own events, coroutines and asset bundles do not depend on them.
- Unity classes that the build excludes entirely, such as
AssetBundlein many games, and members that use them. - Generic methods and collections instantiated with value types the game never uses, such as a
List<T>of a struct the game never stores in a list.
If a required member is unavailable, look for an alternative in the interop assemblies: another overload, a non-generic variant or a different API. If the libraries cannot be downloaded (no internet connection, or a Unity version that has not been published yet), the interop assemblies are generated without unstripping, and Lustral retries one day later.
Game Updates
Lustral detects game updates and regenerates the interop assemblies on the next launch, displaying a progress window. If the game's classes changed, rebuild the mod against the new assemblies; the compiler reports any member that no longer exists.