Skip to content

Start here — new to ChatMapper

This section assumes no prior ChatMapper knowledge. It is written for the person who will either wire ChatMapper output into an engine or hand the editor to a writing team and then consume what comes out.

If your studio already has Desktop ChatMapper projects, read Migrating from ChatMapper Desktop instead.

A project is a single JSON document with five asset collections:

{
"Language": "en-US",
"Title": "The Ferry at Dusk",
"Version": "1.5.1.0",
"Author": "",
"Description": "",
"UserScript": "",
"Assets": {
"Actors": [],
"Items": [],
"Locations": [],
"Conversations": [],
"UserVariables": []
}
}

UserScript is Lua run once at the start of a playthrough, after the asset tables are built.

Actors, Items, Locations and UserVariables share one shape:

{ "ID": 1, "Fields": { "Name": "Ferryman" } }
  • ID is unique within its own collection.
  • Fields maps a field title to a value, and every value is a string"True", "30", "[]" — regardless of the field’s declared type. Coerce at the boundary of your code, once.

Conventional field titles:

CollectionFields
ActorsName, IsPlayer ("True"/"False"), Description, Pictures, Age, Gender
ItemsName, Description, Pictures, Purpose, Scene
LocationsName, Description, Pictures
UserVariablesName, Initial Value, Description

A conversation is a tree (strictly, a directed graph — cycles are legal and common) of dialogue nodes:

{
"ID": 1,
"NodeColor": "White",
"Fields": { "Title": "Asking for passage", "Actor": "1", "Conversant": "2" },
"DialogNodes": []
}

Actor and Conversant hold a stringified actor ID ("2", not "Ferryman"). "" and "-1" both mean unset. Exactly one node must have IsRoot: true; the root is a start marker and is never spoken, even if it carries text.

A node carries ID, ConversationID, IsRoot, IsGroup, ConditionsString, UserScript, NodeColor, DelaySimStatus, FalseConditionAction, ConditionPriority, OutgoingLinks, and a Fields object holding Title, Actor, Conversant, Menu Text, Dialogue Text, Parenthetical, Audio Files, Video File, Animation Files, Pictures — plus anything else the project adds.

The distinction that matters: Dialogue Text is what is spoken, Menu Text is what appears on a choice button.

Links live on the origin node and name their destination by conversation and node ID. A Filename field on a link points at another project file — most runtimes, including Unity’s, cannot follow those, because they load one combined database.

This is the part you must get right. Given a node, what is offered next:

  1. Collect every link destination.
  2. Filter by condition. A destination whose ConditionsString evaluates false is dropped — unless its FalseConditionAction is Passthrough, in which case its own children join the candidate pool in its place.
  3. Suppress by priority. Only candidates at the best (numerically lowest) ConditionPriority present survive.
  4. Expand groups. A surviving node with IsGroup: true is replaced by its own children, run through the same three steps. The group’s UserScript runs before the chosen child’s.

Then:

  • No survivors → end of conversation.
  • Survivors without Menu Text → play immediately (a line, not a choice).
  • Exactly one survivor with Menu Text, not prefixed [f] → play it straight through, no menu.
  • Otherwise → present a menu.

ConditionsString and UserScript are Lua. A conforming host exposes:

BindingMeaning
Variable["Name"]User variables.
Actor["Name"], Item["Name"], Location["Name"]Asset field tables.
Conversation[id], Conversation[id].Dialog[id]With a SimStatus of "Untouched" / "WasOffered" / "WasDisplayed".
Dialog[id]Shorthand for the evaluating node’s own conversation.

Names are cleaned before use as Lua keys: " [ ] . \ / are removed, and -, spaces and newlines become _. So the field Menu Text is Menu_Text, and an actor named Harbour Master is Actor["Harbour_Master"].

Two assets whose names clean to the same key collide, and some hosts — Unity’s Dialogue System among them — let the second silently overwrite the first. The editor’s Unity preflight reports this as an error.

