The Manifest
Game Variants
Offer one game pack in several set-ups, from board sizes to a fully scripted ranked version. The manifest list, the setup.json block and what is refused.
On this page12 sections
A variant is the same game, set up differently. Go on a 9×9, a 13×13 or a 19×19 board. A six-seat and a ten-seat table with the same cards. A scripted game, and the same entities with no scripts attached so people can play it by hand. A fully scripted version whose result can be ranked. One game pack carries all of them, and the room's owner picks one in the lobby before the table exists.
You do not need a script to offer variants, and you do not write any of the JSON on this page by hand: the editor's variant controls produce both halves. This page is the reference for what they produce — read it when you are reading a diff, debugging a refused publish, or deciding whether a variant is the right tool.
If you want the walkthrough instead, it is Making a variant of your game.
Variants or setup options?#
Both are chosen in the lobby by the room's owner, and they answer different questions.
| Variant | Setup option | |
|---|---|---|
| What it is | A different scene — entities, table, seats, zones, snap points, scene scripts | A typed answer — a number, a toggle, a choice, a short text |
| Who acts on it | The platform, before any script runs | Your script, in setup(api, manifest, options) |
Needs entry.script |
No | Yes — a scriptless game is refused for declaring one |
| Can change the table, the board or the seats | Yes | Only as far as your script can spawn and move things |
| Can change the player range | Yes, per variant | No |
| Can run different scripts | Yes — script attachments are part of each variant's scene | No |
| How many | Up to 12, one chosen | Up to 12 options, each answered |
| Visible to your script | Yes — see What a script can see | Yes |
Rule of thumb: if the table itself is different, it is a variant; if the same table is played with a different number, it is a setup option. "Hand size: 5 or 7" is an option. "The four-player board or the six-player board" is a variant. A game may declare both, and the lobby shows the variant first, because the variant decides what the game is and the options are answers handed to whatever script that variant runs.
The two halves#
A game's variants are recorded in two documents, because two different readers need them.
- The manifest (
diceytable.mod.json) holds the list: each variant's id, name, description, player range, whether it is fully scripted and how it is ranked. The registry stores the manifest, so this is the half a lobby, the server and a listing page can read without fetching your scene. - The scene (
setup.json) holds what each variant changes. The lobby never reads it; the host does, once, when the table starts.
The two are joined by the variant's id, and they must agree — see
What publishing refuses.
The manifest list#
{
"schemaVersion": "1.0",
"id": "example.go",
"name": "Go",
"version": "1.0.0",
"type": "game-pack",
"entry": { "setup": "setup.json" },
"compatibility": { "engine": ">=0.1 <1.0" },
"license": "MIT",
"players": { "min": 2, "max": 2 },
"variants": [
{ "id": "19x19", "name": "19×19", "description": "The full board." },
{ "id": "9x9", "name": "9×9", "description": "A quick game on the small board." }
]
}
This game has no entry.script and declares variants anyway. That is legal — it is the main
difference from setup options.
| Field | Required | What it is |
|---|---|---|
id |
yes | The stable identifier: what a room stores as the owner's choice, and the key the scene files this variant's differences under. Lowercase letters, digits and single dashes, 1–40 characters; constructor and prototype are refused. Unique within the list. |
name |
yes | What the lobby's dropdown shows. Plain text, 1–60 characters. |
description |
no | One line shown under the dropdown and in the lobby header. Plain text, up to 240 characters. |
players |
no | { "min", "max" } — this variant's player range when it differs from the game's. See A different player count. |
rulesEnforced |
no | true makes the variant Fully scripted: its script enforces the rules and reports the result. See Fully scripted and ranked. |
ranking |
no | Ranked-play settings, every one with a default. Only on a fully scripted variant. See Fully scripted and ranked. |
An entry is strict: an unknown key is a parse error, not a silently ignored one. The per-field detail is in the manifest reference.
Three rules about the list as a whole:
- The first entry is the lobby's default. A room whose owner never touches the dropdown plays it. So does a room that stored an id your game has since removed — an old room record opens on the first variant rather than failing.
- At most 12. This is one dropdown in a lobby; a dropdown with thirty entries is a catalogue.
- Game packs only. A room, table or asset pack is never the mod a lobby resolves as the room's game, so a list on one is offered to nobody and publishing it is refused.
A list with fewer than two entries is not a choice. The editor removes the key when you delete
your way down to one variant, and an absent variants means what it always has: one way to play.
The setup.json block#
The scene stores variants as a base plus differences:
- The top level of
setup.jsonis the base variant. It is a complete, ordinary scene.variants.basesays which declared id that scene is. - Every other variant is stored as what it changes about the base, under
variants.deltas[<id>]. The base never has an entry there.
Two non-base variants never share with each other. What they have in common is the base. That is the whole model: "the base, and how this one differs from it".
Here is the Go game above, with 19×19 as the base and a 9×9 variant that removes the big board, adds a small one, and has its own snap grid:
{
"schemaVersion": 2,
"sceneId": "go",
"name": "Go",
"environment": {
"tableSurfaceColor": "#2e5d4b", "tableSurfaceTextureAssetId": null, "ambientColor": "#ffffff",
"fogEnabled": false, "fogColor": "#000000", "fogDensity": 0, "skyboxAssetId": null
},
"objects": [
{
"id": "board-19", "kind": "board", "label": "board-19", "displayName": "Board 19×19",
"position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1.5, "y": 1, "z": 1.5 }, "color": "#d9b36c", "ownerSeat": null,
"faceDown": false, "locked": true, "stackCount": 1, "metadata": {},
"velocity": { "x": 0, "y": 0, "z": 0 }, "angularVelocity": { "x": 0, "y": 0, "z": 0 }
}
],
"snapPointGrids": [
{
"id": "grid-19", "label": "Board 19×19", "center": { "x": 0, "y": 0.05, "z": 0 }, "rows": 19, "cols": 19,
"spacingX": 0.075, "spacingZ": 0.075, "rotationY": 0, "snapRadius": 0.03,
"ownerSeat": null, "metadata": {}
}
],
"variants": {
"base": "19x19",
"deltas": {
"9x9": {
"removed": ["board-19"],
"objects": [
{
"id": "board-9", "kind": "board", "label": "board-9", "displayName": "Board 9×9",
"position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.75, "y": 1, "z": 0.75 }, "color": "#d9b36c", "ownerSeat": null,
"faceDown": false, "locked": true, "stackCount": 1, "metadata": {},
"velocity": { "x": 0, "y": 0, "z": 0 }, "angularVelocity": { "x": 0, "y": 0, "z": 0 }
}
],
"scene": {
"snapPointGrids": [
{
"id": "grid-9", "label": "Board 9×9", "center": { "x": 0, "y": 0.05, "z": 0 }, "rows": 9, "cols": 9,
"spacingX": 0.075, "spacingZ": 0.075, "rotationY": 0, "snapRadius": 0.03,
"ownerSeat": null, "metadata": {}
}
]
}
}
}
}
}
Delete the variants key and what remains is a valid scene: the 19×19 game. Playing 9x9 yields
one entity, board-9, and the 9×9 grid.
What a variant's differences can hold#
Every part is optional. An empty object — or no entry at all — means "this variant is the base, unchanged".
| Key | Holds | Meaning |
|---|---|---|
removed |
Entity ids | Base entities this variant does not have. |
objects |
Whole entities | Entities this variant has its own version of. An id the base also holds is that entity customised here and replaces the base's in place; an id the base does not hold is an entity added here. Up to 2,000. |
order |
Entity ids | This variant's own entity order. Written only when the natural order (the base's, with additions at the end) would not reproduce what you arranged. |
scene |
Scene keys | This variant's own copies of the keys in the next table. |
Entities are handled one at a time, which is what keeps a variant that changes one board from forking the whole entity list: the 200 stones the two variants share are stored once, in the base, and a fix to one of them reaches both.
What a variant can have its own copy of#
Everything else in the scene is grouped the way you think about it while authoring. The editor uses these four group names; the keys are what you will see in the file.
| Group | Keys in scene |
Covers |
|---|---|---|
| Table & room | packRefs, environment, room |
The assigned table and room packs, the table surface, lighting and sky |
| Seats & zones | seatZones, seatTemplate, tableZones, seatVariables, zones, roomSettings |
Seats, the seat template, hand and table zones, and the player-count rules |
| Snap points | snapPoints, snapPointGrids |
Snap points and snap-point grids |
| Scene scripts | sceneScriptIds, sceneScriptVars |
Which scripts are attached to the scene, and their variable values |
Two things about these that are easy to get wrong:
- A key is copied whole. Unlike entities, these are not merged item by item. Once a variant has
its own
snapPoints, it has its own list of snap points, and a snap point you later add to the base does not appear in it. The editor's Variants dialog shows which groups a variant holds its own copy of, and can drop the copy so the variant follows the base again. nullis a value. A key that is absent fromsceneis inherited from the base. A key set tonullmeans "this variant does not have one" — it is how a variant says the base assigns a table pack and it assigns none. (environmentis the exception: a scene always has one, so it can be replaced but not removed.)
What is always shared#
scripts, materials, textures and prefabs are libraries, and every variant reads the
same ones. A variant chooses which scene scripts are attached (sceneScriptIds); it never has
its own copy of a script's source. If it did, a fix made in one variant would silently not reach
the others.
The Hierarchy's folders are shared too, and so is the document's identity (schemaVersion,
sceneId, name).
The base is not the default#
These are two separate facts, and they live in two separate documents.
- The default is the first entry in the manifest's list — what a lobby starts on.
- The base is
variants.basein the scene — the variant the top level ofsetup.jsonis, and the one every other variant is stored as a difference from.
They are usually the same variant, because the editor makes your existing scene both when you create your first variant. They do not have to be. Reordering the lobby's list changes the default and touches nothing in the scene. Make this the base in the Variants dialog changes the base and touches nothing a player sees: the file is rewritten so a different variant sits at the top level, and every variant still plays exactly what it played before.
Pick the base for the reason in Backwards compatibility: it is the scene a reader that has never heard of variants will load.
Playing by hand#
A by-hand variant is one whose scene attaches no scripts. There is no flag for it: which scene
scripts a variant attaches (sceneScriptIds) is one of its scene keys, and an entity's script is part
of the entity, so a variant that detaches them runs none of them. The editor does this per variant
with Changes apply to: Only variant selected; see
the same game, by hand.
Two scripts a variant cannot detach, so check them:
- The entry script (
entry.script) runs in every variant. If it should do nothing by hand, it readsapi.getVariant()and returns. - A prefab's script. Prefabs are a shared library, so a piece spawned from a scripted prefab runs its script in every variant.
A variant whose scripts do not register a bot's handler cannot seat that bot: the lobby, the
table's Bots panel and the API refuse it (bot-variant-no-handler), and switching a lobby to the
variant removes the bots already seated. Anything your scripts would have dealt or spawned is not
there either, so author it into that variant's scene.
scripts: falsehas been removed. The old flag switched every script off at once. A manifest that still carriesscriptson a variant is refused withvariant-scripts-removed. Opening such a draft in the editor converts it once: the variant's scene and entity scripts are detached in its own scene, the key is dropped, and the editor reminds you about the entry script.
Fully scripted and ranked#
{
"id": "standard",
"name": "Standard",
"description": "Rules enforced. Can be played ranked.",
"rulesEnforced": true,
"ranking": { "placementGames": 5 }
}
rulesEnforced: true says the variant's script is the referee: it enforces the rules and
reports the result with match.end. The lobby shows a Fully
scripted badge beside it.
ranking offers ranked play. Every field has a default, so "ranking": {} is complete; the
fields are in the manifest reference. The lobby shows
Ranked available, and the room's owner may switch Ranked on. Only a fully scripted variant
may carry it, and a script attached to the variant must call match.end(, or publishing refuses it.
What ranked play guarantees, and how Scores and Player Trust work, is on
Ranked games, Rankings and Player Trust.
The same script serves ranked and casual play: in a room that is not ranked, its matches simply play unranked.
A different player count#
{ "id": "ten-seat", "name": "Big table", "players": { "min": 2, "max": 10 } }
players on a variant replaces the game's own players range while that variant is chosen. A
variant without it uses the game's.
The range is enforced where the room's owner sets the seat cap: the lobby accepts only a cap inside the chosen variant's range, and the server checks the same write against the same range. Changing to a variant that cannot seat the current cap clears the cap and says so, rather than clamping it. The platform seats at most ten players whatever a manifest declares.
Declaring the range does not build the seats. A ten-seat variant also needs its own Seats & zones — and usually its own table — in the scene, or ten people will be offered a table laid out for six.
What a script can see#
Both script surfaces can read which variant is being played, read-only and synchronously:
- Table Scripting:
world.getVariant(). - Mod Scripting:
api.getVariant(), under theread-contextcapability.
Both answer { id, rulesEnforced, ranked }: the declared id (or null for a game with no
variants), whether the variant is fully scripted, and whether this table plays ranked. The variant is
public, so reading it reveals nothing hidden. The manifest argument of setup(api, manifest, options) still carries no variant, and options carries setup-option answers only.
Branch on id when one script is attached in several variants. You can still give a script settings
per variant without code: sceneScriptVars is a per-variant key, so one script attached in two
variants can have different variable values.
In the lobby#
For a game with two or more variants, the room's owner gets a Variant dropdown under Game
options, above the setup options. It lists the variants in manifest order and saves the moment
it changes — there is no Save button. Under it: the variant's description, the range it seats,
and the Fully scripted and Ranked available badges where they apply. When the chosen variant
offers ranked play, the owner also gets the Ranked switch.
Everyone in the lobby, owner or not, sees the choice in the lobby header: Variant: 9×9, with the variant's own player range and badges, and its description below.
The choice belongs to the room, and three things about it are fixed:
- Only the owner can change it. Anyone else's write is refused.
- It locks at Start. The host resolves the scene for the variant the room started with, so the dropdown is disabled afterwards and the server refuses a later change.
- It does not survive a change of game. A variant chosen for one game is dropped when the owner switches the lobby to another, even if the new game happens to declare the same id.
What publishing refuses#
Each variant's own content is validated when setup.json is parsed, exactly as the base scene is:
a malformed entity inside a variant fails the scan the same way a malformed entity at the top
level does. On top of that, four rules exist because the list and the differences live in two
documents and can disagree. All four are errors, so the mod registers as incompatible. Ranked
variants have four rules of their own.
| Code | What you did | Why it is refused |
|---|---|---|
variants-unread |
Declared variants on a room, table or asset pack |
Only a game pack is ever a room's game, so nobody would be offered the choice |
variant-undeclared |
setup.json records differences for an id the manifest does not list |
Nobody could ever choose it |
variant-base-undeclared |
variants.base names an id the manifest does not list |
The scene itself would belong to a variant nobody can choose |
variant-scene-without-manifest |
setup.json records differences, and the manifest declares no variants at all |
Same as above, for the whole block |
The reverse is not an error. A variant the manifest lists and the scene says nothing about is
the base scene unchanged: a variant that differs only in its name, its player range or
rulesEnforced is written that way.
The editor changes both halves together, so you meet these refusals only after editing one of the
files by hand (or after a merge brought in one file without the other). The fix is always to make
the ids match. The editor never drops a variant's differences on its own — a manifest that fails
to read looks exactly like one that declares no variants, and that must not cost you a variant —
so for differences the manifest no longer lists, open ▸ : the dialog names them
and offers Remove them. If you meant to keep the variant, add it back to the manifest's list
with the same id instead.
The scene's own shape has two limits of its own, reported as a schema failure rather than under a
code above: at most 12 variants may have differences recorded, and the base may not also appear
under deltas.
Backwards compatibility#
The top level of setup.json is the base variant, and it is a whole game. A reader that has
never heard of variants — an older copy of DiceyTable, a tool that counts a pack's entities — skips
the variants key and loads a complete, playable scene. It does not see three boards stacked on
one table, and it does not see an empty one. That is the reason the format is "a base plus
differences" rather than "a list of scenes".
The same goes for the manifest: variants is optional, an older reader ignores it, and a manifest
without it is unchanged.
What an older reader cannot do is offer the other variants. So make the base the variant you would want someone to get if they could only get one.
A game that declares no variants is untouched by any of this. Its setup.json has no variants
key, and its manifest has no list.
See also#
- Making a variant of your game — the walkthrough in the editor.
- The variant controls and the Variants dialog.
- Manifest reference — the generated field tables for
variants. - Setup JSON — where the block sits in the scene document.
- What gets rejected — the four refusals, with their exact messages, and the ranked-variant rules.
- Matches and Variants —
world.getVariantand thematchglobal. - Limits and caps — every number on this page in one table.
