Skip to content

Upgrading characters

A character pins the exact versions of its packs. When a pack publishes a new version, nothing happens to existing characters until your app decides to upgrade them. This page covers when and how.

Only base state is saved, so most new pack versions (a fixed formula, a corrected item, new content, better display hints) need no upgrade work at all. The character’s saved state means the same thing under the new version; only the computed sheet changes.

Some updates do change what saved state means: an item renamed or removed, a choice split in two, a new required choice. Those versions have a breaking version bump (the minor version while a pack is 0.x, such as 0.1.4 → 0.2.0; the major version from 1.0.0, such as 1.4.2 → 2.0.0) and ship an upgrade script, written by the pack’s author, that moves characters across. The pack build refuses to release such a version without one, so your app never has to work out what a change meant.

Upgrade scripts are data in the pack release, not code. A pack’s latest release carries all of its scripts, so any older pinned version can be upgraded from it.

const result = upgrade(oldRuleset, newRuleset, character);
if (result.ok) {
showReport(result.report); // let the player review
if (playerAccepts) {
character = result.character; // now pins the new versions
askAbout(result.pending); // new required choices, if any
}
} else {
// result.errors: unknown-id (a pinned pack isn't in the new ruleset), no-upgrade-path
}
  • oldRuleset is the ruleset the character was saved with: its pinned versions. That’s one reason to keep old pack releases around (Packs and rulesets).
  • newRuleset is the ruleset to move to. Every pack the character pins must be in it; packs the character doesn’t use are ignored.
  • For each pack, the engine chains the upgrade scripts one version at a time (1.2 → 1.3 → 2.0), skipping versions that changed nothing.

Upgrading is deterministic and never asks the player anything. New required choices come back in pending, for your app to put to the player the usual way (Building characters).

The engine computes the character under both versions and reports the differences:

"report": {
"values": [
{ "field": "5e-srd-5.1:combat.ac", "before": 16, "after": 17 }
],
"steps": [
{ "pack": "5e-srd-5.1", "from": "0.2.1", "to": "0.3.0",
"step": { "op": "renameContent", "from": "feats.sharpshooter", "to": "feats.sharp-shooter" } }
],
"errors": []
}
  • values: every field whose value changed, with before and after. A field that exists on one side only has only one of them.
  • steps: each upgrade step that changed this character, and the script it came from.
  • errors: anything in the upgraded character that still doesn’t resolve (a reference no script handled). Show these; the character still computes without the broken part.

Show the report before the player accepts: “Armor Class 16 → 17; Sharpshooter renamed to Sharp Shooter”. Name fields and content with their translated names.

That’s an app decision, and different apps choose differently:

  • Ask the player. Show “update available” with the report, and upgrade on accept. Good for personal characters.
  • Upgrade the campaign. The GM moves the campaign to new pack versions, and every character in it is upgraded together, so the table plays by the same rules.
  • Never, for finished things. A character from a completed campaign can stay pinned forever. It keeps computing exactly as it did.

Since upgrade doesn’t change anything until you save its result, you can also run it just to preview what an update would do.