Skip to content

Importers

Every importer turns a foreign file into canonical ChatMapper JSON plus a report of everything that did not map one-to-one. The mappings are deliberately boring, reversible where possible, and documented here so a migration can be planned before it is run.

Formats are detected by content, not extension. Each importer scores the file and the highest confidence wins, which is how two .json formats (Arcweave, articy) and two Twine dialects (Twee 3, Twine 2 HTML) coexist.

interface ImportReport {
source: string; // importer id: "csv" | "twine" | "arcweave" | "articy" | "chatmapper-xml" | …
stats: { conversations, nodes, links, actors, variables, skipped, … };
warnings: { message: string; where?: string }[]; // non-fatal; the import still succeeds
lossy: {
kind: string; // e.g. "harlowe-macro", "arcscript-expression"
where: string; // passage title, element id, row number
original: string; // the source text, verbatim
preservedIn?: string; // custom field on the node that now holds it
count?: number;
}[];
}

The editor shows the report before writing anything. The principle behind lossy: nothing is dropped silently. If the importer could not translate it, the original text is in the report and in a custom field on the node where a human can finish the job.

Forgiving by design: writers hand over whatever their sheet looks like and a valid conversation comes out.

Columns — case-insensitive, order-free, aliases accepted:

ColumnAliasesMeaning
idnode idNode ID. Optional.
actorspeaker, characterSpeaker name; actors are created as met.
textdialogue, lineDialogue Text.
menuchoice, menu_textMenu Text. A row with menu text is a choice.
parentparent_id, fromThe row’s parent node.
gotolink_to, nextAn extra link out of this row.
conditionLua condition.
scriptLua script.
conversationConversation title (falls back to the sheet name).
titleNode title.

Any other column is kept verbatim as a custom field named by its header, and listed as lossy so you know it happened.

Structure inference when parent is absent: a plain row hangs off the row before it; a run of consecutive choice rows all hang off the last plain row, so the common line, line, choice, choice shape becomes a real branch. Anything more intricate needs parent / goto — that is why they exist.

Speakers: a blank actor continues the previous speaker for plain rows; a choice row with no actor gets an auto-created player actor. Variables referenced in conditions and scripts are declared automatically.

The CSV and Excel guide is the writer-facing version.

TwineChatMapper
StoryConversation. An HTML archive with several stories → several conversations.
PassageDialogue node, spoken by an auto-created Narrator actor.
[[Target]]Plain link to the target’s node.
[[Label->Target]], [[Target<-Label]], [[Label|Target]]An interstitial player node carrying Label as Menu Text, whose single child is the target.
Start passageLinked from a synthetic START (ID 0). The start passage is not itself the root, because roots are never spoken.
Passage positionCanvas position, rescaled from Twine’s 100×100 grid.
Tags, story metadataReported.
(set: $x to 1) / <<set $x = 1>> with one variable and one literalTranslated to Variable["x"] = 1.
Every other macro (if, link, goto, display…)Stripped from the display text, preserved verbatim in twine_markup_raw, reported once per macro name per passage.

Why link labels become nodes: in Twine the label belongs to the edge, and two passages can link to the same target under different labels. Writing the label into the target’s Menu Text would collapse those; one extra hop keeps the graph unambiguous and lossless.

Reads the web app’s Export → JSON (single-language and multi-language shapes; only the base locale is imported, the rest reported).

ArcweaveChatMapper
Leaf boardConversation. Folder boards are structure only.
ElementDialogue node in its board’s conversation. Position kept 1:1.
ConnectionLink. Cross-board connections become cross-conversation links.
Connection labelMenu Text on the target node. When a second inbound connection would need to overwrite a label, condition or script the target already has, a relay node is inserted instead (a player choice node for a label, a group node for a condition). Nothing is overwritten.
Entry elements (nothing links to them, or startingElement)Linked from a synthetic START. Not promoted to root.
Character component attached to an elementThe node’s Actor. Other attached components → arcweave_components. Attributes → arcweave_attr_<name>.
Branch → condition → outputFlattened to direct links; each condition’s arcscript becomes the target’s ConditionsString, with if / else-if / else order preserved as ConditionPriority (lower tried first).
JumperResolved to its target element.
arcscript, mechanical subset (comparisons and boolean combinations of declared variables; literal or simple-arithmetic assignments)Translated to Lua.
Any other arcscriptKept verbatim in arcweave_script_raw / arcweave_condition_raw, reported. Never half-translated.
Rich text beyond paragraphs and entitiesPlain text kept; raw HTML preserved when formatting would be lost.
Arcweave string IDsarcweave_id on every node, actor, variable and conversation.

Reads the single-document JSON export (Settings / Project / GlobalVariables / ObjectDefinitions / Packages / Hierarchy).

articyChatMapper
EntityActor (DisplayName, Text; template features as custom fields).
DialogueConversation.
FlowFragment containing dialogueConversation.
FlowFragment containing noneTransparent folder; links pass through it.
DialogueFragmentDialogue node: Text, MenuText, Speaker, StageDirections → Parenthetical.
HubGroup node.
Condition / InstructionGroup node carrying ConditionsString / UserScript.
JumpNot a node: incoming links are rewritten to the jump’s target.
Location / Zone / SpotLocation stub.
AssetItem stub (binaries skipped).
GlobalVariablesUserVariables, namespace flattened (Set.Var → Set_Var).
Pin connectionsInto an input pin = enter (→ that conversation’s root); into an output pin = leave, following the container’s own outgoing connections — which is what turns an articy exit into a cross-conversation link.
PositionsKept.
Scripts beyond the mechanical subsetarticy_script_raw, reported.
Object IDsarticy_id.

Two deliberate approximations, both reported: articy Conditions branch true/false on separate output pins, but ChatMapper gates the whole node, so both branches become plain links and the false branch is flagged; and articy has no player flag, so an entity that speaks at least one fragment with MenuText is inferred to be the player.

  • XML — the Desktop serialisation, parsed in pure TypeScript. Lossless for everything the model holds; see ChatMapper XML.
  • .cmp / .cmpkg — decoded server-side (the Desktop app’s binary format is AES-encrypted with a key compiled into the freely distributed app; it is not a security boundary). A .cmpkg’s media is attached to the project. The Python decoder has been verified across Desktop versions 1.2.0.0 and 1.5.1.0.
  • JSON — the canonical format; imported as-is after normalisation.

Search the project for _raw to find every node with untranslated logic, validate, and simulate the main path. The writer-facing checklist is on Importing existing work.