Background

Hytalor

ModsHytalor
Hytalor

Hytalor

CurseForge
Library

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.

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 _find queries 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_Neutral asset.
  • 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
Hytalor - Hytale Mod | Hytale Wiki