Runtime semantics
This page is the contract a runtime must honour to play a ChatMapper project the way the editor’s simulator, the Desktop app and the standalone HTML player do. Those three are tested against each other; if your runtime follows this page, writers’ expectations transfer.
Vocabulary
Section titled “Vocabulary”- Current node — the node just played (or the root, at the start).
- Candidates — the nodes reachable from it, before filtering.
- Offered — what survives filtering: the set the player is about to see, or the single node that plays next.
- Player choice — a node with non-empty
Menu Text. - Group — a node with
IsGroup: true. Never played; structural.
What is offered from a node
Section titled “What is offered from a node”Given the current node, compute the offered set in four steps:
- Collect every
OutgoingLinksdestination, in link order. A destination may be in another conversation. - Filter by condition. Evaluate each candidate’s
ConditionsStringagainst the current state. A candidate whose condition is false is dropped — unless itsFalseConditionActionis Passthrough (1), in which case its own children join the candidate pool in its place (and are themselves filtered). An empty condition is true. A condition that cannot be evaluated is treated as true and logged. - Suppress by priority. Among the surviving candidates in the same
group, keep only those at the best (numerically lowest)
ConditionPrioritypresent. Normal is3;1is highest. - Expand groups. Replace each surviving group node with its own
children, run through steps 2–3 again. The group’s
UserScriptis carried down and runs before the chosen child’s script.
The result is the offered set.
What is a “group” for priority purposes?
Section titled “What is a “group” for priority purposes?”Priority suppression is scoped so that only siblings compete. Children of the root or of a group node form that node’s group; every other node inherits its parents’ groups. Grouping only follows links inside one conversation; a node reached only from another conversation gets a private group and competes with nobody.
Then: play, menu, or end
Section titled “Then: play, menu, or end”With the offered set in hand:
| Offered set | Runtime does |
|---|---|
| Empty | End of conversation. |
Contains a node with no Menu Text (a line, not a choice) | Play it immediately. It is a continuation, not a decision — enter it even if selectable siblings also survived. |
Exactly one player choice, and its Menu Text does not contain [f] | Play it straight through, no menu. |
| Otherwise | Present a menu of the offered player choices, in link order, labelled by Menu Text with [f] / [a] stripped and [var=…] substituted. |
When the player picks, enter that node and repeat from the top. When a line plays automatically, enter it and repeat. Guard against runaway chains: the reference implementations stop after a large fixed number of automatic advances and report a possible loop.
Entering a node
Section titled “Entering a node”In this order:
- Mark the node
WasDisplayed— unless it is the root (never marked), orDelaySimStatusis true and the node has links, in which case defer until the next offered set has been computed. - Run the accumulated group script(s), then the node’s own
UserScript. - If the
Dialogue Textcontains an input prompt ([?Name]), pause: withhold the line, ask the player, store the answer inName, then emit the line with the token removed and continue. - Emit the line: split
Dialogue Texton|into successive segments, strip picture tokens, substitute[var=…], drop empty segments. The root emits nothing even if it has text.
SimStatus
Section titled “SimStatus”Every node has a status for the current playthrough:
| Status | Set when |
|---|---|
Untouched | Initial. |
WasOffered | The node appeared in a menu. Never downgrades a WasDisplayed. |
WasDisplayed | The node was entered. |
Offering marks each menu entry WasOffered — but only when a menu is
actually shown; an automatic continuation does not mark its siblings. The
root is never marked.
Status is readable and writable from Lua as
Conversation[c].Dialog[d].SimStatus (see Lua). It
persists across conversations within one playthrough.
Playthrough state
Section titled “Playthrough state”A playthrough is the whole project, not one conversation:
- At start (once): create the asset tables, seed every
UserVariablesentry from itsInitial Value(coerced — see the data model), run the projectUserScript. - Entering another conversation (via a cross-conversation link, or a trigger from the host): start from that conversation’s root keeping variables and statuses. Do not re-seed, do not re-run the project script.
- Can a conversation start? Evaluate its root’s
ConditionsStringagainst the current state. False means the host should not start it.
Worked example
Section titled “Worked example”From the example scenario:
START(0) ─▶ Opening line(1) ─▶ [group] Traveller's options(2) ├─▶ Accept the token(3) Menu: "I'll take the crossing." script: has_token = true │ └─▶ Ending - crossing(5) cond: has_token == true └─▶ Stay ashore(4) Menu: "I'll wait for morning." └─▶ Ending - ashore(6)- Enter root. Offered from root:
{1}— no menu text → play 1. Two segments emitted. - Offered from 1:
{2}is a group → expand →{3, 4}, both choices, both Normal priority → menu of two. 3 and 4 becomeWasOffered. - Player picks 3. Enter 3:
WasDisplayed; script setshas_token. Emit its dialogue. - Offered from 3:
{5}; condition true; no menu text → play 5. - Offered from 5: empty → end.
Had the player picked 4, node 5’s condition is never evaluated; had 3’s script not run, 5’s condition would be false and — with nothing else offered — the conversation would end after 3.
Conformance
Section titled “Conformance”The ChatMapper repository’s test suite runs a battery of scenarios — groups,
priority suppression, passthrough, [f], DelaySimStatus, | splits,
[var=], [?Var] prompts, conditions and scripts — through both the
editor’s simulator and the HTML player’s runtime and asserts identical
transcripts and final variable state. Use the
example scenario and the
HTML player build of it as your
oracle: if your runtime disagrees with the player, the player is right.