Writ Sub-Dialogue Tree (Individual Quest Dialogues)
This document describes the structure and parameters of the individual writ sub-dialogues — one .dlg.json per quest, such as q_pyrami.dlg.json. These are the conversations that cv_venturecaptai.dlg.json launches when a player selects a writ from the venture captain's dynamic list. This guide covers only what happens after the player picks a specific writ; the hub/list mechanics live in cv_venturecaptai.dlg.json and its handlers (cv_venturecapsc / cv_venturecat).
How the hub hands off to a sub-dialogue
When the player chooses a writ name in cv_venturecaptai.dlg.json, the cv_venturecat action resolves the display name to a quest resref and calls:
player.ActionStartConversation(npcSpeaker, questResref, true, false);
The fourth argument (false) means the conversation starts at entry level 0, i.e. a fresh start. The engine then loads <questResref>.dlg.json and begins at its first starting node. Everything below is contained within that single file; there is no further chaining back into the hub except via the q_return exit replies.
Canonical shape
Every writ sub-dialogue follows the same template: 15 entries, 27 replies, three starting nodes [10, 5, 0]. The three starting nodes correspond to the three NPC variants who can offer the writ:
| Starting node | Speaker tag | Faction |
|---|---|---|
| E0 | ColsonMaldris |
libertysedge |
| E5 | AaqiralHakam |
theexchange |
| E10 | GlorianaMorilla |
sovereigncourt |
Each variant owns a contiguous block of five entries (E0–E4 male, E5–E9 female-A, E10–E14 female-B), so the same logical flow exists three times with gendered flavor text. The reply pool is shared across all three blocks.
Entry roles (per variant block)
Within each block of five entries, the roles are fixed:
| Offset | Role | Script on its replies |
|---|---|---|
| +0 | Hub / greeting — "what can I help you with" | none (plain routing) |
| +1 | Lore beat (reaction to the writ page) | q_return |
| +2 | Problem description | q_finishquestat (turn-in) + q_return |
| +3 | "Not ready" dead-end guard | q_return |
| +4 | Job briefing → accept | q_startquestat (accept) + q_return |
So for the male variant: E0 = hub, E1 = lore, E2 = problem/turn-in, E3 = not-ready, E4 = accept. Female-A is E5–E9, female-B is E10–E14.
The hub entry (+0) and its three gated replies
The hub entry offers exactly three options, gated by the quest's own state using the legacy parameter set:
| Reply text | Condition param | Meaning |
|---|---|---|
| "Ask how you can help." | HIDEINPROGRESS=<self> |
Shown before acceptance (hidden once the quest is active). Routes to the accept/not-ready split. |
| "You ask what the problem is about." | QUESTINPROGRESS=<self> |
Shown while the quest is active. Leads to the problem/turn-in node. |
| "Tell them it's already been dealt with." | QUESTCOMPLETE=<self> |
Shown after completion. Leads to the turn-in/lore path. |
<self> is the sub-dialogue's own quest resref (e.g. q_pyrami). These three gates partition the player's lifecycle: pre-acceptance, in-progress, and completed.
The "ask how you can help" split (the critical routing)
The "Ask how you can help." reply routes to two targets, one of which must be chosen based on whether the prerequisite writ has been finished. This is the modern database-backed contract:
| Target | Condition params | Meaning |
|---|---|---|
| Accept node (+4) | QUESTRESREF=<self> + QUESTSTATE=ABSENT |
Quest not yet started. Gated by prereq: only fires when the prerequisite is finished. Shows the job briefing with "Accept the job." |
| Not-ready node (+3) | QUESTRESREF=<self> + QUESTSTATE=FALLBACK |
Catch-all. Always fires. Shows "you're not ready yet." Reached when the ABSENT branch is blocked by an incomplete prereq. |
Both branches use QUESTRESREF=<self> — the sub-dialogue's own resref. Do not put the parent resref here. At runtime EvaluateDatabaseBackedQuestCondition loads the <self> row from gc_quests.quests, reads its requiredquest column, and blocks positive states (ABSENT, INPROGRESS, READY, FINISHED) until that prerequisite is finished. Negative/catch-all states (NOTFINISHED, FALLBACK) are exempt from the prereq gate so they remain reachable as fallback messages (see QuestFunctions.cs). The parent therefore lives only in the database; the dialogue never names it. The current prerequisites, as stored in gc_quests.quests:
Sub-dialogue (<self>) |
Prerequisite (requiredquest in DB) |
|---|---|
q_pyrami |
q_uppersiphons |
q_floodedcrypt |
q_puddlesdistric |
q_razorhall |
q_uppersiphons |
q_ransackedretre |
q_foreignquarter |
q_lodgelamented |
q_foreignquarter |
The branches are ordered: ABSENT is evaluated first. When the prereq is finished, ABSENT matches (quest not started) and routes to accept. When the prereq is not finished, ABSENT is blocked by the gate and evaluation falls through to FALLBACK, which always returns true and routes to the not-ready message. This guarantees "ask how you can help" always resolves to exactly one valid target and can never dead-end.
Historical bug: these splits originally used the legacy
QUESTCOMPLETE/NOTCOMPLETEparams, and in some files pointed at the wrong quest entirely (e.g.q_sewersoggy). A later revision usedFINISHED/NOTFINISHED, butFINISHEDevaluated the quest's own completion status rather than the prerequisite's, causing both branches to fail when the quest hadn't been started. The currentABSENT/FALLBACKpattern correctly separates the prereq gate (handled by the database lookup) from the state check (quest-not-started vs. catch-all).
The "not ready" node (+3)
The not-ready node explains the prerequisite ("Better you aid the Upper Siphons first…") and carries a single q_return reply ("Ask about something else.") so the player can exit back to the hub instead of being stuck. Without this exit reply the node is a dead end.
Accept node (+4)
Offers "Accept the job." running q_startquestat with STARTQUEST=<self>, plus a q_return escape. Accepting starts the quest for the player.
Turn-in node (+2)
Reached via the in-progress or completed hub replies. Offers "Tell them the job is done." running q_finishquestat with QUESTRESREF=<self>, gated by COMPLETEPROGRESS=<self> (shown only at 100% progress), plus a q_return escape.
Parameters used by writ sub-dialogues
All parameters are consumed by the handlers catalogued in DIALOGUE_PARAMETERS.md. The ones that appear in these sub-dialogues:
Conditions (on reply references, ConditionParams)
| Param | Value | Where | Purpose |
|---|---|---|---|
HIDEINPROGRESS |
<self> |
hub entry | Hide "ask how you can help" once the quest is active. |
QUESTINPROGRESS |
<self> |
hub entry | Show "ask what the problem is" only while active. |
QUESTCOMPLETE |
<self> |
hub entry | Show "already dealt with" only after completion. |
QUESTRESREF + QUESTSTATE |
<self> + ABSENT/FALLBACK |
ask-help split | ABSENT = accept (gated by prereq); FALLBACK = not-ready catch-all. Prereq resolved from the DB row for <self>. |
COMPLETEPROGRESS |
<self> |
turn-in node | Show turn-in only at 100% progress. |
Actions (on replies, ActionParams / Script)
| Script | Param | Value | Purpose |
|---|---|---|---|
q_startquestat |
STARTQUEST |
<self> |
Accept the quest. |
q_finishquestat |
QUESTRESREF |
<self> |
Turn in the quest. |
q_return |
(none) | — | End the conversation and return control to the hub. |
<self> is always the sub-dialogue's own quest resref. There are no GIVEITEM/REMOVE/NOREWARD overrides in the current set; rewards come from the gc_quests row.
JSON structure reminder
In these .dlg.json files, condition parameters live in ConditionParams on the reply reference (inside an entry's RepliesList), and action parameters live in ActionParams on the reply itself. Both are flat key/value dictionaries:
// Ask-help split: two child entries on the "Ask how you can help." reply.
// Child [0] — accept branch (evaluated first):
{
"Quest": { "type": "cexostring", "value": "q_queststatusaw" },
"ConditionParams": {
"type": "list",
"value": [
{ "__struct_id": 0, "Key": {"type":"cexostring","value":"QUESTRESREF"}, "Value": {"type":"cexostring","value":"q_pyrami"} },
{ "__struct_id": 1, "Key": {"type":"cexostring","value":"QUESTSTATE"}, "Value": {"type":"cexostring","value":"ABSENT"} }
]
},
"Target": { "type": "dword", "value": 4 }
}
// Child [1] — not-ready fallback (catch-all):
{
"Quest": { "type": "cexostring", "value": "q_queststatusaw" },
"ConditionParams": {
"type": "list",
"value": [
{ "__struct_id": 0, "Key": {"type":"cexostring","value":"QUESTRESREF"}, "Value": {"type":"cexostring","value":"q_pyrami"} },
{ "__struct_id": 1, "Key": {"type":"cexostring","value":"QUESTSTATE"}, "Value": {"type":"cexostring","value":"FALLBACK"} }
]
},
"Target": { "type": "dword", "value": 3 }
}
// On the accept node's reply (action):
"Script": { "type": "resref", "value": "q_startquestat" },
"ActionParams": {
"type": "list",
"value": [
{ "__struct_id": 0, "Key": {"type":"cexostring","value":"STARTQUEST"}, "Value": {"type":"cexostring","value":"q_pyrami"} }
]
}
Gotchas
- Positional IDs. Array index ==
__struct_id. Never delete an entry or reply mid-list; append new ones at the end. Deleting one shifts every subsequent ID and orphans references, producing empty dialogues in game. - Three parallel blocks. Any structural change must be applied identically to all three variant blocks (E0–E4, E5–E9, E10–E14) or one NPC will behave differently from the others.
- The ask-help split is self-referencing, not parent-referencing.
QUESTRESREFmust be<self>; the prerequisite is read from that row'srequiredquestcolumn ingc_quests. Putting the parent resref here is the legacy pattern and breaks if the DB prerequisite ever changes — the dialogue would silently gate on the wrong quest. If the accept door won't open or the not-ready door won't close, check that the<self>row exists ingc_questswith the intendedrequiredquest. - Use the modern contract for the parent gate. Prefer
QUESTRESREF+QUESTSTATEover legacyQUESTCOMPLETE/NOTCOMPLETE. The legacy path skips the database-backed evaluator, fails silently (no log), and dead-ends the conversation when unresolved. - Keep the not-ready exit reply. Every not-ready node needs its
q_returnreply, or players who reach it cannot leave. - Fresh start at entry level 0. Because the hub launches these with
ActionStartConversation(..., false), the conversation always begins at the first starting node; there is no memory of a prior visit within the same session.