Packs and rulesets
A pack is a versioned bundle of game data: an edition’s rules, a sourcebook’s content, a homebrew collection. A ruleset is what the engine makes from one or more packs, and every other engine call takes it. This page covers getting packs, loading them, and keeping rulesets around.
Getting packs
Section titled “Getting packs”A pack’s release is one JSON document: its manifest, merged rules, content, upgrade scripts and compiled formulas (pack-release.schema.json). That document is all the engine needs. Text lives next to it, one file per language.
| Where | What you get |
|---|---|
npm, @opencharacter/<pack id> |
The release as the default export, the default language as /i18n/<lang> |
npm, @opencharacter/<id>-i18n |
Every other language, one entry point each (@opencharacter/5e-srd-5.1-i18n/de) |
npm, @opencharacter/packs |
Every official pack’s release, by pack ID |
| GitHub Releases and jsDelivr | The same release.json and language files, for apps that don’t use npm |
| A homebrew author | The same format, under the author’s own npm scope or as a file |
Official and homebrew packs load the same way. To your app, a pack is a JSON document you fetched from somewhere.
import srd from '@opencharacter/5e-srd-5.1'; // the releaseimport en from '@opencharacter/5e-srd-5.1/i18n/en'; // only if you show EnglishLanguage files are separate entry points so your bundle includes only the languages you import. See Text and translation.
Licenses and credit lines
Section titled “Licenses and credit lines”Every pack declares a license. A CC-BY or ORC pack carries a credit line, and a pack whose license
asks for more than one notice lists the others by name in notices. The credit line and every
notice must reach your users, the notices after the credit line. All are in the ruleset:
for (const pack of ruleset.packs) { if (pack.credit) showInCreditsScreen(pack.id, pack.license, pack.credit); for (const [name, text] of Object.entries(pack.notices ?? {})) { showInCreditsScreen(pack.id, name, text); }}The 5E SRD pack is CC-BY-4.0, so an app that uses it must show its credit line somewhere its
users can find it (an About or Credits screen is the usual place). The orc-2e pack is under
the ORC License: its credit line is the attribution, and its notices are orc,
reserved-material and expressly-designated. mechanics-only packs carry
no rules text by design, but may still have a credit and notices: the FitD packs credit their
SRD (CC-BY), and the MÖRK BORG pack carries what its third-party license requires. Show them the
same way.
Loading: loadRuleset
Section titled “Loading: loadRuleset”const loaded = loadRuleset([coreRules, sourcebook, homebrew]);if (!loaded.ok) { // loaded.errors: [{ code, path, data }], e.g. missing-dependency, formula-error} else { const { ruleset } = loaded;}loadRuleset takes pack releases in load order and:
- checks each pack’s spec version against the engine (
spec-version-unsupported), and that every dependency is loaded and in range (missing-dependency); - merges the rules and content, keyed by qualified ID (
pack:id), so packs never collide; - applies
extends, overrides and hides; - resolves the queries that only look at content (“every martial weapon”);
- parses and type-checks every formula, the same checks the pack’s build ran.
Load errors are for whoever set up the game: a missing pack, a pack too new for your engine, a
broken homebrew formula. They carry an error code you can show with the spec’s standard text
(Text and translation).
Load order
Section titled “Load order”Your app decides the load order. Every dependency has to come before the packs that depend on it, and later packs win when two change the same thing. A sensible order:
- the edition’s core rules;
- official content packs;
- the table’s homebrew, in the order the GM chose.
When two packs override the same property of the same item, or the same setting, the later one
wins and the ruleset lists it in ruleset.conflicts
({ target, property?, packs }). Nothing is overridden silently, but nothing is rejected either.
Show conflicts to whoever chose the packs (a GM’s campaign settings screen), not to players
mid-session.
Which packs a character uses
Section titled “Which packs a character uses”A character file pins the exact versions of the packs it uses, in load order:
packs: - { id: 5e-srd-5.1, version: 0.1.0 } - { id: my-table-homebrew, version: 2.3.0 }Load exactly those releases, in that order, to compute the character. Two consequences:
- Keep old versions. A character pinned to
5e-srd-5.10.1.0 needs that release, even after 0.1.1 is out. Store releases byid@versionand keep any version a saved character still pins. Published versions never change, so a release you’ve fetched once is good forever. - Characters can need different rulesets. Two characters at the same table may pin
different versions until someone upgrades them. Key your ruleset cache by the character’s
packslist, not by game.
If the ruleset’s versions don’t match the character’s pins, computeSheet still computes the
sheet and reports pack-version-mismatch in its errors. Treat that as a reason to offer an
upgrade, not as a failure.
A campaign usually settles on one pack list, and new characters pin whatever versions the campaign uses. That’s an app decision; the format doesn’t model campaigns.
Caching rulesets
Section titled “Caching rulesets”loadRuleset does real work (merging, resolving queries, type-checking every formula), and its
output is plain JSON. So:
-
Load once, call many times. Keep the ruleset in memory for as long as the pack list is in use, and pass it to every call.
-
Cache by pack list. A key like
5e-srd-5.1@0.1.0,my-table-homebrew@2.3.0works. -
Skip the checks the build ran. The reference engine skips the formula checks of releases you name in
prechecked. Name only releases from your own bundle (the official packages), never one a user gave you:import orc2e from '@opencharacter/orc-2e';const loaded = loadRuleset([orc2e, tableHomebrew], { prechecked: new Set([orc2e]) }); -
Serializing is fine, within one engine version. You can store a ruleset or send it from a server to clients, as long as the same engine version reads it. The ruleset’s shape beyond its top-level properties is PROVISIONAL and engine-specific, so after upgrading the engine, or in a different engine or port, rebuild it from the pack releases.
Listing content
Section titled “Listing content”Some screens need content the character doesn’t own yet: a class picker, a shop, a spell list. Choices inside content come with their options (Building characters), but “which classes exist” isn’t a choice; it’s a catalog. Today your app reads it from the ruleset:
const classes = Object.values(ruleset.content).filter( (item) => item.type === '5e-srd-5.1:class' && !item.hidden,);// item.qid is the qualified ID to put in an addItem changeruleset.contentTypeslists every content type by qualified name, with its declared fields.ruleset.contentlists every item by qualified ID, afterextendsand overrides, withtype(the qualified type name),hidden(hidden by a later pack: leave it out) anddeprecated(still valid, but don’t offer it for new characters).- An item’s
fieldsare its raw values. Numbers and dice in them are formulas as written in the pack (usually plain literals like1d8).
The item shape inside the ruleset is PROVISIONAL, so code that reads it may need updating when the engine changes. There is no engine call for catalogs yet.