Skip to main content

Config migrations

The plugin runs config migrations at mod start, before any manual config (ZoneLevelConfig, InstanceLevelConfig, LevelRewardsConfig) is loaded. Two kinds of migrations are supported:
  • JSON content migrations (default) - read a config file, apply step ops to its JSON, write it back when the file’s Version is strictly less than MigrateVersionInferiorTo. A backup of the pre-migration content is written to <filename>.pre-migration so it is not overwritten by Hytale’s Config save (which uses .bak).
  • File-system migrations (Type: "file") - rename / move config files inside the plugin data directory. Idempotent on source presence: if the source path no longer exists, the step is a no-op and re-runs on future startups are safe. No JSON backup is written; the move itself preserves the file.
File migrations always run before JSON content migrations, so a JSON migration can target a file at its post-rename path.

When migrations run

  • When: During plugin startup, after ConfigManager is created and before ZoneLevelConfig.initialize(), InstanceLevelConfig.initialize(), and LevelRewardsConfig.initialize().
  • Which files: Config files resolved via ConfigManager.getConfigFile(fileName) or relative paths under the plugin data dir. Config file names must match exactly (e.g. RPGLevelingConfig.json, InstanceLevelConfig.json, ZoneLevelConfig.json, LevelRewardsConfig.json). File-system migrations may use subfolder paths (e.g. languages/MessagesLanguageMapping_english.json).

Folder layout

  • Index: src/main/resources/migrations/index.json - lists version folders and their migration filenames.
  • Per-version: src/main/resources/migrations/<version>/ - e.g. migrations/0.2.9/. Inside each version folder, place one JSON file per config migration (e.g. InstanceLevelConfigMigration.json).
  • Discovery: The runner loads the index, then for each version and each filename loads migrations/<version>/<filename> from the classpath. You must add each new migration file to the index.

Index format

{
  "0.2.9": ["InstanceLevelConfigMigration.json", "RPGLevelingConfigMigration.json"],
  "0.3.1": ["PassivesConfigMigration.json", "MessagesLanguageMappingMigration.json"],
  "0.3.0": ["PassivesConfigMigration.json"]
}
Keys are version strings; values are arrays of migration file names in that folder.

Per-migration file format

Each file under migrations/<version>/<name>.json has:
FieldDescription
TypeOptional. "json" (default when absent) for JSON content migrations, or "file" for file-system migrations.
ConfigFileNameRequired for json migrations. Exact config file name (e.g. InstanceLevelConfig.json). Not used by file migrations.
MigrateVersionInferiorToSemantic version (e.g. 0.2.9). For json migrations, runs when the config file’s Version is strictly less than this. For file migrations, this field is informational (idempotency comes from “source missing → no-op”). Should match the folder name for clarity.
StepsArray of step objects (see below). Applied in order.
Example:
{
  "ConfigFileName": "InstanceLevelConfig.json",
  "MigrateVersionInferiorTo": "0.2.9",
  "Steps": [
    {
      "op": "removeArrayElements",
      "path": "Instances",
      "arrayMatch": { "Id": "Default" }
    }
  ]
}

Step operations

Paths use dot-separated segments. Each segment is either a key (object member) or a numeric index (array element). This allows arbitrary depth (e.g. Rewards.2.Items.0.ItemId).

set

Sets a value at the given path. Creates parent objects if missing; for arrays the index must already exist.
  • "op": "set"
  • "path": "Version" - top-level key
  • "value": "0.2.9" - string, number, or boolean
  • "whenCurrentEquals" (optional) - if present and non-empty, the step runs only when the current string at path equals this value exactly. Use this to refresh a default message without overwriting customized text (e.g. translations).
Examples:
  • "path": "Version", "value": "0.2.9"
  • "path": "Instances.0.LevelMin", "value": 5
  • "path": "Rewards.2.Items.0.Quantity", "value": 10

remove

Removes a key or an array element at the given path.
  • "op": "remove"
  • "path": "SomeKey" - remove top-level key
  • "path": "Instances.0" - remove first element of Instances
  • "path": "Rewards.1.Items.2" - remove nested array element

removeArrayElements

Removes all elements in the array at path that match every key-value in arrayMatch. The path can point to an array at any depth.
  • "op": "removeArrayElements"
  • "path": "Instances" - array to filter
  • "arrayMatch": { "Id": "Default" } - object; each array element that has all these keys with these values is removed
Example for a nested array:
  • "path": "Rewards.0.Items", "arrayMatch": { "ItemId": "OldItem" }

renameKeyInArray

Renames a key in every object in the array at path. Skips non-object elements. No-op when from is missing on an element.
  • "op": "renameKeyInArray"
  • "path": "Overrides" - array of objects
  • "from": "Multiplier" - old key name
  • "to": "XpRateMultiplier" - new key name
Example (XPCurveOverridesConfig 0.3.7):
{ "op": "renameKeyInArray", "path": "Overrides", "from": "Multiplier", "to": "XpRateMultiplier" }

move (file migrations only)

Moves a file inside the plugin data directory. Covers both rename (same parent) and move-into-subfolder (different parent) in a single step. Parent directories of to are created on demand. Idempotent: when from does not exist the step is a no-op; when to already exists the step is skipped with a warning so existing files are never overwritten.
  • "op": "move"
  • "from": "MessagesLanguageMapping.json" - source path relative to plugin data dir
  • "to": "languages/MessagesLanguageMapping_english.json" - target path relative to plugin data dir
Example file migration (rename + move into a folder):
{
  "Type": "file",
  "MigrateVersionInferiorTo": "0.3.5",
  "Steps": [
    {
      "op": "move",
      "from": "MessagesLanguageMapping.json",
      "to": "languages/MessagesLanguageMapping_english.json"
    }
  ]
}

appendToCommaSeparated

Appends values to a comma-separated string at the given path without overwriting. Reads the current string, splits by comma, adds any values from value that are not already present, then writes back (joined with comma, no spaces). If the key is missing, treats current as empty and adds all values.
  • "op": "appendToCommaSeparated"
  • "path": "BlacklistedEntityRoles" - path to the string
  • "value": ["Pet_Follower", "Pet_Follower_Large", "Pet_Follower_Large_Combat"] - string or array of strings to add
Example: existing "Citizen_" becomes "Citizen_,Pet_Follower,Pet_Follower_Large,Pet_Follower_Large_Combat"; existing "Citizen_,MyRole" becomes "Citizen_,MyRole,Pet_Follower,Pet_Follower_Large,Pet_Follower_Large_Combat".

Version format

Versions are compared as semantic versions (e.g. 0.2.9, 1.0). Comparison is strictly less than: the migration runs only when config Version < MigrateVersionInferiorTo. After applying a migration, the config’s Version is set to that migration’s MigrateVersionInferiorTo so it is not applied again.

Config file names

Use the exact names from ConfigManager:
  • RPGLevelingConfig.json
  • InstanceLevelConfig.json
  • ZoneLevelConfig.json
  • LevelRewardsConfig.json
  • MessagesLanguageMapping.json

Example: remove Default instance

  • Config: InstanceLevelConfig.json
  • File: migrations/0.2.9/InstanceLevelConfigMigration.json
  • Index: "0.2.9": ["InstanceLevelConfigMigration.json"] in migrations/index.json
  • Condition: Migrate when Version < 0.2.9
  • Step: Remove from Instances any element with "Id": "Default":
{
  "op": "removeArrayElements",
  "path": "Instances",
  "arrayMatch": { "Id": "Default" }
}
After applying, the config’s Version is set to 0.2.9.