Unity Game Modding
Decision Tree
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.
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.
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)
- 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
- Generate dummy DLLs:
Cpp2IL.exe --game-path "<game_root>" --output-as dummydll --output-to "<output_dir>"
- Use a .NET reflection dumper (see
scripts/DumpTypes.cs) to extract exact method signatures from the dummy DLLs
- Write Harmony plugin targeting the discovered methods
- 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
- 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:
- Use reflection to find types by name at runtime
- Dump all fields/methods to BepInEx log
- Identify target methods (by name heuristics: spawn, drop, amount, count)
- 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
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:
# 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:
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):
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.
1---2name: unity-game-modding3description: Modify Unity games — save file editing, BepInEx IL2CPP plugin development, IL2CPP metadata analysis, and runtime Harmony patching.4---56# Unity Game Modding78## Decision Tree9101. **Can the change be done via save file?** (money, XP, inventory counts)11 → Save editing (fastest, safest). See §Save Editing.12 ⚠️ 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.13142. **Does the game use IL2CPP?** (check for `GameAssembly.dll` + `il2cpp_data/Metadata/global-metadata.dat`)15 - **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.16 - **Complex patch** (conditional multipliers, runtime config, multiple methods): → BepInEx 6 IL2CPP + Harmony plugin. See §IL2CPP Code Patching.17183. **Does the game use Mono?** (check for `Managed/` folder with .dll assemblies)19 → BepInEx 5 Mono + Harmony, or direct dnSpy/ILSpy editing.2021## Save Editing2223### Locating saves24- Windows: `%USERPROFILE%\AppData\LocalLow\<Company>\<Game>\`25- Also check: `%USERPROFILE%\AppData\Local\`, `%USERPROFILE%\Documents\`, `%USERPROFILE%\Saved Games\`26- Steam: `steamapps/common/<Game>/` sometimes has local saves27- **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.2829### Common formats30- **Gzip-compressed JSON**: `.GZ` files → `gunzip -c SAVE.GZ > out.txt`, edit, `gzip -c out.txt > SAVE.GZ`31- **Plain JSON**: direct sed/python edit32- **Binary/protobuf**: needs reverse engineering of schema33- **Encrypted**: check for XOR keys or AES in GameAssembly strings3435### Pitfalls36- ALWAYS backup before editing (`cp SAVE.GZ SAVE.GZ.bak`)37- Edit ALL related fields (e.g. `money` AND `startOfDayMoney`) or the game may detect inconsistency38- Don't touch fields you don't understand (XP curves, checksums, level gates)39- Re-compress with same method (gzip level doesn't matter, but format must match)40- Some games have integrity hashes — search for `hash`, `checksum`, `crc` fields near edited values4142## IL2CPP Code Patching (BepInEx 6)4344### When static analysis fails45- 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).46- **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`.47- **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.48- **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`4950### Metadata v39 Workflow (Unity 6)511. 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`522. Generate dummy DLLs: `Cpp2IL.exe --game-path "<game_root>" --output-as dummydll --output-to "<output_dir>"`533. Use a .NET reflection dumper (see `scripts/DumpTypes.cs`) to extract exact method signatures from the dummy DLLs544. Write Harmony plugin targeting the discovered methods555. Either: (a) replace BepInEx core's `Cpp2IL.Core.dll` + `LibCpp2IL.dll` with newer versions, or (b) use BepInEx nightly build that already includes v39 support566. Deploy plugin to `BepInEx/plugins/` and launch game5758### Installation (Windows x64)59```60# Download BepInEx 6 IL2CPP61curl -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"62# Extract to game root (where GameAssembly.dll lives)63unzip -o bepinex.zip -d "<game_root>/"64```65Verify: `doorstop_config.ini` + `winhttp.dll` + `BepInEx/` + `dotnet/` in game root.6667### First-run requirement68Game MUST be launched once after BepInEx install. BepInEx generates:69- `BepInEx/interop/` — managed wrappers for all game types70- These are needed to write strongly-typed plugins7172**⚠️ 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:73- Replace `BepInEx/core/Cpp2IL.Core.dll` + `BepInEx/core/LibCpp2IL.dll` with versions from Cpp2IL 2022.1.0-pre-release.21+74- Or use BepInEx nightly (be.785+) which bundles updated Cpp2IL75- Or pre-generate interop with standalone Cpp2IL and place in `BepInEx/interop/` manually7677**Recovery if game won't start**: Rename `winhttp.dll` → `winhttp.dll.bak` to disable BepInEx entirely and restore normal game launch.7879### Plugin development80See `templates/BepInEx-IL2CPP-Plugin.csproj` and `templates/ReflectionHarmonyPlugin.cs`.81See `scripts/DumpTypes.cs` for a .NET reflection dumper to extract exact method signatures from Cpp2IL dummy DLLs.8283Key pattern for unknown game internals:841. Use reflection to find types by name at runtime852. Dump all fields/methods to BepInEx log863. Identify target methods (by name heuristics: spawn, drop, amount, count)874. **Patch the DROP SOURCE, not the inventory sink** (see pitfall below)8889**⚠️ 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.9091### Build & deploy92```bash93dotnet build -c Release94cp bin/Release/net6.0/<Plugin>.dll "<game_root>/BepInEx/plugins/"95```9697### Verification98- Check `BepInEx/LogOutput.log` or `<game>_BepInEx/LogOutput.log` after launch99- Look for plugin load messages and patch confirmations100- If "No game types found" → interop not generated yet, run game once first101102## IL2CPP Metadata Analysis (when tools work)103104For metadata ≤ v37:105```bash106# Il2CppDumper107./Il2CppDumper GameAssembly.dll global-metadata.dat ./output/108# Produces: dump.cs (type definitions), script.json (offsets for IDA/Ghidra)109```110111For metadata v39+: use `strings` + Python regex extraction as fallback:112```python113import re114with open('global-metadata.dat', 'rb') as f:115 data = f.read()116strings = [s.decode('ascii') for s in re.findall(rb'[\x20-\x7e]{4,}', data)]117# Filter for class/method names118```119120Also: UnityPy for asset inspection (MonoScript names, TextAsset contents):121```bash122pip install UnityPy123```124Note: IL2CPP builds strip typetree data — MonoBehaviour fields won't be readable via UnityPy.125126## Pitfalls127128- **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+.129- **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.130- **Anti-cheat**: Online/multiplayer games may detect BepInEx. Single-player only.131- **Steam overlay**: Sometimes conflicts with doorstop. Disable if game crashes on launch.132- **Game updates**: Break interop assemblies and offsets. Re-generate after updates.133- **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.134- **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.135- **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.136- **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.137- **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.