Packs
A pack is a versioned bundle of rules and/or content: a game edition, a sourcebook, a homebrew collection. Packs are plain files. Nothing in a pack is code.
Layout
Section titled “Layout”In this repository, packs are grouped by game: packs/<game>/<pack>/. A game folder holds
every pack for that game: its editions (5e/srd-5.1, later srd/srd-5.2) and any content-only
or homebrew packs. A pack’s ID is <game>-<pack> (5e-srd-5.1, mothership-1e,
fitd-scoundrel), and the build rejects an ID that doesn’t match its folders. Name pack
folders after their source (srd-5.1, 1e), not v1/v2. The edition is part of the ID; the
pack’s version counts data releases within it, starting at 0.1.0.
packs/<game>/<pack>/ pack.yaml # manifest rules/*.yaml # rules documents (any number, merged) content/<dir>/<name>.yaml # one content item per file i18n/<lang>.yaml # player-facing text, one file per language i18n/<lang>/*.yaml # or one language split over several files upgrades/<from>-to-<to>.yaml # upgrade scripts- Rules documents (
rules.schema.json) hold fields, link slots, content types, settings, situational choices, result bands, shared actions, always-on effects, character-level choices, constraints and display hints. Every section is optional. A pack may split its rules over any number of files; the build merges them, and an ID defined twice is an error. - Content items (
content-item.schema.json): exactly one item per file. The item’sidMUST match its path:content/weapons/longsword.yamlholdsweapons.longsword, andcontent/magic-items/weapon-plus-one.yamlholdsmagic-items.weapon-plus-one. Each directory segment becomes one dotted ID segment. - Translations (
i18n.schema.json): see i18n.md. Mechanics files contain no player-facing text. - Upgrade scripts (
upgrade.schema.json): see upgrades.md.
The build step turns these files into one pack release (pack-release.schema.json): { manifest, rules, content, upgrades?, i18n? }, which is what loadRuleset takes. In this repo, pnpm build:packs validates every pack and writes its release; pnpm validate runs the same checks without writing anything.
Manifest
Section titled “Manifest”pack.yaml (pack.schema.json):
| Property | Required | Meaning |
|---|---|---|
id |
yes | Pack ID: lowercase words joined by hyphens, dots allowed between them (5e-srd-5.1). Permanent. In this repository, <game>-<pack folder>. |
version |
yes | Semver, starting at 0.1.0. Each update gets a unique version; published versions never change. |
specVersion |
yes | The minimum spec version the pack needs. loadRuleset refuses a pack that needs a newer spec (spec-version-unsupported). |
license |
yes | SPDX identifier (CC-BY-4.0, CC0-1.0), ORC, OGL-1.0a, or mechanics-only. |
credit |
for CC-BY, ORC | The credit line (attribution) the license requires. Apps MUST pass it on to their users. |
notices |
for ORC, OGL | Other notices the license requires, by name (a slug → its text), in order. Apps MUST pass each on after credit. |
defaultLanguage |
yes | BCP 47 tag. The i18n file for it MUST exist. |
dependencies |
no | Pack ID → semver range. |
hides |
no | Qualified IDs of dependency content to hide from the ruleset. |
homepage |
no | URL. |
The pack’s display name is the translation key pack.name.
credit and notices are legal text in the language the license requires, so they live in the
manifest, not in i18n. Apps show them verbatim and MAY label a notice by its name. A pack with
license: ORC MUST have credit (the attribution of the Licensed Material used, and how the pack
wishes to be credited) and the notices orc (the ORC Notice), reserved-material and
expressly-designated (the Expressly Designated Licensed Material notice, saying so when there is
none); other notices MAY follow. loadRuleset lists each pack’s credit and notices in the
ruleset’s packs, and a release keeps them in its manifest.
OGL-1.0a is the Open Game License v1.0a (Wizards of the Coast), which has no SPDX identifier.
The license asks for its own full text with the work (its section 10) and the copyright
notices of the Open Game Content used (sections 6 and 15), so a pack with license: OGL-1.0a
MUST have the notices ogl-license (the license’s full text, verbatim) and section-15 (the
COPYRIGHT NOTICE: the license’s own line and each source’s, exactly as the sources print them,
then the pack’s own); it SHOULD name the Product Identity it mentions and say which parts are
Open Game Content (sections 7 and 8) in further notices.
mechanics-only means the pack carries mechanics and neutral labels only: no rules text, flavor
text or logos from the source game. Use it for games without an open license, or for a pack that
takes only mechanics and names from its source. Such a pack still carries what its source asks of
it: the credit of an open license whose material it uses (a CC-BY SRD’s attribution), and the
credit and notices a third-party license it is published under requires (a copyright line, a
not-affiliated statement). Apps pass them on as for any license.
- Content IDs are dot-separated segments of lowercase words joined by hyphens (
magic-items.flame-tongue). They appear in formulas only as strings. - Field IDs are dot-separated identifier segments with no hyphens (
abilities.str.mod,combat.critRange). Formulas refer to fields by bare name, so a hyphen would read as subtraction. - Qualified IDs add the pack:
5e-srd-5.1:weapons.longsword,5e-srd-5.1:abilities.str.score. - Inside a pack, a reference MAY be local (
weapons.longsword). It resolves first in the referring pack, then in its dependencies in declaration order. A local reference that matches in more than one dependency is ambiguous and MUST be qualified. - Character files always use qualified IDs (characters.md).
- The pack ID
homebrewis reserved for a character’s own homebrew (characters.md); no pack may use it. - IDs are permanent and never reused. Removed content is marked
deprecated(withsinceand optionallyreplacedBy), and an upgrade script moves characters off it.
Bare field names in formulas resolve the same way as local content references: the formula’s own pack first, then its dependencies. This resolution order is provisional.
Dependencies and load order
Section titled “Dependencies and load order”- A pack declares the packs it builds on in
dependencies.loadRulesetfails withmissing-dependencywhen one is missing or outside its range. - The app chooses the load order, and every dependency MUST load before the packs that depend on it. Later packs win.
- When two packs override the same property of the same thing, the later one wins and the ruleset reports the conflict in
conflicts, so nothing is overridden silently.
Overrides, hiding and extends
Section titled “Overrides, hiding and extends”- Override: a content file with
override: <qualified id>instead ofid/typechanges only the properties it mentions (“Longsword damage is 1d10”). Arrays (effects, tags, choices) are replaced as a whole. PROVISIONAL: finer-grained array patching is undecided. - Hide:
hidesin the manifest removes dependency content from the ruleset. Queries no longer match it. A character that owns hidden content gets a rules issue (content-hidden), not an error. - Extends:
extends: <ref>makes a permanent variant. The item inherits everything from its parent and then applies its own properties, with the same replacement rules as overrides.extendschains MUST NOT loop (circular-extends) and are limited by themaxExtendsDepthsafety limit. Overrides,extendsandhideswork on content items only. A new edition of a game is its own pack and doesn’t depend on the earlier one; a content pack depends on the one edition it was built for. - Templates are different: they are applied to a character’s copy during play (content.md).
Releases
Section titled “Releases”pnpm build:packs validates every pack and then builds each into its release (refusing invalid
packs):
-
dist/release.json: the pack release:- the manifest, the merged rules document, the content and every upgrade script;
- a
formulastable mapping each formula’s source text to{ text, tree }(its canonical text and parsed tree).
Formulas elsewhere stay as written. A port can parse them or look up their trees.
loadRulesetrefuses a release whose compiled tree doesn’t match its text. -
dist/index.js: the same release as a JavaScript module, the package’s main entry point (import pack from '@opencharacter/5e-srd-5.1'). -
dist/i18n/<default language>.{json,js}: the default language, an opt-in entry point (import en from '@opencharacter/5e-srd-5.1/i18n/en'). -
Other languages go in the pack’s
-i18npackage (<pack>/i18n-package/, published as@opencharacter/<id>-i18n). It has its own version, so translation fixes never bump the pack. The build refuses a pack that has other languages but no-i18npackage. -
@opencharacter/packsre-exports every official pack’s release by pack ID, and each pack’s default-language text by pack ID (import en from '@opencharacter/packs/i18n/en'). -
The spec’s standard text: the same build writes each language of the spec’s
i18n/<lang>.yaml(error, rules-issue, comparison and dice text, i18n.md) topackages/spec/dist/i18n/<lang>.{json,js,d.ts}, the entry point@opencharacter/spec/i18n/<lang>(import text from '@opencharacter/spec/i18n/en').
Build output (dist/) is never committed.
Validation checks
Section titled “Validation checks”Besides schema validation, a conforming pack:
- has a matching
idfor every content file path, and no duplicate IDs across the merged rules; - resolves every reference: content types,
extends, grants, choice item lists, query types, actionuse, result-band sets, display-hint fields and groups, effect target fields; - gives every content item’s fields values of their declared types, with no undeclared fields and no missing required ones;
- has a default-language i18n file containing every required key (i18n.md);
- has a
creditline when the license is CC-BY; - has a
creditline and the noticesorc,reserved-materialandexpressly-designatedwhen the license isORC. - has the notices
ogl-licenseandsection-15when the license isOGL-1.0a.