Guides › Development

Client patches

Why recipes

A plugin that adds a spell needs a row in Spell.dbc on both sides: the server must know the spell and the game client must show it. A plugin never ships game data. It ships a recipe, patches.json, that says which rows to add or change, and LonelyIce applies it to the player's own client files and to the server.

Recipes also solve id clashes. A plugin does not pick ids for new rows; it names them, and the installer hands out a free id per name. Two plugins can each add a spell without knowing about each other.

The reference is Patches: DBC rows, named ids, client files. This guide walks through the recipe of lonelyice.waystones.

1. Point the manifest at the recipe

"patches": "data/patches.json"

data/ is laid out by AddPlugin, so the file ends up in the plugin folder and the package.

2. Name the ids

"ids": {
  "translocation": { "table": "Spell.dbc", "copy": 44080 }
}

translocation is a name local to the plugin. It gets an id of Spell.dbc; the new row starts as a copy of stock spell 44080 (Teleport: Zul'Aman Instance), so only the differences have to be written. key (default 0) names the table's key field when it is not the first one.

The id is the next free one of the table: above the stock rows of the server's DBC file, above every id already given out, above every fixed id any installed recipe uses, and above the ids in the table's server override (spell_dbc). It is stored in the world database, table plugin_ids (plugin, name, dbc, id), and never changes while the plugin stays installed.

3. Describe the rows

"patches": [
  {
    "table": "Spell.dbc",
    "rows": [
      {
        "id": "@translocation",
        "comment": "Translocation: a 5 s cast with the Dalaran teleport icon, based on 44080 (Teleport: Zul'Aman Instance).",
        "set": {
          "28": 6,
          "133": 3167,
          "136": { "en": "Translocation", "ru": "Транслокация" },
          "153": { "en": "" },
          "170": { "en": "Translocates you to the chosen Kirin Tor waycrystal.", "ru": "Перемещает вас к выбранному путевому кристаллу Кирин-Тора." },
          "187": { "en": "" }
        }
      }
    ]
  }
]
  • "id": "@translocation" is the named row; it is added when missing (mode upsert). "@other.plugin/name" refers to another plugin's named id.
  • A numeric id changes a stock row (mode update, the default for numbers). "mode": "insert" or "upsert" adds a row with a fixed id, optionally with "copy": <stock id>. Prefer named ids for anything new.
  • set keys are field numbers of the table, as DBC editors number them. Here: 28 casting time index, 133 spell icon, 136 name, 153 rank text, 170 description, 187 tooltip.
  • Values: an integer, a float (1.5), true/false, a string, a localized string, or { "ref": "name" } for another named id. The key field itself cannot be set.
  • A localized string is an object of locale → text. It fills the 16 locale slots starting at the field number (136 to 151 here), so the field must have them. Each slot takes the exact locale (ruRU), then the language (ru), then en/enUS/enGB, then any text given. Write at least en; LonelyIce's own plugins always provide en and ru.
  • comment is ignored by the installer; use it to say what the row is.

4. Add the SQL that goes with it

"install": {
  "world": [
    "INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES ({{id:translocation}}, 'spell_custom_translocation')"
  ]
},
"uninstall": {
  "world": [
    "DELETE FROM `spell_script_names` WHERE `ScriptName` = 'spell_custom_translocation'"
  ]
}
  • Statements per database (world, characters, auth), in the AzerothCore SQL dialect, run through the core's database layer, so they work on every backend.
  • {{id:name}} is replaced by the plugin's id for name; {{id:other.plugin/name}} by another plugin's.
  • uninstall is stored in the database when the recipe is installed and runs when the plugin is removed, even though its files are gone by then. Make it undo exactly what install did.

Use install/uninstall for rows that depend on the ids. Tables and content that do not belong in the plugin's update SQL (Shipping SQL) are not undone on removal.

5. Add files, if any

"files": [ { "from": "client/files/waystone.blp", "to": "Interface/Icons/waystone.blp" } ]

Each file is copied into the generated client archive at to (forward slashes are fine). Addons do not go here; they use client.addons in the manifest and are copied into Interface/AddOns.

6. Read the id in server code

// The id of the Translocation spell, 0 while the plugin's patches are not installed.
uint32 TranslocationSpell()
{
    static uint32 const id = []
    {
        QueryResult r = WorldDatabase.Query("SELECT `id` FROM `plugin_ids` WHERE `plugin` = 'lonelyice.waystones' AND `name` = 'translocation'");
        return r ? r->Fetch()[0].Get<uint32>() : 0u;
    }();
    return id;
}

Never hard-code an id from your test server: another player's installation gives out different ones. Look the id up after the databases are open (a WorldScript::OnStartup hook, or the first use), not while scripts are created. The waystones spell script is bound by name through the spell_script_names row the recipe installs.

What the installer does

LonelyIce applies the recipes every time its server starts, after the database updates and before the world loads. For each installed plugin, in dependency order:

  1. Stamp. A hash of the recipe text, the plugin version and the installer version is kept in the world table plugin_patches. An unchanged plugin is skipped. A changed one is uninstalled and installed again, keeping its ids.
  2. Ids. New names get ids; names no longer in the recipe release theirs.
  3. Server rows. Rows of tables the core also reads from the world database are written there: Spell.dbc rows go to spell_dbc, each locale slot with its own locale. The server's extracted DBC files stay stock. Other tables are changed on the client only.
  4. SQL. The install statements run; uninstall and the removal of the server rows are stored.
  5. Client. When the launcher knows the game folder, it builds one archive per client locale, Data/<locale>/patch-<locale>-4.MPQ: the tables are read from the player's own stock archives, every recipe is applied in order, the files are added, and a marker file identifies the archive as LonelyIce's. The archive is rebuilt only when a recipe, a file, an id or the client's archives changed.

For a plain worldserver, run the same step by hand with the server stopped:

LonelyIce.exe --pkg apply -c <path>\worldserver.conf --client <game folder>

Without --client only the databases are updated.

Removal and disabling

When a plugin is removed or disabled (moved to plugins/.disabled), the next server start

  • runs its stored uninstall statements and deletes its server rows,
  • rebuilds the client archive without it, or deletes LonelyIce's archive when no recipes are left.

What happens to its ids depends on which of the two it was:

  • Disabled: its rows in plugin_ids stay (the log says plugin disabled, its patches removed (its named ids are kept)). Enabling it again reinstalls the recipe with the same ids, so ids stored elsewhere, such as spells characters learned, still fit.
  • Removed: its ids are released (named ids released); a later reinstall may get different ones. This also happens when a plugin is removed while it is disabled.

Data that stored the old ids, such as spells characters learned, is not cleaned up by the installer. Handle that in uninstall if it matters.

A plugin with server code whose library failed to load keeps its ids and server rows, but its recipe is left out of the client archive until it loads again.

The client archive

  • Its name is fixed: patch-<locale>-4.MPQ in the locale folder. All plugins share it.
  • An archive of that name that LonelyIce did not write (no LonelyIce marker inside) is never deleted or overwritten. Before LonelyIce writes its own, it moves that one to the first free backup name, patch-<locale>-4.MPQ.bak, then .bak2, .bak3, …, and logs <locale>: patch-<locale>-4.MPQ was not written by LonelyIce, kept as <name>. If it cannot be moved (the game is running, for example), nothing is written for that client and the patch step reports the error. With no recipes at all, the foreign archive is left where it is. Still, do not ship or hand-place a patch-<locale>-4.MPQ of your own: LonelyIce's archive takes its place.
  • While LonelyIce extracts maps from the client, it hides its archive so the server's data stays stock.

Testing a recipe

  1. Install the plugin, start the server and read the log lines starting with Plugin patches:. They list the ids given out, the plugins installed and the archives built. A failure is logged as Plugin patches failed: ..., naming the plugin and table.
  2. Check SELECT * FROM plugin_ids WHERE plugin = '<id>' in the world database.
  3. Start the game and check the row in the client (spellbook, tooltip).
  4. To reinstall after editing, change patches.json or the plugin version; any change of either reinstalls it.