# Unity Game Modding

> Modify Unity games — save file editing, BepInEx IL2CPP plugin development, IL2CPP metadata analysis, and runtime Harmony patching.

- Skill: `wcpaka-lgtm/unity-game-modding` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add wcpaka-lgtm/unity-game-modding`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wcpaka-lgtm/unity-game-modding/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: wcpaka-lgtm (https://skillmd.com/u/wcpaka-lgtm)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/wcpaka-lgtm/unity-game-modding

---


# Unity Game Modding

## Decision Tree

1. **Can the change be done via save file?** (money, XP, inventory counts)
   → Save editing (fastest, safest). See §Save Editing.
   ⚠️ Some games store saves in the **Windows Registry** (`HKCU\Software\<Dev>\<Game>`) as encrypted blobs — if so, save editing is infeasible; go to step 2.

2. **Does the game use IL2CPP?** (check for `GameAssembly.dll` + `il2cpp_data/Metadata/global-metadata.dat`)
   - **Simple patch** (disable a method, force return true/false): → **Direct binary patch** of GameAssembly.dll. See `unity-il2cpp-modding` §Direct binary patching. Fastest — no BepInEx needed.
   - **Complex patch** (conditional multipliers, runtime config, multiple methods): → BepInEx 6 IL2CPP + Harmony plugin. See §IL2CPP Code Patching.

3. **Does the game use Mono?** (check for `Managed/` folder with .dll assemblies)
   → BepInEx 5 Mono + Harmony, or direct dnSpy/ILSpy editing.

## Save Editing

### Locating saves
- Windows: `%USERPROFILE%\AppData\LocalLow\<Company>\<Game>\`
- Also check: `%USERPROFILE%\AppData\Local\`, `%USERPROFILE%\Documents\`, `%USERPROFILE%\Saved Games\`
- Steam: `steamapps/common/<Game>/` sometimes has local saves
- **Windows Registry**: `reg query "HKCU\Software\<Company>" /s` — some Unity games (e.g. Revolution Idle) store all data as encrypted `REG_BINARY` values. If encrypted, skip to code patching.

### Common formats
- **Gzip-compressed JSON**: `.GZ` files → `gunzip -c SAVE.GZ > out.txt`, edit, `gzip -c out.txt > SAVE.GZ`
- **Plain JSON**: direct sed/python edit
- **Binary/protobuf**: needs reverse engineering of schema
- **Encrypted**: check for XOR keys or AES in GameAssembly strings

### Pitfalls
- ALWAYS backup before editing (`cp SAVE.GZ SAVE.GZ.bak`)
- Edit ALL related fields (e.g. `money` AND `startOfDayMoney`) or the game may detect inconsistency
- Don't touch fields you don't understand (XP curves, checksums, level gates)
- Re-compress with same method (gzip level doesn't matter, but format must match)
- Some games have integrity hashes — search for `hash`, `checksum`, `crc` fields near edited values

## IL2CPP Code Patching (BepInEx 6)

### When static analysis fails
- Il2CppDumper supports up to metadata v37 (Unity 2021). **Metadata v39 (Unity 6) is NOT supported** by Il2CppDumper v6.7.40–v6.7.46 (exit 150/127).
- **BepInEx v6.0.0-pre.2 (stable) ALSO FAILS on v39** — its bundled Cpp2IL.Core.dll (March 2024) only supports metadata v23–29. The game will crash on launch with `Unsupported metadata version found! We support 23-29, got 39` in `BepInEx/LogOutput.log`.
- **Solution A (preferred): Standalone Cpp2IL 2022.1.0-pre-release.21+** — explicitly supports metadata v35, 38, 39, 104, 105, 106. See §Metadata v39 Workflow below.
- **Solution B: BepInEx nightly (be.785+, June 2026)** — bundles updated Cpp2IL that handles v39. Download via nightly.link (no auth): `https://nightly.link/BepInEx/BepInEx/workflows/build/master/BepInEx_CI_BleedingEdge_<sha>_<num>.zip`

