Hytalor - Asset Patcher
Hytalor is a light-weight asset patching framework designed to reduce mod conflicts by allowing multiple plugins to modify the same base game asset, without overwriting each other.
Instead of overwriting entire JSON files, Hytalor allows smaller patches which are merged together into a final asset at runtime.
How This Works
Hytalor works by leveraging Hytaleβs normal asset loading order, where later-loaded assets override earlier ones.
It creates a new AssetPack that loads after the base game and all other plugins.
Instead of replacing assets directly, Hytalor:
- Collects all patches targeting a base asset
- Creates a single, fully combined asset by copying the base asset and applying each patch in order
- Writes the combined result into the Hytalor AssetPack using the same asset path as the base asset
When Hytale loads assets, it detects this AssetPack last, causing the patched versions to override the originals automatically.
Whenever a patch changes, Hytalor rebuilds the combined asset. Hytale then detects the updated asset and reloads it using its normal asset refresh system.
β¨ Key Features
-
Conflict-Mitigation
Multiple mods can modify the same asset without ovewriting each other. -
Wildcard Asset Targeting
Apply a single Patch to many assets using wildcards. -
Smart Array Selection
Modify array elements using an index or special queries. -
Array Patch Operations
Supports merge, replace, add, and remove operations on arrays. -
Hot-Reloading
Changes to patch files are automatically resolved and applied when saved, like any other asset.
π¦ Usage/Examples
The patch files must be placed inside a directory:
Server/Patch/ in your assetpack.
For example
YourPlugin
βββ manifest.json
βββ Server
βββ Patch
βββ YourPatch.json
π§© Patch Files
Each patch file targets one or more asset by specifying entire paths, using wildcards, or even RegEx.
These Assets can also be from other mods. If a target asset is overwritten by another mod, this version will be used as the "base" asset for patching.
{
"_BaseAssetPath": "Server/Weathers/Zone1/Zone1_Sunny.json",
"Stars": "Sky/Void.png",
}
Only overrides the Stars texture in the Zone1_Sunny asset.
{
"_BaseAssetPath": "Server/Weathers/Zone1/*.json",
"Stars": "Sky/Void.png",
}
Overrides the Stars texture of every asset insde the Weathers/Zone1 directory
Regex can also be used instead of wildcards for more precise control.
{
"_BaseAssetPath": "regex:Server/Weathers/Zone[12]/.*Sunny.json",
"Stars": "Sky/Void.png",
}
This will override only Zone1_Sunny and Zone2_Sunny
It is also possible to specify an array of asset paths. And these paths can also use regex or wildcards:
{
"_BaseAssetPath": [
"Server/Weathers/Zone1/.*json"
"regex:Server/Weathers/Zone[23]/.*Sunny.json"
],
"Stars": "Sky/Void.png",
}
Priority
If your patch has to be applied before another patch of the same asset, you can use the _priority field.
Higher priority means it will be applied first. Default value is 0.
{
"_BaseAssetPath": "Server/Weathers/Zone1/Zone1_Sunny.json",
"_priority": 10,
"Stars": "Sky/Void.png",
}
π§ Array Patching
Hytalor recursivly merges JSON assets. Objects are merged by key, while arrays are modified using special control fields.
| Field | Description |
|---|---|
_index |
Select array element by index |
_find |
Selects first element matching a query |
_findAll |
Selects all elements matching a query |
_op |
The operation to apply at the matches element(s) |
| Operation | Description |
|---|---|
merge (default) |
Merge provided fields into the element |
replace |
Replace the entire element |
add |
Insert a new element (or append to end if no index / find) |
addBefore |
Inserts element before _index or matched elements (or adds to front if no index / find) |
addAfter |
Inserts element after _index or matched elements (or append to end if no index / find) |
upsert |
If _find matches existing element, merge into it, otherwise add new element at _index (or at end if not specified) |
remove |
Remove the element |
[!IMPORTANT]
The _index refers to the index at the time of the patch being applied, meaning that it may point to somewhere else if another patch happens before it.
It is therefore advised to use_findqueries instead if order is very important.
Example: Merge
Expand
When merging, only the provided fields are updated, and all other fields remain untouched.
The object is not replace, just partially modified.
π Base Asset (Before)
{
"Clouds": [
{
"Texture": "Sky/Clouds/Light_Base.png",
"Speed": 0.5,
"Opacity": 0.8
}
]
}
π§© Hytalor Patch
{
"_BaseAssetPath": "Server/Weathers/Zone1/Zone1_Sunny.json",
"Clouds": [
{
"_index": 0,
"_op": "merge",
"Speed": 0.7
}
]
}
β Resulting Asset (After)
{
"Clouds": [
{
"Texture": "Sky/Clouds/Light_Base.png",
"Speed": 0.7,
"Opacity": 0.8
}
]
}
Example: Add
Expand
{
"_BaseAssetPath": "Server/NPC/Roles/_Core/Templates/Template_Animal_Neutral.json",
"Instructions": [
{
"_index": 0,
"_op": "add",
"Continue": true,
"Sensor": {
"Type": "Any"
},
"Actions": [
{
"Type": "SpawnParticles",
"Offset": [
0,
1,
0
],
"ParticleSystem": "Hearts"
}
]
}
]
}
What this does
- Applies to the
Template_Animal_Neutralasset. - Adds new object to
Instructions, at begging of original array.
Using addBefore and removing the _index results in the same.
Example: Upsert
Expand
Upsert merges into the element queried for in _find if it exists. Otherwise adds a new element into the array at the specified _index or at end if not set.
π Base Asset (Before)
{
//...
"Categories": [
{
"Id": "Arcane_Spellbooks",
"Icon": "Icons/CraftingCategories/Arcane/Spellbook.png",
"Name": "server.benchCategories.spellbooks"
}
]
//...
}
π§© Hytalor Patch
"Categories": [
{
"_index": 0,
"_op": "upsert",
"_find": {
"Id": "Arcane_Spellbooks"
},
"Id": "Arcane_Spellbooks",
"Icon": "Custom.Icon",
"Name": "Custom.Name"
},
{
"_index": 0,
"_op": "upsert",
"_find": {
"Id": "Arcane_Staves"
}
"Id": "Arcane_Staves",
"Icon": "Custom.Icon",
"Name": "Custom.Name"
}
]
β Resulting Asset
{
//...
"Categories": [
{
"Id": "Arcane_Staves",
"Icon": "Custom.Icon",
"Name": "Custom.Name"
},
{
"Id": "Arcane_Spellbooks",
"Icon": "Custom.Icon",
"Name": "Custom.Name"
}
]
//...
}
Patching Primitive Arrays
Expand
Primitve arrays are a bit different to Patch, due to it expecting primitive values and not objects.
π Base Asset (Before)
{
//...
"Categories": [
"Furniture.Benches"
],
"Tags": {
"Type": [
"Bench"
]
}
//...
}
π§© Hytalor Patch
Adding to the end of the array is simple:
{
"_BaseAssetPath": "Server/Item/Items/Bench/Bench_Arcane.json",
"Categories": [
{
"_op": "add",
"_value": "Custom.Category"
}
]
}
Overwriting the array can be done using the Query system:
{
"_BaseAssetPath": "Server/Item/Items/Bench/Bench_Arcane",
"$.Tags.Type": [
"Custom.Type"
]
}
β Resulting Asset (After both patches)
{
//...
"Categories": [
"Furniture.Benches",
"Custom.Category"
],
"Tags": {
"Type": [
"Custom.Type"
]
}
//...
}
_find and _findAll Query examples.
Hytalor uses JsonPath queries to search inside arrays. More info here: (https://github.com/json-path/JsonPath)
Example: Merge Query
Expand
For merge operations, direct JsonPath assignment can be used:
{
"_BaseAssetPath": "Server/Weathers/Zone1/*",
"$.Clouds[*].Colors[?(@.Hour < 12)].Color": "#00EE00"
}
The exact same can be achieved using structed JSON as well:
{
"_BaseAssetPath": "Server/Weathers/Zone1/*",
"Clouds": [
{
"_findAll": "$[*]",
"Colors": [
{
"_findAll": "$[?(@.Hour < 12)]",
"Color": "#00FF00"
}
]
}
]
}
What this does
- Applies to every weather asset in
Server/Weathers/Zone1/. - Selects every cloud
- Then every color with an Hour value below 12
- Sets the color value
Example: Remove Query
Expand
{
"_BaseAssetPath": "Server/Weathers/Zone1/*",
"Clouds": [
{
"_findAll": "$[*]",
"Colors": [
{
"_findAll": "$[?(@.Hour < 12)]",
"_op": "remove"
}
]
}
]
}
What this does
- Applies to every weather asset in
Server/Weathers/Zone1/. - Selects every cloud
- Removes every color element with an Hour value below 12
Example: Adding
Expand
{
"_BaseAssetPath": "Server/Weathers/Zone1/*",
"Clouds": [
{
"_findAll": "$[*]",
"Colors": [
{
"_find": "$[?(@.Hour > 14)]",
"_op": "add",
"Hour": 14,
"Color": "#00FF00"
}
]
}
]
}
What this does
- Applies to every weather asset in
Server/Weathers/Zone1/. - Selects every cloud
- Finds first color with Hour > 14
- Adds new object before the found element
π Road Map
- Use value from an asset when assigning a new value, or when querying.
- Creating patches through code
