Dicey Table

IDs, Names and Tags

Every Scene Entity carries three names. They are not synonyms, they are not interchangeable, and exactly one of them is an address.

Field For Inspector field Rules
id Addressing. The only way anything resolves an entity. not shown as an editable field Assigned on creation, ≤96 characters, never changes
label Machines. Uniqueness, scripts, mods, and — for kind: "card" — the card's identity Slug [a-z0-9-], unique in the scene, ≤80 characters
displayName Humans. The Hierarchy row, status messages, the entity's engine name Name Free-form, ≤80 characters, optional

Neither name is an address. id is.

Nothing — no script, no mod, no runtime lookup — resolves an entity by either name.

id — the address#

id is assigned when the entity is created and never changes for the life of that entity. It is what parentId points at, what an action targets, what a script or a mod receives back from a query, and what the Hierarchy panel keys its rows on so selection and expansion survive a rebuild.

You never type an id. There is no Inspector field for it, and there should not be — editing an address is not an authoring operation.

Two properties make it the address and neither name a candidate:

  • It is stable. Renaming an entity does not move it. Restoring a version does not renumber it. A host migration does not reassign it.
  • It is unambiguous. Two entities can share a displayName quite legitimately. No two share an id.

Compare this with the engine's guid, which addresses a Live Entity and is not stable — see Document vs engine.

label — the slug, for machines#

label is the entity's slug. It is lowercase, dash-separated, unique within the scene, and it is what you type into the Inspector's Slug field.

chess-board      wood-table      red-die-1      ace-of-spades

The editor normalizes whatever you type: the input is lowercased, accented characters are folded to ASCII, every run of other characters collapses to a single dash, leading and trailing dashes are trimmed, and the result is truncated. If that collides with an existing slug, a numeric suffix is appended until it does not. So typing Chess Board!! into Slug produces chess-board, and typing it again produces chess-board-1.

label is what non-human consumers use:

  • Scene scripts and mods refer to entities by slug in the code you write.
  • Default names generated on import are slugs (an imported chess-set-1k-board becomes chess-board; wood_table_2k becomes wood-table).
  • Uniqueness is enforced on the slug, not on the human name.

⚠ For a card, the slug is the card#

This is the one place where renaming has a consequence beyond readability.

For an entity of kind: "card", label is the card's identity, and card identity is what drives hidden-information redaction — the mechanism that stops a peer from learning which card is face-down in someone else's hand by reading the network traffic.

Changing a card's Slug changes which card it is. Not what it is called: which card it is. A deck whose card slugs are rewritten is a different deck as far as identity-bearing state is concerned.

This is exactly why label could never be relaxed to accept spaces and capitals. Doing so would have made "rename this card so it reads nicely" and "change this card's identity" the same gesture. Instead, the human name was given a field of its own.

Treat a card's slug as data, not as a caption. If you want the card to read differently in the Hierarchy, change its Name.

What the schema actually enforces#

Worth knowing if you are hand-writing a setup file or generating entities:

  • The schema constrains label to 1–80 characters and requires it to be present. The [a-z0-9-] shape is produced and maintained by the editor's slug normalization, not by a schema pattern.
  • Nothing DiceyTable generates will produce a label outside that shape, and every tool that consumes a slug assumes it. Author slugs as slugs.

displayName — for humans#

displayName is free-form: spaces, capitals, punctuation, up to 80 characters. Chess Board, Player 1's Hand, Big Red Die. It is optional.

It is what you see in:

  • the Hierarchy row,
  • the Inspector's Name field,
  • status messages and the context menu,
  • the Live Entity's pc.Entity.name.

When it is absent or blank, the slug is shown instead. One resolver — objectDisplayName() — decides this everywhere, so the Hierarchy row, the entity name, the status line and the context menu can never disagree about what something is called.

Clearing the Name field does not leave the entity nameless; it falls back to the slug.

The Name is replicated — and, for a card, redacted with the card#

Everything a player can see, they can also read: displayName rides in every snapshot, and whatever you type in Name reaches every peer entitled to that entity.

For kind: "card" the host redacts it exactly as it redacts the card's identity. When a viewer may not see a card's face — it is face down and unowned, or it is in someone else's hand, and it is not revealed to that viewer's team — the wire copy that peer receives has label rewritten to Card, metadata.cardId removed, metadata.__redacted set, and displayName deleted. Every hidden card therefore looks alike, with no leftover signal that this one had a special name. Flip the card up, deal it into that player's hand, or reveal it to their team and the name comes back along with the face, because it is the same entitlement.

So naming a card Ace of Spades is safe, and it is what the field is for. What the Name field is not is a hiding place:

  • it is public whenever the card is public — you cannot use it for something that must stay secret after a reveal;
  • no other kind's displayName is redacted at all. A token named Traitor Marker announces itself to everyone, always.

The sanctioned place for "what is this really" is secretMetadata: a per-entity property bag, capped at 2 KiB of JSON, that the host strips from every viewer not entitled to the entity's identity — for a deck and for every non-card kind, that means every viewer except the host. See Object state.

This is a fix, not a long-standing rule. Until 2026-07-28 redaction neutralized label and metadata.cardId and never looked at displayName, so a face-down card named Ace of Spades was broadcast intact to every peer. If you authored around that by keeping card names deliberately vague, you no longer need to.

