Packaging & Distribution

A campaign file on its own is not installable. To ship one — to a friend, or to the Workshop — it needs a folder with a manifest.json beside it, and that manifest is what the campaign grid reads before anything is played.

Folder layout

One campaign is one folder containing exactly two files:

my-campaign/
  manifest.json     how it appears in the grid
  campaign.json     the campaign itself

Both names are required and neither is configurable. A folder missing either is skipped with a warning rather than half-loaded.

manifest.json

{
  "id": "the-sunken-crypt",
  "name": "The Sunken Crypt",
  "levelRange": { "min": 3, "max": 6 },
  "theme": "undead-horror",
  "tags": ["dungeon", "undead", "investigation"],
  "difficulty": "hard",
  "enabled": true,
  "minEngineVersion": "0.1.0"
}
FieldRequiredWhat it does
idyes Unique across everything installed. On a collision the first-seen wins and yours is skipped, so make it specific — the-sunken-crypt, not crypt. It must match the id inside campaign.json.
nameyes Shown on the campaign card.
levelRangerecommended { "min": n, "max": n }. Drives the level bracket a DM filters by.
difficultyrecommended Free text, shown on the card. The bundled campaigns use easy, medium, hard, very-hard — matching them keeps the grid readable.
themeoptional A short slug such as stealth-politics. Used for grouping.
tagsoptional Kept with the entry for search and future filtering. Nothing in the current UI renders them — they cost nothing and may start mattering, but do not rely on them being visible.
enabledoptional Set false to keep a campaign installed but hidden. Absent means enabled.
minEngineVersionoptional See below.
source— Ignored. The app sets this from where the pack was found. Writing it does nothing.

minEngineVersion

If your campaign uses something a older build cannot do, say so:

"minEngineVersion": "0.2.0"

A player whose app is older is told “needs Campaign Cookbook 0.2.0 or newer” and the pack is not loaded. Without it, the same player gets a campaign that appears to install and then behaves oddly at the table, which is far harder for anyone to diagnose.

Plain dotted numbers only — 1.2.0. There is no range syntax, no carets and no tildes. It answers one question (is this new enough?) and you should not have to learn a version grammar to answer it.

Leave it out if you are unsure. Only add it when you knowingly depend on something new. An unnecessary minEngineVersion locks players out of a campaign that would have worked for them.

Sagas: several campaigns in order

For a multi-campaign story meant to be played in sequence, add a saga.json at the top and put each campaign in its own subfolder:

my-saga/
  saga.json
  part-one/
    manifest.json
    campaign.json
  part-two/
    manifest.json
    campaign.json
{
  "id": "the-long-dark",
  "name": "The Long Dark",
  "minEngineVersion": "0.1.0",
  "campaigns": [
    { "id": "part-one", "order": 1 },
    { "id": "part-two", "order": 2 }
  ]
}

Each entry's id is the folder name. A minEngineVersion on the saga applies to every member unless that member sets its own.

Campaigns in one saga share a continuous world seed, so the towns, dungeons and NPCs a party met in part one are the same ones in part two. That is the reason to use a saga rather than shipping two unrelated campaigns.

What happens when it is installed

Every pack that did not come with the app is validated when it is found, not when it is played. A pack that fails never appears in the campaign grid at all — the alternative is a DM choosing it with players waiting and discovering the problem mid-scene.

Refused for any of:

Rejections are reported to the DM with the reason, so a pack that vanishes is never mistaken for one that failed to download. One bad pack does not stop the others loading.

Run it through the Testbed first. It uses the same validator the app does, so a campaign that passes there passes on install. That is the whole point of them sharing one implementation — the tool cannot bless something the app rejects.