A condition that cannot be evaluated should be treated as true and logged, not as false. Failing open keeps content reachable.

Markup is part of the stored text. A consumer that cannot honour a tag should strip it for display rather than rewriting the value.

MarkupMeaning
|Sentence split — one node, successive lines.
[f]On Menu Text: force a menu even for a single option.
[a]On Menu Text: an action line rather than a spoken one.
[var=Name]Substituted with the variable’s value at display time.
[?Name], [x=?Name]Prompt for player input, store in Name, consume the token from the line.
[pic=N], [pica=N], [picc=N]Swap the displayed image mid-line.
[em1]…[/em1][em4]Author emphasis spans; colour/style/label configured per project.

5. Extending the format without forking it

Section titled “5. Extending the format without forking it”

Three rules, and they are load-bearing for the whole ecosystem:

  1. Custom fields. Add whatever you need to any Fields object. Consumers that do not recognise a field must preserve it, not drop it. This is how the LearnBrite runtime fields, Unity’s Sequence field and every importer’s *_raw provenance fields work.
  2. _editor. One top-level object for editor-only metadata — canvas positions, reviewer notes, field type hints, collapsed state. It is stripped from every export and any consumer may ignore it. Never put anything there that the project’s meaning depends on.
  3. Graceful degradation. A consumer that does not understand an extension must still produce a working project without it.

What this rules out: renaming canonical keys, changing a field value away from string, or adding a meaningful top-level sibling to Assets.

For your own engine, the pattern is: define your fields (Sequence, cutscene_id, vo_take, whatever), add them as a custom field template in the editor so writers get them on every node, and read them in your importer. No schema change, no fork, and the project still opens in every other ChatMapper tool.

Sign in at app.chatmapper.com. Your first sign-in creates a workspace and offers three starts: the working example, a blank project, or an import.

The parts that matter to a developer:

  • Canvas. Drag-and-connect, right-click menus, minimap, collapse/expand subtrees, auto-layout (left-to-right or top-to-bottom), and a structured layout mode that keeps the graph tidy without manual placement.
  • Keyboard tree building. Tab for a child, Enter for a sibling, Home for the root, arrows to move, Ctrl+/ to reorder siblings (menu order), double-click to edit text in place. Project-wide find and replace on Ctrl+F / Ctrl+H.
  • Inspector. All node fields including conditions and user scripts, custom fields with per-field types and an export this field flag, node colour, group flag, DelaySimStatus, FalseConditionAction, ConditionPriority.
  • Simulator. Plays a conversation with real Lua (a WebAssembly Lua runtime, with a subset interpreter as fallback). Step history — start, back, forward, end — plus a Lua console to evaluate expressions and assign variables against the live run state. This is a step debugger for narrative logic, and it is the single biggest reason writers stop filing “the branch doesn’t fire” bugs.
  • Validator. Duplicate IDs, unreachable nodes, orphaned and broken links, undefined variables, cross-conversation link integrity, spoken lines with no actor, plus per-target preflights.
  • History and collaboration. Autosave, revision snapshots with diff and restore, live presence, optimistic-concurrency conflict resolution, reviewer notes with a review status per node, and Owner/Admin/Editor/Viewer roles.
  • Localisation. Add languages and each translatable field gains a per-language version; a toolbar switch changes what canvas, inspector and simulator display.

Export ▾ groups the formats by what you would do with them.

