TOML Patch File Schema

Supported TOML form, JSON pointer, JSONPath, regex, and polyglot patchlet

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 ExtendDroneScanEffectExtend_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 whose Name property is the row’s name (e.g., N_Drone_Scan); this Name is what the [obj_Name] patch file sections match against.
  • The properties of that struct (e.g., LifeTime) are themselves objects located under the struct’s Value array. Each property object carries metadata such as $type, ArrayIndex, and Value, where Value holds the actual property value. For example, the LifeTime property of N_Drone_Scan is 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 scannerLevoire_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 EnemiesHard 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 its Name. Simple and precise for one-off patches.
  • Regular expression (the .*: prefix): matches many data table entries at once by their Name. 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 from v.orig, v.current, or v.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.