TOML Patch File Schema
The automod TOML patch files work very well for patching .uasset data tables, while providing arbitrary JSON structure manipulations using JSON Pointer or JSONPath expressions.
The supported TOML patch file form looks like the following:
[obj1_Name]
property1_1 = value1_1
...
â €
â €
...
â €
[objN_Name]
propertyN_1 = valueN_1
...
The objX_Name matches the data table struct Name property in UAssetAPI’s JSON representation, located inside the array expressed using the following JSON Pointer: /Exports/0/Table/Data/. For example, here is a patch file for the Stellar Blade EffectTable.uasset Drone Scan Effect Extend – DroneScanEffectExtend_30s mod by cktt12:
# Based on https://www.nexusmods.com/stellarblade/mods/802
[N_Drone_Scan]
LifeTime = 30
The N_Drone_Scan matches the object accessible at /Exports/0/Table/Data/722 and modifies its LifeTime property (the Value of the object at /Exports/0/Table/Data/722/Value/78) to the JSON number 30 (based on Stellar Blade v1.3.2; see EffectTable.json in the .cache directory to fully appreciate the UAssetAPI JSON representation).
The N_Drone_Scan object can also be expressed using the JSONPath query:
$..[?(@.Name == 'N_Drone_Scan')]
and the LifeTime object as:
$..[?(@.Name == 'N_Drone_Scan')]..[?(@.Name == 'LifeTime')]
In addition to numbers, the supported property values are TOML booleans, strings, enums (of the string form <enum>::<element>), arrays, and inline tables (for representing JSON objects).
Each patch file should be named after the corresponding .uasset target filename, but suffixed with the .toml file extension instead.
For example, the above patch file example for Stellar Blade’s EffectTable.uasset should be named EffectTable.toml.
The UAssetAPI JSON Representation
Before patching, automod converts each .uasset to UAssetAPI’s .json format in-process using uasset4j (the JVM port of UAssetAPI), and patch files operate directly on that JSON representation. Two parts of the structure matter most when writing patch files:
/Exports/0/Table/Data/is the array holding a data table’s entries. Each entry is a struct object whoseNameproperty is the row’s name (e.g.,N_Drone_Scan); thisNameis what the[obj_Name]patch file sections match against.- The properties of that struct (e.g.,
LifeTime) are themselves objects located under the struct’sValuearray. Each property object carries metadata such as$type,ArrayIndex, andValue, whereValueholds the actual property value. For example, theLifeTimeproperty ofN_Drone_Scanis the object at/Exports/0/Table/Data/722/Value/78.
Because [obj_Name] sections and property = value lines handle these details for you, you usually do not need to know the exact JSON indices. When a patch is more involved, though, the extracted JSON files are cached in the .cache directory (e.g., EffectTable.json) so you can inspect the exact structure.
Regular Expression and Polyglot Patchlet
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=> v.startsWith("Gear_") && !v.contains("_HitDmgUp")' ] #2
CalculationMultipleValue = 2
[ '=> Note: Doubling gear (beneficial) effectiveness' ] #3
'=>' = '''{
if (!v.startsWith("Gear_")) return false
!v.contains("_HitDmgUp")
}'''
CalculationValue = '=> v.orig[ Double ] * 2'
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=kt> v.startsWith("Gear_") && !v.contains("_HitDmgUp")' ] #2
CalculationMultipleValue = 2
[ '=kt> Note: Doubling gear (beneficial) effectiveness' ] #3
'=kt>' = '''
if (!v.startsWith("Gear_")) false else !v.contains("_HitDmgUp")
'''
CalculationValue = '=kt> (v["orig"] as Double) * 2'
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=ts> v.startsWith("Gear_") && !v.includes("_HitDmgUp")' ] #2
CalculationMultipleValue = 2
[ '=ts> Note: Doubling gear (beneficial) effectiveness' ] #3
'=ts>' = '''
!v.startsWith("Gear_")?
false
: !v.includes("_HitDmgUp")
'''
CalculationValue = '=ts> v.orig() * 2'
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=js> v.startsWith("Gear_") && !v.includes("_HitDmgUp")' ] #2
CalculationMultipleValue = 2
[ '=js> Note: Doubling gear (beneficial) effectiveness' ] #3
'=js>' = '''
!v.startsWith("Gear_")?
false
: !v.includes("_HitDmgUp")
'''
CalculationValue = '=js> v.orig() * 2'
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=py> v.startswith("Gear_") and ("_HitDmgUp" not in v)' ] #2
CalculationMultipleValue = 2
[ '=py> Note: Doubling gear (beneficial) effectiveness' ] #3
'=py>' = '''
False if not v.startswith("Gear_") else "_HitDmgUp" not in v
'''
CalculationValue = '=py> v.orig() * 2'
['.*: ^Gear_.*_HitDmgUp.*$'] #1
CalculationMultipleValue = 0.5
[ '=lua> return (v:find("^" .. "Gear_") == 1) and (v:find("_HitDmgUp") == nil)' ] #2
CalculationMultipleValue = 2
[ '=lua> Note: Doubling gear (beneficial) effectiveness' ] #3
'=lua>' = '''
if v:find("^" .. "Gear_") == nil then
return false
else
return v:find("_HitDmgUp") == nil
end
'''
CalculationValue = '=lua> return v.orig * 2'
The Java regular expression (regex) syntax can be used to match object names in bulk, using the TOML literal string with the automod .*: prefix (#1).
You can also use a polyglot patchlet – any Scala/TypeScript/JavaScript/Kotlin/Python 3/Lua 5.2 code to match object names using the =>/=ts>/=js>/=kt>/=py>/=lua> prefixes, with the predefined variable v holding the value of the object name string (#2).
Since TOML does not allow a multiline (literal) string inside the square brackets for matching object names, an alternative syntax is provided for matching them with complex code: the object name matcher is still prefixed by =>/=ts>/=js>/=kt>/=py>/=lua>, but the key holds a description of the patchlet, and the actual code is specified under a special property named =>/=ts>/=js>/=kt>/=py>/=lua> (#3).
Example #3 also shows how to update a property value based on its original game value, prefixing the value with =>/=ts>/=js>/=kt>/=py>/=lua>: v.orig[Double] * 2 (or v.orig() * 2 for non-Scala ones). In this case, the variable v holds the API to access the original value, whose expected type must be supplied (e.g., Double).
A variant of the above is used to create the effect-table demo mod (see automod’s patches\.all-in-one-patches\*987*\EffectTable.toml).
Here is another example that is a bit more complicated (see patches\.all-in-one-patches\*1329* for more):
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=> {
val newArray = JsonNodeFactory.instance.arrayNode
newArray.add(toJsonNode("""{ "Alias": "NoReactionSlug_Step" }"""))
Option(v.currentOpt[String].get) match {
case Some(value) =>
val array = toJsonNodeT[ArrayNode](value)
for (i <- 0 until array.size)
array.get(i).get("Alias").asText match {
case "NoReactionSlug_Step" | "BlockAction_Step" =>
case _ => newArray.add(array.get(i))
}
case _ =>
}
newArray.toString
}'''
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=kt>
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.*
val mapper = ObjectMapper()
val newArray = mapper.createArrayNode()
newArray.add(mapper.readTree("""{ "Alias": "NoReactionSlug_Step" }"""))
val current = v["current"] as? String
if (current != null) {
val array = mapper.readTree(current) as ArrayNode
for (e in array) {
val alias = e.get("Alias").asText()
if (alias != "NoReactionSlug_Step" && alias != "BlockAction_Step") newArray.add(e)
}
}
newArray.toString()
'''
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=ts>
//JsonNode predefined as:
// type JsonNode = { [key: string]: any; }
let newArray: JsonNode[] = [];
newArray.push({ "Alias": "NoReactionSlug_Step" });
let array: JsonNode[] = JSON.parse(String(v.current()));
if (array != null) {
for (let i = 0; i < array.length; i++) {
switch(array[i].Alias) {
case "NoReactionSlug_Step":
case "BlockAction_Step": break;
default: newArray.push(array[i]);
}
}
}
JSON.stringify(newArray)
'''
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=js>
let newArray = [];
newArray.push({ "Alias": "NoReactionSlug_Step" });
let array = JSON.parse(v.current());
if (array != null) {
for (let i = 0; i < array.length; i++) {
switch(array[i].Alias) {
case "NoReactionSlug_Step":
case "BlockAction_Step": break;
default: newArray.push(array[i]);
}
}
}
JSON.stringify(newArray)
'''
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=py>
import json
newArray = []
newArray.append({ "Alias": "NoReactionSlug_Step" })
array = json.loads(v.current()) if v.current() is not None else None
if array is not None:
for e in array:
match e.get("Alias"):
case "NoReactionSlug_Step":
pass
case "BlockAction_Step":
pass
case _:
newArray.append(e)
json.dumps(newArray)
'''
['.*: ^P_Eve_Fusion_Skill_.*_Finish.*$']
StartSelfEffect = '''=lua>
newArray = {}
newArray[1] = { ["Alias"] = "NoReactionSlug_Step" }
array = JSON.parse(v.current)
if (array ~= nil) then
j = 2
for _, e in ipairs(array) do
if (not (e.Alias == "NoReactionSlug_Step" or e.Alias == "BlockAction_Step")) then
newArray[j] = e
j = j + 1
end
end
end
r = JSON.stringify(newArray)
return r
'''
The v.currentOpt (or v.current() for non-Scala) method retrieves the property value after the preceding patches have been applied, whereas v.orig (or v.orig()) retrieves the original game value, unaffected by them. This distinction determines how later patches compose with earlier ones: using v.orig overrides the effects of the preceding patches on that property, while using v.current/v.currentOpt applies the patch incrementally on top of them (see the Patch Auto-Merging section for patch ordering information).
Also, as can be observed, arbitrarily complex Scala code can be embedded in the patchlet.
Debugging such complex code might be a challenge as there is no debugger facility for it.
However, one can still resort to the “old school” logging (i.e., printing to the console inside patchlets).
In general, you can access any object property value in the same .uasset by using the v.valueOf method.
Here is the CodeContext or PolyCodeContext object API for v:
type CodeContext = {
def objName: String
def orig[T]: T
def currentOpt[T]: Option[T]
def valueOf[T](objName: String, property: String): Option[T]
def ast: com.fasterxml.jackson.databind.JsonNode
def origAst: com.fasterxml.jackson.databind.JsonNode
}
type JsonNode = { [key: string]: any }
interface PolyCodeContext {
objName(): string
orig(): any
current(): any | undefined
ast(): JsonNode
origAst(): JsonNode
valueOf(objName: string, property: string): any | undefined
}
Note that each patchlet is dynamically loaded, compiled, and executed every time it is used, so it takes more time than regular exact-match/value patches or regex-based ones.
As previously mentioned, complex property value types are supported using TOML inline table and array syntax.
For example, the following patch file is how the Stellar Blade Altess Levoire and Abyss Levoire can use sword and scanner – Levoire_Use_Sword v1.0.0 mod by Violetkylin removes sword/drone scan restrictions (P_Eve_Stance_BlockSword/N_Drone_BlockScan name properties) in the Altess/Abyss Levoire EnterZoneEffects array property value, while still disallowing fishing (P_Eve_BlockFishingMode):
# Based on https://www.nexusmods.com/stellarblade/mods/1660
[Zone_ATL_01]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_ATL_02]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_ATL_03]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_01]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_02]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_03]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_04]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_05]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
[Zone_AYL_06]
EnterZoneEffects = [ { "Name" = "2", "$type" = "UAssetAPI.PropertyTypes.Objects.NamePropertyData, UAssetAPI", "ArrayIndex" = 0.0, "Value" = "P_Eve_BlockFishingMode", "PropertyTagExtensions" = "NoExtension", "IsZero" = false, "PropertyTagFlags" = "None" } ]
Instead of typing all that, however, it is a lot less effort to use a regex/patchlet combination:
['.*: ^Zone_A(T|Y)L.*$']
EnterZoneEffects = '''=> {
var newArray = Seq[Map[String, Any]]()
for (m <- v.orig[Seq[Map[String, Any]]]) m("Value") match {
case "P_Eve_Stance_BlockSword" | "N_Drone_BlockScan" =>
case _ => newArray +:= m
}
newArray
}'''
['.*: ^Zone_A(T|Y)L.*$']
EnterZoneEffects = '''=ts>
let newArray: JsonNode[] = [];
for (const m of (v.orig() as JsonNode[])) switch(m.Value) {
case "P_Eve_Stance_BlockSword":
case "N_Drone_BlockScan": break;
default: newArray.push(m)
}
newArray
'''
['.*: ^Zone_A(T|Y)L.*$']
EnterZoneEffects = '''=js>
let newArray = [];
for (const m of v.orig()) switch(m.Value) {
case "P_Eve_Stance_BlockSword":
case "N_Drone_BlockScan": break;
default: newArray.push(m)
}
newArray
'''
['.*: ^Zone_A(T|Y)L.*$']
EnterZoneEffects = '''=py>
newArray = []
for m in v.orig():
match m.get("Value"):
case "P_Eve_Stance_BlockSword":
pass
case "N_Drone_BlockScan":
pass
case _:
newArray.append(m)
newArray
'''
['.*: ^Zone_A(T|Y)L.*$']
EnterZoneEffects = '''=lua>
newArray = {}
j = 1
for _, m in ipairs(v.orig) do
if (not (m.Value == "P_Eve_Stance_BlockSword" or m.Value == "N_Drone_BlockScan")) then
newArray[j] = m
j = j + 1
end
end
return newArray
'''
As you can observe, an array is represented as a sequence in the patchlet languages (e.g., Scala Seq, Python list, JavaScript array) and an object as a map (e.g., Scala Map, Python dict, JavaScript object). The patchlet facility automatically converts between the TOML/JSON representation and each language’s native types: in the example above, v.orig[Seq[Map[String, Any]]] reads the EnterZoneEffects array as a sequence of maps (one per UAssetAPI property object), and the returned newArray sequence is converted back into the array form automod writes to the .uasset.
JSON Pointer and JSONPath
In addition to using regex and Scala code to find data table objects, you can also access any object via its JSON Pointer path. The path should be prefixed with .@:, for example, the following updates the distance of drone scanning in TargetFilterTable.uasset:
# Based on https://www.nexusmods.com/stellarblade/mods/802
['.@: /Exports/0/Table/Data/459']
FarDistance = 30000
TargetCheckValue1 = 3000
['.@: /Exports/0/Table/Data/460']
FarDistance = 30000
TargetCheckValue1 = 3000
Note that the JSON Pointer path should resolve to a JSON object:
- automod first checks whether it has an array field named
Value(i.e., a UAssetAPI JSON struct object); if so, it updates struct properties according to the UAssetAPI JSON schema. - otherwise, it updates the object property directly; an error is raised if the property name is not present (to prevent unintended patching).
While JSON Pointer is very precise for accessing objects in any JSON structure, a better alternative is to use JSONPath (with the specific implementation done using JsonPath).
For example, here is a (better) JSONPath equivalent to the TargetFilterTable.uasset modifications above:
# Based on https://www.nexusmods.com/stellarblade/mods/1660
['.@: $..[?(@.Name =~ /N_Drone_Normal_Scan1_1_Target.*/)]']
FarDistance = 30000
TargetCheckValue1 = 3000
Also, the following makes the same DifficultyStatGroupTable.uasset modifications as the Stellar Blade Hard Mode More Health and Shield for All Enemies – Hard Mode Enemies More Life and Shield x6 values by ElBibu256:
# Based on https://www.nexusmods.com/stellarblade/mods/802
['.@: $..Data[16:32]']
StatValue4 = '=> v.orig[Double] * 6'
StatValue5 = '=> v.orig[Double] * 6'
['.@: $..Data[60:87]']
StatValue4 = '=> if (v.objName == "324" | v.objName == "327") 420 else v.orig[Double] * 6'
StatValue5 = '=> if (v.objName == "324" | v.objName == "327") 420 else v.orig[Double] * 6'
Note that any text/character after .@: and before $ or / is ignored so it can be used for documentation.
Also, it is best to use regex/patchlet for patching data tables and .@: for everything else.
For example, the following hard-er mode patchlet is better than the .@: equivalent above:
# Based on https://www.nexusmods.com/stellarblade/mods/1156
['=> Multiply HP/SH on objects 17-32 and 301-327 by 6, and object 324 & 327 to 420/420.']
'=>' = '''v.toIntOption match {
case Some(n) => (17 <= n && n <= 32) | (301 <= n && n <= 327)
case _ => false
}'''
StatValue4 = '=> if (v.objName == "324" | v.objName == "327") 420 else v.orig[Double] * 6'
StatValue5 = '=> if (v.objName == "324" | v.objName == "327") 420 else v.orig[Double] * 6'
Choosing How to Target Objects
automod provides four ways to target objects, each suited to a different situation:
- Exact name (
[obj_Name]): matches a single data table entry by itsName. Simple and precise for one-off patches. - Regular expression (the
.*:prefix): matches many data table entries at once by theirName. Best for bulk, pattern-based patches over a data table. - Patchlet (the
=>/=ts>/=js>/=py>/=lua>prefixes): arbitrary code to select entries and/or compute property values. Use it when a regex cannot express the selection (e.g., comparing numbers parsed from the name), or when values must be derived fromv.orig,v.current, orv.valueOf. - JSON Pointer/JSONPath (the
.@:prefix): addresses any node in any JSON structure, by exact path or by query. Use it for non-data-table.uassets, or when the object cannot be reached by name.
In practice, prefer regex/patchlet for data tables, since their entries are naturally identified by Name, and .@: for everything else. Prefer JSONPath over JSON Pointer whenever you do not know the exact paths, and a patchlet whenever you need the original or current game value to compute a patch.