The Auto Modding Script (automod) is an automated, cross-platform, polyglot, and highly parallel Unreal Engine and Unity game mod generator. It creates Unreal Engine mods from succinct TOML patch files for .uasset properties, which may include embedded regular expressions and Scala, TypeScript, JavaScript, Kotlin, Python, and/or Lua patchlets. For Unity games, it patches assets with class-scoped TOML or script (.kt, .js, .py, etc.) patches and raw file patches. This enables complex patching tasks with significantly lower manual effort, all with patch auto-merging support. It runs on Windows, Linux, and macOS (amd64 or arm64).
A VSCode/VSCodium extension is provided to ease its use, complementing an existing extension providing TOML syntax highlighting/checking.
Patch File Examples
# Based on https://www.nexusmods.com/stellarblade/mods/897
[P_Eve_SkillTree_JustEvade_BurstGauge1]
CalculationMultipleValue = 4
LifeTime = 11
LoopIntervalTime = 1
LoopTargetFilterAlias = "Self"
OverlapCount = 1
StartDelayTime = 1
[P_Eve_SkillTree_JustEvade_BurstGauge2]
CalculationMultipleValue = 3
LifeTime = 11
LoopIntervalTime = 1
LoopTargetFilterAlias = "Self"
OverlapCount = 1
StartDelayTime = 1
[P_Eve_SkillTree_JustParry_BetaGauge1]
CalculationMultipleValue = 8
LifeTime = 11
LoopIntervalTime = 1
LoopTargetFilterAlias = "Self"
OverlapCount = 1
StartDelayTime = 1
[P_Eve_SkillTree_JustParry_BetaGauge2]
CalculationMultipleValue = 6
LifeTime = 11
LoopIntervalTime = 1
LoopTargetFilterAlias = "Self"
OverlapCount = 1
StartDelayTime = 1
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=> v.objName match {
case "P_Eve_SkillTree_JustParry_BetaGauge1" => 8
case "P_Eve_SkillTree_JustParry_BetaGauge2" => 6
case "P_Eve_SkillTree_JustEvade_BurstGauge1" => 4
case "P_Eve_SkillTree_JustEvade_BurstGauge2" => 3
}'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=kt>
when (v["objName"]) {
"P_Eve_SkillTree_JustParry_BetaGauge1" -> 8
"P_Eve_SkillTree_JustParry_BetaGauge2" -> 6
"P_Eve_SkillTree_JustEvade_BurstGauge1" -> 4
"P_Eve_SkillTree_JustEvade_BurstGauge2" -> 3
else -> 0
}'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=ts>
let r: number = 0;
switch(v.objName()) {
case "P_Eve_SkillTree_JustParry_BetaGauge1": r = 8; break;
case "P_Eve_SkillTree_JustParry_BetaGauge2": r = 6; break;
case "P_Eve_SkillTree_JustEvade_BurstGauge1": r = 4; break;
case "P_Eve_SkillTree_JustEvade_BurstGauge2": r = 3; break;
}
r
'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=js>
let r = 0;
switch(v.objName()) {
case "P_Eve_SkillTree_JustParry_BetaGauge1": r = 8; break;
case "P_Eve_SkillTree_JustParry_BetaGauge2": r = 6; break;
case "P_Eve_SkillTree_JustEvade_BurstGauge1": r = 4; break;
case "P_Eve_SkillTree_JustEvade_BurstGauge2": r = 3; break;
}
r
'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=py>
r = 0
match v.objName():
case "P_Eve_SkillTree_JustParry_BetaGauge1":
r = 8
case "P_Eve_SkillTree_JustParry_BetaGauge2":
r = 6
case "P_Eve_SkillTree_JustEvade_BurstGauge1":
r = 4
case "P_Eve_SkillTree_JustEvade_BurstGauge2":
r = 3
r
'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
['.*: ^P_Eve_SkillTree_Just(.*BetaGauge|.*BurstGauge).*$']
CalculationMultipleValue = '''=lua>
local r = {
["P_Eve_SkillTree_JustParry_BetaGauge1"] = 8,
["P_Eve_SkillTree_JustParry_BetaGauge2"] = 6,
["P_Eve_SkillTree_JustEvade_BurstGauge1"] = 4,
["P_Eve_SkillTree_JustEvade_BurstGauge2"] = 3
}
return r[v.objName]
'''
OverlapCount = 1
LifeTime = 11
StartDelayTime = 1
LoopIntervalTime = 1
LoopTargetFilterAlias = 'Self'
# Unity patches are scoped to a script class by the file name ClassName@bundle.toml, and
# .@ JSONPath expressions are rooted at $ (each matching object's data).
# 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'
# Whole files (e.g. Assembly-CSharp.dll) are patched by name File$path.py and receive the
# file bytes via v["orig"] / v["current"].
# Warm Snow player ability cooldowns -> 2s — Managed$Assembly-CSharp.dll.py
import struct
data = bytearray(v["current"])
new = struct.pack("<f", 2.0)
for base, expected in [ # (offset, expected float)
(0x127E73, 14.0), # base drawCoolDown
(0x126E74, 28.0), # GourdLiquor
(0x127C90, 15.0), # GodOfSwords
]:
assert data[base] == 0x22 and struct.unpack("<f", bytes(data[base+1:base+5]))[0] == expected
data[base + 1:base + 5] = new
data
// A whole-bundle script decodes via v.toJson() (asset4j JSON), patches with JSONPath,
// and re-encodes with v.fromJson(...).
// Bladed Fury "easier mode" — config.kt (Kotlin)
val tree = v.toJson() as com.fasterxml.jackson.databind.node.ObjectNode
val json = ObjectMapper().readTree(tree["Files"][0]["Asset"]["Objects"][0]["Data"]["m_Script"].asText())
val monsters = JsonPath.using(configuration).parse(json)
.read<ArrayNode>("$..*[?(@.AttackParam != null && @.DefenseParam != null && @.Money != null)]")
for (m in monsters) {
m.replace("AttackParam", DoubleNode.valueOf(m["AttackParam"].asDouble() * 0.5))
m.replace("DefenseParam", DoubleNode.valueOf(m["DefenseParam"].asDouble() * 0.5))
m.replace("Money", IntNode.valueOf((m["Money"].asLong() * 10).toInt()))
}
tree["Files"][0]["Asset"]["Objects"][0]["Data"]["m_Script"] = TextNode(ObjectMapper().writeValueAsString(json))
v.fromJson(tree)