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 obj
X_Name
matches the UAssetAPI’s JSON representation data table struct Name
property, 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 modify its LifeTime
property located at /Exports/0/Table/Data/722/Value/78
’s Value
property 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 as the corresponding .uasset
target filename, but it should be suffixed using the .toml
file extension instead.
For example, the above patch file example for Stellar Blade’s EffectTable.uasset
should be named as EffectTable.toml
.⠀
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
[ '=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/Python 3/Lua 5.2 code to match object names using the =>
/=ts>
/=js>
/=py>
/=lua>
prefixes, with the predefined variable v
holding the value of the object name string (#2
).
TOML does not allow a multiline (literal) string inside the square brackets for matching object names; so, an alternative syntax is supported to allow for a complex code to match them.
That is, the object name matcher is still prefixed by =>
/=ts>
/=js>
/=py>
/=lua>
, but what follows is (or can be) a description of the patchlet. The actual code is specified under a special property name =>
/=ts>
/=js>
/=py>
/=lua>
that maps to the Scala expression/code block to match object names (#3
).
Example #3
also shows how to update a property value based on its original game value: v.orig[Double] * 2
(or v.orig() * 2
for non-Scala ones), prefixed by =>
/=ts>
/=js>
/=py>
/=lua>
. 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 = '''=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()
) method retrieves the property value after the preceding patches have been applied, which can be used to incrementally arrange patches (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 are 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 run each time it is applied, thus it does take time to perform compared to regular exact match/value patches or regex-based.
As previously mentioned before, complex property value types are supported using TOML inline table and array syntax.
For example, the following patch file is what the Stellar Blade Altess Levoire and Abyss Levoire can use sword and scanner – Levoire_Use_Sword v1.0.0 mod by Violetkylin does, i.e., removing 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 is represented using the patchlet language sequence/array and an object as a map, and the patchlet facility takes care of the type conversions between the two representations for each supported patchlet language.
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, to make 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 use .@:
for other kinds.
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'