The rule: editing Name leaves Slug alone#

The Inspector shows Name and Slug as two fields, one above the other, and they are independent:

Editing the Name leaves the Slug alone once the Slug has been customized.

Setting a human name never touches the slug. That is the guardrail that makes the card rule above safe: renaming an entity for readability can never silently change a card's identity, because renaming does not write to label at all.

The converse is also true and worth stating: editing Slug does not update Name. If you have set a human name, that is the name; the slug moves underneath it.

You edit displayName label
Name changes untouched
Slug untouched changes (normalized, de-duplicated)

Tags#

Tags are how you group entities without parenting them. A tag is a short lowercase token, an entity can carry many, and membership is by tag rather than by containment — which is what makes a Tag Group different from a Folder or a parent.

Tags are useful because they cost nothing structurally: an entity can be in a parent assembly and in five tag groups at once, and tag-based lookup is the mechanism both the editor's filter and the scripting surfaces are built on.

Author tags#

Rule Value
Character class [a-z0-9_-] — letters, digits, underscore, dash
Length 1–32 characters
Maximum per entity 100
Case Stored lowercase; input is lowercased and trimmed
Duplicates Collapsed, order preserved

The tag character class includes underscore while the slug character class does not. my_tag is a valid tag; my_slug is not a valid slug.

Tags are edited from the TAGS chip input in the ENTITY header, and drive the Hierarchy's tag filter pills, Select all with tag, and tag-scoped group editing.

The reserved dt: namespace#

Tags beginning dt: are platform-owned and unauthorable. Not "discouraged" — unrepresentable.

The author-tag pattern is:

/^[a-z0-9_-]+$/i

: is deliberately not in that character class. A platform tag therefore cannot be expressed as an author tag, which means an author or a mod can never forge one. A schema parse of ["dt:internal"] fails outright; it is not accepted and quietly ignored, it is rejected.

That guarantee is what the platform tags are worth having:

Tag Applied to
dt:internal Every entity DiceyTable creates that is not an authored Scene Entity — debug overlays, gizmo proxies, ghosts, previews, snap markers, joint holders, per-entity Face/Back children, imported-model sub-roots
dt:object Every table-entity root
dt:kind:<kind> dt:kind:card, dt:kind:die, … — one per Platform kind
dt:seat-group Seat-zone and seat-label entities, alongside the user-visible seat-group tag
dt:child Any entity parented to another table entity

If an author could forge dt:internal, they could hide an entity from the editor. If they could forge dt:object, they could make something claim to be a table entity that is not. The regex is the whole separation. It is not widened.

In the editor, platform tags are shown dimmed and cannot be removed.

dt: tags appear in read results#

This is the part that catches people out.

Platform tags are unauthorable, but they are not invisible. They are real tags on the Live Entity, and they come back when you enumerate that entity's tags in the editor or the runtime. Any code there — a filter, a count, a "does this entity have any tags?" check — must account for them, or it will see tags it did not put there.

Scripts are the exception, and the distinction matters. dt: tags live on the Live Entity, not in the scene document. A table script's ObjectData.tags is the authored tag set from the object's definition, and tableObjectStateSchema validates tags against /^[a-z0-9_-]+$/, which forbids :. So ObjectData.tags can never contain a dt: tag and a script-side tag filter can never match one. Account for platform tags when you are reading the entity graph; do not defend against them in a script.

Two specific cases:

  • dt:child is added and removed automatically by parenting. Parent an entity and it gains dt:child; unparent it and it loses it. Nobody typed it and nobody can.
  • A tag filter with an unsatisfiable needle matches nothing, not everything. Asking for { tags: ["dt:internal"] } does not return the whole table just because the needle is invalid — that would be the opposite of what was asked. An empty needle list, on the other hand, means "no filter" and matches everything.

Tags are for grouping, names are not#

The engine's own guidance, which DiceyTable follows: tag-based searches are faster and more flexible than deep name-based searches, and nothing should identify an entity by a name prefix.

DiceyTable used to detect internal entities by a __DiceyTable name prefix. That was replaced by dt:internal precisely so names could stay readable while detection moved off them. The rule that came out of it:

An Entity's name is for humans; its id is for machines. Nothing addresses an entity by name — scripts and mods address by id or by tag.

Summary#

  • id addresses. It is stable, unique, never typed, and the only thing that resolves an entity.
  • label is the slug: the uniqueness key, the machine-facing handle, and for a card its identity, which drives hidden-information redaction. Edit it in Slug, and for cards edit it with care.
  • displayName is the human name. Optional, free-form, shown everywhere a person reads it, falls back to the slug. It is replicated, and on a card it is redacted together with the card's identity — safe to write, not a hiding place. Real secrets go in secretMetadata.
  • Editing Name never touches Slug.
  • Tags group entities by membership rather than containment. dt:-prefixed tags are platform-owned, unforgeable — and still present in anything that reads tags.

See also#

  • Object state — every replicated field, and secretMetadata's per-kind redaction rule.
  • Mod scripting APITableObjectState.label is the slug a mod reads; displayName is optional and often absent.
  • ObjectHandleObjectData.name carries the slug, not the display name. It is the single thing scripts get wrong about this page.
  • TypesSpawnObjectOptions.name sets the slug too, not the human name.
  • worldworld.getAllObjects({ tag }), filtering on the tags defined here.