Dialogue System imports ChatMapper XML natively (Tools → Pixel Crushers → Dialogue System → Import → Chat Mapper). There is no separate file format: the Unity — Dialogue System XML export emits exactly the same bytes as the plain XML export, and adds a preflight for the things that import cleanly but then misbehave:

  • Error: two assets whose names clean to the same Lua key (Guard and guard) — Dialogue System keys its Lua tables by cleaned name, and one silently overwrites the other.
  • Warning: an asset with no Name.
  • Warning: an Item with no Is Item field. ChatMapper keeps items and quests in one collection; Is Item is how Dialogue System tells them apart.
  • Warning: a | inside a bracketed tag — Dialogue System splits lines on | and will tear the tag in half.
  • Warning: a link with a Filename (cross-project link) — unsupported there.
  • Warning: a condition that does not parse as the supported Lua subset, so you know to verify it in Unity.
  • Summary warning: how many dialogue nodes have no Sequence field.

Apply the built-in “Unity — Dialogue System” custom field template to add Sequence and Response Menu Sequence to dialogue nodes and Is Item to items, so your writers fill them in as they go.

Exports a .yarn file for the project: dialogue chains become lines, child nodes with menu text become -> options, links become jumps, variables are declared, and the translatable subset of the Lua becomes <<if>> / <<set>>. Lua that cannot be translated is emitted as a comment and reported in the export warnings rather than silently dropped.

Export ChatMapper JSON and read it. The format is specified above; the repo also ships docs/format/chatmapper-json.md (normative), a complete fictional example-scenario.json covered by a test so it cannot rot, and reference-parser.ts — a single dependency-free, MIT-licensed TypeScript file that parses and walks the format and compiles standalone under tsc --strict. Start there rather than from scratch.

One self-contained .html file — embedded scenario, embedded player, inline CSS, no external requests, runs from file://. It records the path taken and produces an end-of-run summary (nodes visited, endings reached, final variable state). Useful for playtest builds, stakeholder review and QA links long before any engine work exists.

A zip of the HTML player plus imsmanifest.xml as a single SCO. On completion it reports cmi.core.lesson_status and cmi.core.score.raw. Scoring uses a numeric variable — one you nominate, else a project variable literally named score — normalised to its declared min/max; failing that it falls back to “reached an ending” or proportion of nodes visited, and tells you which rule it used. The xAPI variant adds an emitter: initialized, one responded per choice, completed with score.

Voiceover Script CSV (one row per spoken line, markup stripped — the recording sheet), Proofreading Text, Screenplay RTF, Assets CSV, Dialogue CSV, an everything CSV, Reviewer Notes CSV, and a Conversation SVG for design docs.

CMPKG + media packages the project XML and JSON together with every attached media file. Plain ChatMapper XML opens directly in the Windows app.

Import accepts:

SourceNotes
.cmp, .cmpkgDesktop ChatMapper’s native (encrypted) files, decrypted server-side.
.xmlDesktop XML.
.jsonCanonical ChatMapper JSON.
CSV / TSV / .xlsxForgiving spreadsheet import: id, actor, text, menu, parent, goto, condition, script, conversation (aliases accepted, order-free). Missing id/parent infers linear chains from row order.
TwineTwee 3 source or a Twine 2 HTML story/archive; passages become nodes, [[links]] become links.
ArcweaveJSON project export; boards become conversations, elements become nodes, connections become links.
articy:draft XJSON export; entities become actors, dialogues become conversations, fragments become nodes, hubs become group nodes, jumps become links, global variables become variables.

Every importer produces a report before anything is applied: counts, warnings, and a lossy list of constructs that could not be mapped faithfully. Nothing is dropped silently — untranslatable markup and scripts are preserved verbatim in provenance custom fields (twine_markup_raw, arcweave_script_raw, articy_script_raw, articy_id, …) so a human can finish the job.

  1. Import or author a small conversation.
  2. Validate, then Simulate it — confirm the branching logic is what you think it is.
  3. Add your engine’s fields as a custom field template.
  4. Export to your target and import it in the engine.
  5. Write the pipeline step. For Unity that is the Dialogue System importer; for a custom runtime, start from the reference parser and implement §2’s traversal rules against your own presentation layer.
  6. Hand the editor to the writers. Give reviewers the Viewer role and the standalone HTML player.