### Metadata v39 Workflow (Unity 6)
1. Download Cpp2IL standalone: `https://github.com/SamboyCoding/Cpp2IL/releases/download/2022.1.0-pre-release.21/Cpp2IL-2022.1.0-pre-release.21-Windows.exe`
2. Generate dummy DLLs: `Cpp2IL.exe --game-path "<game_root>" --output-as dummydll --output-to "<output_dir>"`
3. Use a .NET reflection dumper (see `scripts/DumpTypes.cs`) to extract exact method signatures from the dummy DLLs
4. Write Harmony plugin targeting the discovered methods
5. Either: (a) replace BepInEx core's `Cpp2IL.Core.dll` + `LibCpp2IL.dll` with newer versions, or (b) use BepInEx nightly build that already includes v39 support
6. Deploy plugin to `BepInEx/plugins/` and launch game

### Installation (Windows x64)
```
# Download BepInEx 6 IL2CPP
curl -k -L -o bepinex.zip "https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-pre.2/BepInEx-Unity.IL2CPP-win-x64-6.0.0-pre.2.zip"
# Extract to game root (where GameAssembly.dll lives)
unzip -o bepinex.zip -d "<game_root>/"
```
Verify: `doorstop_config.ini` + `winhttp.dll` + `BepInEx/` + `dotnet/` in game root.

### First-run requirement
Game MUST be launched once after BepInEx install. BepInEx generates:
- `BepInEx/interop/` — managed wrappers for all game types
- These are needed to write strongly-typed plugins

**⚠️ CRITICAL**: This only works if BepInEx's bundled Cpp2IL supports the game's metadata version. For Unity 6 (metadata v39), the stable BepInEx v6.0.0-pre.2 will FAIL and crash the game. You must either:
- Replace `BepInEx/core/Cpp2IL.Core.dll` + `BepInEx/core/LibCpp2IL.dll` with versions from Cpp2IL 2022.1.0-pre-release.21+
- Or use BepInEx nightly (be.785+) which bundles updated Cpp2IL
- Or pre-generate interop with standalone Cpp2IL and place in `BepInEx/interop/` manually

**Recovery if game won't start**: Rename `winhttp.dll` → `winhttp.dll.bak` to disable BepInEx entirely and restore normal game launch.

### Plugin development
See `templates/BepInEx-IL2CPP-Plugin.csproj` and `templates/ReflectionHarmonyPlugin.cs`.
See `scripts/DumpTypes.cs` for a .NET reflection dumper to extract exact method signatures from Cpp2IL dummy DLLs.

Key pattern for unknown game internals:
1. Use reflection to find types by name at runtime
2. Dump all fields/methods to BepInEx log
3. Identify target methods (by name heuristics: spawn, drop, amount, count)
4. **Patch the DROP SOURCE, not the inventory sink** (see pitfall below)

**⚠️ For "give me more X" multipliers, patch where X is GENERATED, not where it's STORED.** A mining game has a pipeline: `OreNode.GetDropCount(hits)` (how much spawns) → world pickups → `T_Bag.AddItems(item, count)` (inventory). Multiplying at the **inventory sink** (`AddItems`) gets clamped by bag capacity and produces inconsistent results. Multiplying at the **drop source** (`GetDropCount` postfix: `__result *= N`) spawns more physical ore in the world — capacity stops binding and the user sees "the node drops N× more," which is what they asked for. Also hunt for a hidden penalty multiplier (e.g. `NodeBreakSystem.rewardMultiplier = 0.5`) that silently halves drops, and reset it to 1.0 in an `Awake` postfix. See the `unity-il2cpp-modding` skill §4b for the full pipeline diagram.

### Build & deploy
```bash
dotnet build -c Release
cp bin/Release/net6.0/<Plugin>.dll "<game_root>/BepInEx/plugins/"
```

