Choosing a surface
You are writing code for a DiceyTable table. There are two places to put it, they share no objects, and each one can do things the other cannot. Pick before you start.
- Table Scripting — TypeScript, written in the editor, saved inside the scene document. You
get
world,globalEventsand, in an object script,refObject. It runs on the host. - Mod Scripting — one JavaScript file in your mod's GitHub repository. You get
api, handed tosetup(api, manifest). It is gated by capabilities you declare in the manifest.
The decision table#
Find the thing you want to do. If it is on this list, the answer is not a preference.
| You want to | Surface | Because |
|---|---|---|
| Move, rotate or reposition an entity from code | Table | handle.setPosition / setRotation. Mod scripting has no transform method at all. |
| Remove an entity from the table | Table | handle.destroy(). delete is withheld from mods. |
| React to one specific entity | Table | refObject.onPickedUp, onRolled, onDropped — entity-scoped delegates. Mod hooks are table-wide. |
| Post a chat line as the table | Table | world.broadcast. |
| Pace a sequence, or poll | Table | world.wait, globalEvents.onTick. |
| React to chat, or to turn start and end | Table | onChatMessage, onTurnStarted, onTurnEnded. |
| Draw a panel, button, checkbox or text on the table | Mod | api.setUiElement. Table scripting has no UI surface. |
| Play a sound, or change an entity's sound | Mod | api.playSound, api.setObjectSound. |
| Read the contents of a deck or bag | Mod | api.getContainerContents. |
| Read what is in a seat's hand | Mod | api.getHandObjects. |
| Know which seat or team this player is in | Mod | api.getMySeat, api.getMyTeam — answered per peer. |
| Filter entities by several tags at once | Mod | api.listObjects({ tags, match }). world.getAllObjects takes one tag. |
split or combine a deck from code |
Mod | api.objectAction(id, "split"). Table scripting declares both and exposes no method for either. |
Two rows are worth reading twice. A mod cannot move or delete anything. It can flip, rotate, lock, shuffle, draw, deal, split, combine and roll — and that is the complete list. If your game needs a piece to end up somewhere specific, it needs a table script.
What each surface is really for#
Table Scripting is for rules. It runs on one peer, holds live handles, and reacts to entity events with entity-scoped delegates. It is the natural home for "when this die settles, add its face to the score", "when a card lands in this zone, flip it", "at the start of each turn, deal one".
Mod Scripting is for the table around the rules. It runs with a declared capability list, answers per-peer questions, and owns the two surfaces table scripting has none of — sound and on-table UI. It is the natural home for "show each player a score panel only they can see", "play a shuffle sound the game ships itself", "read the deck to build a draft display".
Nothing stops a mod shipping both: an editor-authored scene with scene scripts inside it, and
an entry.script JavaScript file beside it. They will not be able to call each other — they are
different frames on different surfaces with no shared memory — but they can both act on the same
table and observe each other's effects through the snapshot.
What you give up either way#
| Table Scripting | Mod Scripting | |
|---|---|---|
| Authored in | The editor's script editor, with full API autocomplete | Any editor. The API declarations exist and power this reference; wiring them into an editor for autocomplete has not been done. |
| Distributed as | Part of the scene document inside your mod | A .js file in your mod's repository |
| Language | TypeScript, transpiled on save | JavaScript, run verbatim |
| Runs on | The host only | Gated per method — see Host authority |
| Security model | A three-type intent allowlist, no capabilities | 11 declared capabilities, plus the same intent-level checks |
| Object model | Live ObjectHandles with methods and per-entity events |
Plain snapshot data plus api.objectAction(id, action) |
| Errors surface as | Script-console diagnostics with the script name and phase | Mod runner diagnostics |
| Actions available | 13 in the type union, 9 with a method | 10, all callable |
Both surfaces are scanned by the same five static patterns, and both run in the same kind of opaque-origin iframe. See Sandbox limits.
Names that look shared and are not#
The two surfaces were designed with different vocabularies on purpose, and the near-misses are the easiest way to write code that silently never runs:
| Table Scripting | Mod Scripting | Relationship |
|---|---|---|
globalEvents.onTurnStarted |
api.on("onTurnStart", …) |
None. Different names, different payloads. |
globalEvents.onObjectDropped |
api.on("onObjectDropped", …) |
Same name, different payload — the mod hook fires off an event-log line whose message starts "moved ", and carries the log event plus a snapshot, not a handle. |
globalEvents.onCardDrawn |
api.on("onCardDrawn", …) |
Same name, different payload, same caveat. |
globalEvents.onZoneEnter / onZoneLeave |
api.on("onZoneEnter", …) / "onZoneLeave" |
Same names, and for once the same four-string payload. The mod side needs read-world on top of subscribe-events, and both are host-only. |
globalEvents.onTriggerEnter / onTriggerLeave |
api.on("onTriggerEnter", …) / "onTriggerLeave" |
Same names, and the same five-field payload. Same rule as the zone pair: the mod side needs read-world on top of subscribe-events, and both are host-only. |
world.getAllObjects({ kind, tag }) |
api.listObjects({ kind, tag, tags, match }) |
Different filters. Do not assume parity. |
ObjectData.name |
TableObjectState.label |
The same underlying value — the slug — under two names. |
world does not exist in a mod's runtime, and api does not exist in a table script's. Pasting
code between them does not produce an error you can see in the editor; it produces a script that
throws on its first call.
The two surface values#
The surface badge says which declaration a member comes from. There is no third value and nothing belongs to both.
table-script#
Declared in the table-scripting declarations (packages/shared/src/scripting.ts). Reachable
through world, globalEvents or refObject.
mod#
Declared in the mod-scripting declarations (packages/shared/src/modScripting.ts). Reachable
through api, or through the exports.setup entry point.
A type used by both surfaces is documented once, on the surface whose reference page owns it, and cross-linked from the other.
See also#
- Host authority — which peer each surface runs on.
- Sandbox limits — the language subset and the scanner, which apply to both.
- Action vocabularies — the 13 and the 10, side by side.
- Anatomy of a mod — where each of these files sits in a repository.
- Known limitations — where the two surfaces are asymmetric by accident rather than by design.
- The mod
apiobject — Surface B's 25 methods, each with its capability. - Mod hooks and capabilities — Surface B's hook events and its 12 capability slugs.
- Types — Surface A's
Vec3,ObjectKind,SpawnObjectOptions,PlayerInfoandTurnInfo. - setup.json — what a mod places before either surface runs.
- Limits and caps — the numeric ceilings both surfaces hit.
- Scripting API — the reference index for both surfaces, once you have chosen.
- Events — the
globalEventsdelegates, which exist only on the table-scripting side. world— the global a mod does not have.
