Unity Modding
Unity Modding
Besides Unreal Engine games, automod patches Unity games using asset4j (the JVM port of AssetsTools.NET). Unity mods are scoped to a game’s data directory (e.g., WarmSnow_Data) configured in .config.json, and the patch files live in patches\<game-id>\<mod-name>\ just like UE mods.
A Unity game is selected with its unity flag in .config.json. automod then:
- Decodes the target bundles with asset4j, using the game’s per-version
ttmapschema (auto-downloaded from the automod releases during setup when the game’smapUriis set but the map is not yet present intools/ttmap). - Patches decoded objects via class-scoped TOML or script patches, raw files (e.g., non-asset files) via whole-file script patches, and
.NETassemblies via IL patching using dnlib4j. - Re-encodes the bundles in-process with asset4j.
- Packages everything into a
<mod-name>.zip/.7zarchive embedding theinstall.pyinstaller.
Enabling a mod is just removing the leading dot from its folder name (.easy-mode -> easy-mode); dot-prefixed folders are skipped by .batch unless selectively specified as a .batch argument.
Class-scoped TOML patches
A patch file named <ClassName>@<bundle>.toml (e.g., MonsterSetting@resources.assets.toml) targets the objects of a script class (MonsterSetting) inside a bundle (resources.assets). Every .@ section is a JSONPath applied to each matching object’s decoded data, and $ refers to the object’s own root:
# Warm Snow "easy mode" — MonsterSetting@resources.assets.toml
['.@: $.list.Array[*]']
MonsterHP = '=> v.orig[Double] * 0.5'
MonsterAtk = '=> v.orig[Double] * 0.5'
MonsterDef = '=> v.orig[Double] * 0.5'
SoulDrop = '=> v.orig[Double] * 10'
Class-scoped script patches
A patch file named <ClassName>@<bundle>.kt (or .js, .ts, .py, .sc, .lua) runs the script body as the transform over v.objects – a list of {id, data} where data is each matching object’s decoded JSON. The script edits the objects in place and returns them; automod re-encodes via the targeted round-trip. v.className exposes the class name.
// MonsterSetting@resources.assets.kt — halve every monster's stats
import com.fasterxml.jackson.databind.node.*
import com.fasterxml.jackson.databind.ObjectMapper
for (obj in v.objects) {
val data = obj["data"] as ObjectNode
for (m in data["list"]["Array"]) {
m as ObjectNode
m.replace("MonsterHP", DoubleNode.valueOf(m["MonsterHP"].asDouble() * 0.5))
m.replace("MonsterAtk", DoubleNode.valueOf(m["MonsterAtk"].asDouble() * 0.5))
}
}
v.objects
Whole-file raw patches
A patch file named after the target file (with $ in place of /) patches a whole file. v.orig is the original bytes, v.current the bytes after any earlier patches; the script returns the patched bytes.
# Warm Snow player cooldowns -> 2s — Managed$Assembly-CSharp.dll.py
import struct
data = bytearray(v["current"])
new = struct.pack("<f", 2.0)
for base, expected in [
(0x127E73, 14.0), # base drawCoolDown
(0x126E74, 28.0), # GourdLiquor
]:
assert data[base] == 0x22 and struct.unpack("<f", bytes(data[base+1:base+5]))[0] == expected
data[base + 1:base + 5] = new
data
For a bundle/asset file (e.g. config), the script can instead work on the JSON representation: v.toJson() decodes the current bytes with asset4j, the script patches the tree, and v.fromJson(tree) re-encodes. v.resource(name) reads any file from the patch directory.
// Bladed Fury "easier mode" — config.kt
val tree = v.toJson() as com.fasterxml.jackson.databind.node.ObjectNode
// ... JSONPath-select and scale MonsterData / BehaviorNode ...
v.fromJson(tree)
IL patching
For Unity .NET assemblies (e.g., Managed$Assembly-CSharp.dll), automod supports static IL patching via dnlib4j. A .il.toml file replaces fragile hex-offset byte patches with semantic TOML rules that survive game updates keeping the same instruction patterns.
The file is named <AssemblyName>.il.toml (e.g., Managed$Assembly-CSharp.dll.il.toml). The target assembly is derived by stripping .il from the name.
Constant swaps ([[float]], [[int]], [[long]])
Replace numeric constants in method bodies. Searches for matching load-constant opcodes and patches the operand:
# Warm Snow "easy mode" — halve ability cooldowns
[[float]]
type = "PlayerAnimControl"
method = ".ctor"
old = 14.0
new = 2.0
occurrence = 2
[[float]]
type = "PlayerAnimControl"
method = "GourdLiquorContainerOn"
old = 28.0
new = 2.0
[[float]]
type = "PlayerAnimControl"
method = ".ctor"
old = 15.0
new = 2.0
occurrence = "1,2"
Fields: type/method (optional filters, null = match all), old (value to find), new (replacement), occurrence (which match: integer, comma-separated string, or array; omitted = patch every match).
String swaps ([[string]])
Replace string literals loaded by ldstr instructions:
[[string]]
type = "SomeClass"
method = "SomeMethod"
old = "original string"
new = "replacement string"
Instruction sequence replacement ([[il]])
Find-and-replace of IL instruction sequences using find/replace arrays:
[[il]]
type = "SomeClass"
method = "SomeMethod"
find = ["ldc.i4.1", "ret"]
replace = ["ldc.i4.0", "ret"]
occurrence = 1
Instruction specs are space-separated: "opcode" or "opcode operand". Operands can be strings, method references ("Type::Method"), field references ("ldfld Type::Field"), integers, or floats.
Method body replacement ([[method]])
Replace or prepend/append to a method’s entire body:
[[method]]
type = "SomeClass"
method = "SomeMethod"
kind = "body" # "body" (replace all), "entry" (prepend), "exit" (before every ret)
replace = ["nop", "ret"]
Code injection ([[inject.call]], [[inject.field_read]], [[inject.field_write]])
Inject code at call sites or field access sites:
[[inject.call]]
target = "TargetType::TargetMethod"
when = "AFTER" # BEFORE, AFTER, AROUND, or REPLACE
scope = "ScopedType" # optional: only inject in this type's methods
code = ["ldstr \"injected\"", "call System.Console::WriteLine(string)"]
IL patching integrates with the standard raw file pipeline — .il.toml files are collected alongside other patches and applied sequentially. The patched DLL gets chmod +x on non-Windows platforms.
ttmap schemas
A ttmap records the Unity type trees (and script classes) a game needs for decoding external-tree assets. Each supported game pins its ttmap (e.g., warmsnow_3.1.0.1.ttmap); when the game’s mapUri is configured but the map file is not yet present in tools/ttmap (or tools/usmap for Unreal Engine games), automod downloads it from the automod releases during setup. To regenerate schemas (e.g., after a game update), use the .ttmapgen command:
./automod .ttmapgen dll "game/WarmSnow_Data/Managed" warmsnow.ttmap 2020.3.22f1 3.1.0.1
./automod .ttmapgen il2cpp GameAssembly.dll global-metadata.dat silksong.ttmap 2022.3.10f1 1.0.0
ttmapgen.jar is auto-installed (version-matched to automod’s asset4j) on first use; the dll/il2cpp modes additionally require a .NET 10 runtime to build the C# harness.
Installing Unity mods
Each generated archive embeds install.py and a gameDataDir.txt hint. Uncompress the archive, then:
python3 install.py # install; originals are backed up first
python3 install.py -r # restore the originals (undo)
The installer needs Python 3 on the machine installing the mod (not to run automod itself); if Python is unavailable, uncompress the archive and copy the files into the game’s data directory manually. Backups live in <game-dir>/../<mod-name>-backup/. The installer is cross-platform (Windows, Linux, macOS) and sets the executable bit on patched .dll files on non-Windows platforms.