### Verification
- Check `BepInEx/LogOutput.log` or `<game>_BepInEx/LogOutput.log` after launch
- Look for plugin load messages and patch confirmations
- If "No game types found" → interop not generated yet, run game once first

## IL2CPP Metadata Analysis (when tools work)

For metadata ≤ v37:
```bash
# Il2CppDumper
./Il2CppDumper GameAssembly.dll global-metadata.dat ./output/
# Produces: dump.cs (type definitions), script.json (offsets for IDA/Ghidra)
```

For metadata v39+: use `strings` + Python regex extraction as fallback:
```python
import re
with open('global-metadata.dat', 'rb') as f:
    data = f.read()
strings = [s.decode('ascii') for s in re.findall(rb'[\x20-\x7e]{4,}', data)]
# Filter for class/method names
```

Also: UnityPy for asset inspection (MonoScript names, TextAsset contents):
```bash
pip install UnityPy
```
Note: IL2CPP builds strip typetree data — MonoBehaviour fields won't be readable via UnityPy.

## Pitfalls

- **BepInEx stable crashes Unity 6 games**: v6.0.0-pre.2's bundled Cpp2IL only supports metadata v23–29. On v39 games it fails during interop generation and the game won't start. Always check `BepInEx/LogOutput.log` for "Unsupported metadata version" before assuming other causes. Fix: use BepInEx nightly (be.785+) or replace `BepInEx/core/Cpp2IL.Core.dll` + `LibCpp2IL.dll` with versions from Cpp2IL 2022.1.0-pre-release.21+.
- **Recovery from broken BepInEx**: Rename `winhttp.dll` → `winhttp.dll.bak` in game root. This disables Doorstop/BepInEx entirely and the game launches normally. No uninstall needed.
- **Anti-cheat**: Online/multiplayer games may detect BepInEx. Single-player only.
- **Steam overlay**: Sometimes conflicts with doorstop. Disable if game crashes on launch.
- **Game updates**: Break interop assemblies and offsets. Re-generate after updates.
- **curl on Windows/MSYS**: Use `-k -L` flags and explicit output path (`-o "C:/Users/.../file.zip"`). Relative paths and `~` may not resolve correctly in git-bash.
- **dotnet SDK vs runtime**: Plugin build needs SDK (8.0+). BepInEx ships its own `dotnet/` runtime folder for the game process — don't confuse them.
- **Inventory capacity silently caps multiplied amounts**: When you Harmony-prefix an inventory `AddItems(item, count)` to multiply `count`, the method body may have an internal capacity check that clamps the actual amount added. Logs show `AddItems: 2->100 (x50)` but the return value is 2. **Diagnosis:** add a postfix comparing requested vs returned amount. **Fix:** in the prefix, set the bag's `maxCapacity` and `baseMaxCapacity` fields to 99999 via reflection BEFORE the method body runs. Patching `get_MaxCapacity` postfix alone is insufficient — IL2CPP may inline the getter so the body reads the backing field directly. Also watch for capacity-restoring methods (`ApplyBackpackCapacityFromUpgrade`, `SetMaxCapacity`, `OnBuffsChanged`) that reset the field.
- **Patch fires but no visible effect — two failure modes**: (A) Wrong subsystem — you're patching a world-object method, not the player inventory. (B) Right method but internal validation caps the result. Always add a postfix logging return value vs modified argument to distinguish the two.
- **Clean rollback while keeping save edits**: Users often say "keep my money, remove the mod." Close the game first (DLLs lock while running), then: `rm BepInEx/plugins/<Mod>.dll` (or the whole `BepInEx/`), restore the proxy with `cp winhttp.dll.bak winhttp.dll`, and `rm doorstop_config.ini`. **Do NOT restore `SAVE.GZ.bak`** unless they also want the save reverted — that would wipe their money/progress.

