Dicey Table

Intents

A TableIntent is a single described mutation of table state. In the host-authoritative model, a player or spectator never mutates shared state directly — it sends an intent to the host, and the host's IntentDispatcher (apps/web/src/playcanvas/intent/IntentDispatcher.ts) applies it and broadcasts the resulting TableSnapshot.

Verified count: 26#

TableIntent (packages/shared/src/tableObjects/intents.ts) is a union of 26 member types. The dispatcher's switch (intent.type) is exhaustive over the union (TypeScript's never check at its default case proves it — IntentDispatcher.ts), and counting its case labels gives the same 26: 22 handled directly, 1 (combine-selection) deliberately delegated elsewhere, and 3 declared-but-not-implemented.

This page previously said 24 and listed two Zone* intents. Both were wrong at the same time and they cancelled out, which is why the total looked stable: the replicated TableSnapshot.zones model was retired on 2026-07-30 (zone authoring is Edit-Mode-only now), taking ZoneUpsertIntent/ZoneDeleteIntent with it, while DeckLoadIntent, CardOverlaySetIntent, SurfacePressIntent and SeatHoldersReconcileIntent had been added and never documented. Source wins; the sections below are counted against it.

By area#

Object lifecycle and manipulation (7)#

Intent Fields What it does
SpawnIntent object: TableObjectDefinition Creates a new entity.
ReparentIntent objectId, parentId: string | null, mode?: "attached" | "weld" Re-parents an entity (or detaches it to the root with parentId: null). Host-validated against cycles, self-parenting, missing parents and over-deep chains. Transform and organization only — not a physics constraint; see Joint for that.
ObjectActionIntent objectId, action (one of 19) Applies one of the 19 object actions.
CombineSelectionIntent objectIds: string[], faceDown: boolean Merges a whole held card/deck multi-selection into one shuffled deck (the shake-to-combine gesture). Handled directly in TabletopRuntime.applyIntent — not in IntentDispatcher — because it needs the full object lifecycle (merge, destroy, createObject). Host-authoritative specifically so the merge is seeded and shuffled once, rather than every client shuffling to its own order.
DragIntent objectId, position, rotation?, release?, noSnap? Streams (or commits, with release: true) a drag. rotation is optional so a non-host drop can still commit a final orientation without waiting on the last drag-update tick.
TransformIntent objectId, position?, rotation?, scale?, release? Sets an entity's transform directly (editor gizmo moves), independent of the drag gesture.
DeckLoadIntent source, deckId?, deckVersion?, name, decklist, defaultPartitionId?, partitionOrder?, position, faceDown? Materializes a saved deck as one deck pile per partition — never as N loose cards. The decklist travels inside the intent, because deck rows are owner-authoritative and default to private, so the host cannot fetch someone else's deck. That makes this the one intent whose size a remote peer chooses: it is capped at 600 lines / 1,000 cards per pile / 2,000 per load / 10,000 card entries on the table, and any breach refuses the whole load rather than truncating it. Refused for a spectator unconditionally — ahead of even spectatorCanInteract — and for a player without playerCanSpawn. A spawned pile carries card ids only.

Cards (1)#

Intent Fields What it does
CardOverlaySetIntent objectId, target? ("value" | "counter"), key?, op, value? Sets, adds to, clears or resets one overlay value or counter count on a card. Only the mutable VALUES replicate — the regions, formats and counter art that render them are resolved locally from the mod's data/cardSchema.json. Refused for any key the schema does not declare, in the host's handler.

Prop surfaces (1)#

Intent Fields What it does
SurfacePressIntent objectId, surfaceId, u, v Reports a pointer press on a prop surface as a surface-normalised hit point. Deliberately not a widget id: the HOST re-resolves (u, v) against its own copy of the layout, so a peer cannot fabricate a press on a widget that is not there.

Host-only (1)#

Intent Fields What it does
SeatHoldersReconcileIntent seats?, reason Re-fits each seat's authored furniture to whoever is actually sitting. Not a request a peer may makeisIntentAllowedForParticipant drops it from any non-host sender before any permission is consulted, because unlike every other variant it creates objects and can evict a seat's rack.

