A C# runtime for Parlance, a git-native narrative design tool for story-driven games.
Parlance is the authoring tool; this is the Unity runtime for what it produces. You write your story in Parlance's visual editor — dialogue and quest canvases, a searchable reference index, live playtest — and it saves as human-readable JSON directly in your repo. No database, no import/export step, git as the single source of truth.
This package reads that JSON and runs it in Unity: dialogues (conditional lines, fallback and locked choices, line and choice tags, engine commands), conditions, effects, skill checks (with conditional modifiers), character dialogue offers, quests, endings. It is verified against Parlance's published conformance vectors rather than against its author's confidence.
| parlance-unity | Parlance spec | Families |
|---|---|---|
main (unreleased) |
v0.15.0 — pinned to a3c6454 |
11 of 11 — 207 / 207 vectors |
conformance/PIN is the authoritative record of which upstream ref the vectors came from.
Install this package via the Unity Package Manager (UPM).
- In Unity, open Window > Package Manager.
- Click the + icon in the top left and select Add package from git URL...
- Enter the URL of this repository (e.g.,
https://github.com/Orbitope/parlance-unity.git).
Since the runtime is composed of pure C# static classes, there are no MonoBehaviour singletons to manage or add to your scene.
Load your JSON using Unity's TextAsset or System.IO.File, parse it into a Dictionary<string, object> (using your preferred JSON library, such as System.Text.Json or Newtonsoft.Json), and pass it to the runtime.
State is immutable: every entry point returns a new state and never mutates its input.
using Parlance;
// 1. Initialize your project JSON and Game State. The project is the whole
// data set (rules, factions, quests, …); a dialogue is one entry in it.
var projectDict = ParseMyJson(myParlanceJsonTextAsset.text);
var dialogues = (Dictionary<string, object>)projectDict["dialogues"];
var dialogue = (Dictionary<string, object>)dialogues["d_intro"];
var state = State.FromDict(mySavedGameStateJson);
// 2. Present a node: resolves conditional nodes, partitions the choices,
// interpolates {placeholders}. Every entry point returns { "error": … }
// instead of throwing when the call is invalid.
var step = Runtime.StepDialogue(dialogue, "node_start", state, projectDict);
var node = (Dictionary<string, object>)step["node"];
string nodeId = (string)node["id"]; // the RESOLVED node: may differ from the id you asked for
if (!(bool)step["textHidden"] && node.TryGetValue("text", out var line))
{
Debug.Log(line); // no line when hidden, or on a text-less choice node
}
foreach (Dictionary<string, object> choice in (List<object>)step["visibleChoices"])
{
Debug.Log(choice["text"]); // selectable
}
foreach (Dictionary<string, object> choice in (List<object>)step["lockedChoices"])
{
// Opt-in: failing choices authored whenLocked: "show". Render greyed out,
// with lockedText in place of text when set. Never selectable.
Debug.Log(choice.TryGetValue("lockedText", out var lt) ? lt : choice["text"]);
}
// 3. onEnter effects are RETURNED, not applied. You decide when they fire —
// on first arrival, not on replay. Walk them in order and dispatch any
// `engine` effect yourself: ApplyEffect leaves state unchanged for it.
var onEnter = (IEnumerable<object>)step["onEnterEffects"];
foreach (Dictionary<string, object> effect in onEnter)
{
if ((string)effect["type"] == "engine") MyEngine.Dispatch((string)effect["command"], effect.GetValueOrDefault("args"));
}
state = Runtime.ApplyEffects(onEnter, state, projectDict);
// 4. Take a choice. Pass a seeded RNG so checks are reproducible.
var outcome = Runtime.ChooseChoice(
dialogue,
nodeId,
"ch_ask",
state,
projectDict,
Rng.ForStep(mySeed, stepIndex)
);
if (outcome.ContainsKey("error")) throw new InvalidOperationException((string)outcome["error"]);
state = (State)outcome["newState"];
if (outcome.ContainsKey("checkResult"))
{
Debug.Log(outcome["checkResult"]); // passed, roll, total, skillValue, dice (+ bonus, appliedModifiers)
}
var nextNodeId = outcome.GetValueOrDefault("nextNodeId"); // null on a terminal choice; resolve it with the next StepDialogue- A gated node with choices or
isEndis not skipped — only its line is hidden.StepDialoguereturns it withtextHidden: trueandnode.text == ""; its choices are offered, anisEndnode still ends the dialogue, and itsonEnterstill fires. Only an interstitial gated node (no choices, notisEnd) is skipped, throughnext.StepDialogueandAdvanceNodeshare oneResolveNodewalk. - Resolve once per arrival, on the arrival state. If you apply
onEnterbefore presenting, resolve withResolveNodeand present withStepResolvedNode(node, postEffectState, project, arrivalState)sotextHiddenis judged on arrival while choices filter on the post-effect state. Never re-resolve a node the player has already seen. ChooseChoiceonly accepts a choice that was offered. A hidden choice, a locked choice, or afallbackwhile a non-fallback choice is visible returns anerrorcontainingnot selectable. A fallback is offered only when no other choice is visible.- Tags pass through untouched.
node["tags"]and each visible choice'stagscome back exactly as authored; hang animation, audio or camera cues off them. onEntereffects are not applied for you.StepDialoguereturns them; firing them is the caller's job, on first arrival only. Applying them on every render double-counts on a rewind.- A node with no choices is not necessarily over. If it has
next, callAdvanceNode— that's a listen-only beat. Treating it as the end silently truncates ambient chains. - Passive checks never roll. Show or hide a passive-check choice with
PassiveCheckPasses(skill + Σbonus >= difficulty), so a check modifier means the same thing in both modes. - Run
ResolveQuestsafter every state change. It fires quest stageonCompleteand outcomeeffectswhose condition holds, once each, and records them inQuestFired. It never advances a stage for you. - Levelling is derived, not stored.
Xpis total-earned;LevelForXp,PointsEarnedandAvailablePointsderive from it.InvestSkillPointis a guarded player action, not an effect; callRecomputeSkillson load so checks read preset + invested skill. - When a scene ends, ask
NextContinuations. A pending cutscene comes first, then any character routed withset_active_dialogue, otherwise each character's best offer. Consume them withClearPendingCutscene/ClearActiveDialogue. - Which dialogue a character opens is
ResolveCharacterDialogue. It ranks the dialogues whoseoffernames that character (priority tier, then condition specificity, then id); pass your visited set so non-replayable dialogues are not offered twice.
You can verify the runtime against the exact same conformance vectors used by the GDScript implementation. Since it's a pure C# library, you can run the NUnit tests from the command line without launching Unity:
cd Parlance.Tests
dotnet testMistfall Inn is a small, complete murder mystery built on the Parlance engine. It's a useful reference for seeing the runtime driving real authored content rather than test fixtures.
MIT, including the vendored spec files.