A large share of modern Unity games ship IL2CPP rather than Mono — Genshin, Hollow Knight: Silksong, Among Us, most post-2020 commercial Unity titles. UnityScope must cover both for "premier agentic pipeline for modders" to mean anything.
This is a planning doc. No IL2CPP code is shipped yet.
| Concern | Mono | IL2CPP |
|---|---|---|
| Loader | BepInEx 5 (Mono) or BepInEx 6 | BepInEx 6 (IL2CPP) |
| Plugin base class | BaseUnityPlugin from BepInEx.dll |
BasePlugin from BepInEx.IL2CPP.dll |
| Game types | Standard .NET reflection works | Must go through Il2CppInterop proxy assemblies |
| Component lookup | gameObject.GetComponent<Foo>() |
Same API — but Foo is the proxy, not the C++ type |
| Field/property reads | Direct via reflection | Through Il2CppInterop's generated wrappers |
| MonoBehaviour subclassing | Native | Requires [RegisterTypeInIl2Cpp] and Il2Cpp object lifecycle |
| Coroutines | Native | Wrapped via Il2CppSystem.Collections.IEnumerator |
| Sockets / threading / file I/O | Host CLR — works | Host CLR — works (transport layer untouched) |
The transport layer (HTTP, named pipe, discovery file, auth token, request router, main-thread dispatcher, JSON writer) runs in the host CLR — none of it changes for IL2CPP. That's most of the code.
What does change: anything that touches game objects via reflection. Today that's SceneEndpoint and (when implemented) HierarchyWalker, ComponentSerializer, TypeHandlerRegistry, find selectors, invoke.
Introduce an IInspector seam under UnityScope.Runtime/Inspection/. Two implementations:
MonoInspector—System.Reflectionagainst game types directly.Il2CppInspector—Il2CppInterop.Runtimecalls; usesIl2CppType.Of<T>(), navigates proxy assemblies generated byIl2CppAssemblyUnhollower/Il2CppInterop.
Build outputs:
UnityScope.Runtime.Mono.dll— referencesBepInEx.dll,MonoInspectoronly.UnityScope.Runtime.IL2CPP.dll— referencesBepInEx.IL2CPP.dll+Il2CppInterop.Runtime.dll,Il2CppInspectoronly.
Shared code (Transport/, Server/, Endpoints/, Json/) compiles into both via shared csproj <Compile Include> globs. One solution, two plugin DLLs, one wire format. Mod authors install whichever matches their target game's BepInEx variant.
Today's plan is for games to ship adapter packages registering custom extractors (the way blackout-access knows about BlackoutText/BlackoutButton/BlackoutDropdown). For IL2CPP games, those adapters reference the game's proxy assemblies (the unhollowed DLLs in BepInEx/interop/), not the original. Same registration API, different reference targets. Adapter authors don't write IL2CPP-specific glue — they just [hint path] the unhollowed assembly instead of the original.
- Method invocation (
POST /invoke) — IL2CPP makes calling generic methods, methods with ref/out parameters, or methods on inner generic types painful. Spike will start with no-arg / simple-arg methods only. - Field setting on value types — IL2CPP value-type semantics through proxies are fiddly; skip until requested.
- Performance —
Il2CppInteropcalls have marshal overhead./treewalks of huge hierarchies will be slower than Mono. Pagination already in the design absorbs most of this. - First-call latency — proxy assemblies are JIT'd / loaded on demand. A second snapshot is much faster than the first; document this so agents don't think the plugin is broken.
Don't build IL2CPP until Mono spike works and proves the agent-loop is genuinely useful. Then:
- Extract
IInspectorinterface; refactorMonoInspectorout ofSceneEndpointetc. (Should be a small change because the codebase is small.) - Add
UnityScope.Runtime.IL2CPP.csprojreferencing BepInEx 6 IL2CPP + Il2CppInterop. - Implement
Il2CppInspectorfor/sceneand/treefirst. Validate against a free IL2CPP game (e.g. Among Us on Steam, or a clean Unity demo build to IL2CPP). - Iterate per endpoint.
Ship both DLLs in one release, plus a one-paragraph "which one do I install" decision tree in the README. Detection heuristic (for the install script later): if <GameDir>\GameAssembly.dll exists → IL2CPP; if <GameDir>\<Name>_Data\Managed\Assembly-CSharp.dll exists → Mono.
- BepInEx 6 IL2CPP is still in beta as of late 2025 — do we pin a specific bleeding-edge version, or wait for a stable tag?
- Do we want the same plugin GUID across Mono and IL2CPP builds (simpler discovery) or separate (clearer install)?
- Should the discovery file include
runtime: "mono" | "il2cpp"so MCP tools can warn agents about feature differences (e.g./invokelimitations on IL2CPP)?