There is no ZoneUpsertIntent/ZoneDeleteIntent. The replicated TableSnapshot.zones model was retired on 2026-07-30; seat zones are authored in Edit Mode and ride the scene, not the intent stream.

World-space and screen UI (2)#

UiElementUpsertIntent { modId, element: TableUiElementDefinition } and UiElementDeleteIntent { modId, elementId } — a mod's on-table UI panel content.

Snap points (2)#

SnapPointUpsertIntent { snapPoint: TableSnapPointDefinition } and SnapPointDeleteIntent { snapPointId }.

Vector lines (2)#

VectorLineUpsertIntent { vectorLine: TableVectorLineDefinition } and VectorLineDeleteIntent { vectorLineId } — freehand/drawn line annotations.

Decals (2)#

DecalUpsertIntent { decal: TableDecalDefinition } and DecalDeleteIntent { decalId }.

Text labels (2)#

TextLabelUpsertIntent { textLabel: TableTextLabelDefinition } and TextLabelDeleteIntent { textLabelId }.

Joints (2)#

JointUpsertIntent { joint: TableJointDefinition } and JointDeleteIntent { jointId } — create/remove a physics constraint (fixed / hinge / spring) between two independently dynamic bodies. Not parenting — see the parenting/joint/weld split.

Sound (1)#

SetObjectSoundIntent { objectId, action: SoundAction, ref: SoundRef | null } — sets, replaces, or clears (ref: null) an entity's per-event sound override. A mod may only author a { kind: "builtin"; material } or { kind: "mod"; modId; name } ref pointing at its own declared sound — never a raw first-party clip id; that constraint is enforced at the sandbox surface before the intent is ever dispatched.

Declared but not implemented (3) ⚠#

These three types are real members of the TableIntent union — they will type-check and they appear in apiSymbols-style introspection of the type — but none of them is author-reachable and none of them does anything. IntentDispatcher.ts handles all three identically: it logs Intent type "<type>" is not implemented in gameplay runtime dispatch yet. and returns without mutating state or emitting a snapshot.

Intent Fields Status
EditEntityPatchIntent patch: EntityPatch Declared, unimplemented. Survives only as a type after its backing mechanism (EditSceneSnapshot.entityPatches) was deleted — see Document vs engine. Pruning it from the union alongside the field was judged more disruptive than leaving a dead type.
EditSceneSettingsIntent environment: Partial<SceneEnvironment> Declared, unimplemented as an intent. The environment itself is live — ambient, fog and the skybox all apply — but it is applied by the runtime directly from the scene document (patchEditSceneEnvironment), not by dispatching this intent. Sending it still does nothing.
EditSpawnPresetIntent presetId: StandardObjectPresetId, position: Vector3Tuple Declared, unimplemented. Note this is distinct from the everyday "spawn a standard preset" flow, which goes through SpawnIntent with a definition built by buildStandardObjectPresetDefinition() — this type was a separate, never-finished path.

If you are scanning the source and counting variants: all three live in packages/shared/src/sceneEditor.ts (not tableObjects/intents.ts, unlike most of the other 23; CardOverlaySetIntent lives in packages/shared/src/cardOverlays.ts) and are re-exported into the TableIntent union as SceneEditorEditEntityPatchIntent / SceneEditorEditSceneSettingsIntent / SceneEditorEditSpawnPresetIntent.

What every implemented intent has in common#

Every mutating path through applyIntent()/the dispatcher's handlers calls pushUndo() before mutating and ends by emitting a fresh TableSnapshot — so a new intent handler that skips either step is a bug, not a variant. Player- and spectator-originated intents pass through a validation context first (createValidationContext in IntentDispatcher); some intents (zone-*, snap-point-*, vector-line-*, decal-*, text-label-*, joint-*, ui-element-*, set-object-sound) have no additional per-kind validation beyond that context, while spawn, object-action, drag, transform and reparent each call a dedicated validate*Intent function first.

See also#

  • Object actions — the action values ObjectActionIntent carries.
  • Object state — the shapes these intents create, update or reference.
  • Host authority — why only the host applies an intent.
  • Mod scripting APIapi.createObject and api.objectAction, the two mod calls that become intents.