Authoring Dialogues & Quest Givers
Branching NPC conversations and the NPCs that carry them
A Dialogue is a node graph of NPC lines and player-selectable options. A QuestGiver is a separate content type: the NPC's spawn placement, role, and appearance. NPCs never own a dialogue file directly - a QuestGiver.Dialogue field (or a spawn-hub.json dialogue key) attaches one by id, so one dialogue tree can serve many NPCs.
Configuration seam
Server/MMOSkillTree/Dialogues/<id>.jsonDialogues/Pattern B raw-Payload asset (NpcDialogueAsset). The dialogue engine itself lives in ziggfreed-common; the mod registers its own actions and conditions onto it.
Server/MMOSkillTree/DialogueTemplates/<id>.jsonDialogueTemplates/Native Parent bases for dialogues. This type loads before Dialogues, so a dialogue's Parent can target one.
Server/MMOSkillTree/QuestGivers/<id>.jsonQuestGivers/Pattern A structured asset (QuestGiverAsset.CODEC). The same codec decodes the owner-layer mods/mmoskilltree/quest-givers.json, so an owner file can add or override givers by id (pack, then owner, last write wins).
Dialogue file shape
Folder: Server/MMOSkillTree/Dialogues/<id>.json (id = filename, lowercased).
{
"Name": "guide_wilds_dialogue",
"Payload": {
"Parent": "questgiver_standard",
"Start": [
{ "Node": "greet", "Conditions": [ { "Type": "QuestState", "Quest": "...", "State": "ACTIVE" } ] }
],
"Nodes": { "greet": { "TextKey": "...", "Options": [ ... ] } },
"Fragments": { "hub_footer": [ { "LabelKey": "...", "Open": "hub" } ] }
}
}| Field | Type | Default | Description |
|---|---|---|---|
Name | string | optional | Human-readable echo of the filename-derived id. Ignored on decode, emitted on export. |
Payload.Parent1.5.0 | string | optional | Native Parent inheritance target (a dialogue or a DialogueTemplates/ id). Stripped before decode; not visible to the codec. See Native Parent inheritance. |
Payload.Start | array of DialogueEntry | optional | Ordered greeting-node candidates; the first whose Conditions (AND-combined) pass wins. Inherits from Parent if omitted. |
Payload.Nodes | map: nodeId → DialogueNode | optional | The node graph. Keyed-merges against Parent if set. |
Payload.Fragments | map: fragmentId → DialogueOption[] | optional | Reusable option lists a node splices in via IncludeOptions. Stripped before decode; the codec never sees it. See Fragments. |
A DialogueEntry (one Start row) has Node (string, required) and an optional Conditions array (AND-combined).
DialogueNode fields
| Field | Type | Default | Description |
|---|---|---|---|
TextKey | string | optional | i18n key for the NPC's line, resolved client-side onto #NodeText.TextSpans. Rich markup works here (<color is="#hex">, <b>, \n). |
Text | string | optional | Deprecated raw fallback. Never author this - it is not localized. |
Conditions | array of DialogueCondition | optional | Node self-gate. When a Start candidate resolves to this node, these must ALSO pass (AND). Lets one node cover state-varied beats instead of duplicating (node × state). |
Options | array of DialogueOption | optional | The selectable lines. |
IncludeOptions | string array (fragment ids) | optional | Splices each named Fragments entry's options in AFTER this node's own options. Unknown id logs a warning; the node keeps its own options. Stripped before decode. |
DialogueOption fields
| Field | Type | Default | Description |
|---|---|---|---|
LabelKey | string | optional | i18n key for the button label. Buttons have no TextSpans - rich markup does not render here; use Presentation/Style for color instead. |
Label | string | optional | Deprecated raw fallback. |
Conditions | array of DialogueCondition | optional | AND-combined; the option hides while any fails. Re-evaluated on every render AND again on click (server-authoritative). |
Actions | array of DialogueAction | optional | Ordered; run in array order on click. |
Presentation | object { Color, Icon } | optional | Per-option color/icon override. Color is a hex string tinting the button's default/hover/press states. Icon is {"Item": "<item id>"} or {"Glyph": "<token>"} (Item wins if both set). |
Style | string | optional | One of accept / turnin / continue / neutral / farewell (case-insensitive; unknown values are ignored). Overrides the action-derived style. |
When Style/Presentation are absent, each registered action type carries a default style resolved from the option's decisive action: AcceptQuest/CompleteQuest → accept (green), TurnInQuest → turn-in (blue), OpenPage/RunCommand → neutral (steel), the engine's default Farewell → muted X. An option with no Goto/Close action just re-renders the same node - useful for a conditions-gated informational line.
Option sugar
A pre-codec rewrite (DialogueSugar) turns flat option keys into canonical Actions, then strips the consumed keys - the codec/validator/executor only ever see canonical actions. Bare (non-Do) sugar keys on one option expand in the fixed order below regardless of authored key order. Sugar appends after any explicit Actions array, so both can be mixed.
| Field | Type | Default | Description |
|---|---|---|---|
Talk | string (may be "" or omitted) | optional | → {"Type":"Talk"(,"Target":value)}. The MMO overrides the generic no-op carrier to fire a TALK_TO_NPC objective, defaulting Target to the context NPC. |
Accept | string (quest id) | optional | → {"Type":"AcceptQuest","Quest":value}. Accepts through the normal quest gate (unlock/level/prereq); auto-tries a completion check right after. |
Goto | string (node id) | optional | → {"Type":"Goto","Node":value}. |
Talk / Accept / Complete / TurnIn | - | optional | See individual rows; all four expand in this fixed order regardless of authored key order. |
TurnIn | string (quest id) | optional | → {"Type":"TurnInQuest","Quest":value}. Turns in the quest's active TURN_IN objective, matched to the context NPC when the objective names a turnInNpcId. |
Complete | string (quest id) | optional | → {"Type":"CompleteQuest","Quest":value}. Force-completes via the same authority as /mmoquestadmin complete, even with unmet objectives - the "skip the tutorial" path. Idempotent for a finished non-repeatable quest. |
Reward | object (+ sibling Once) | optional | → {"Type":"Reward","Once":<bool, default true>,"Reward":{...}}. See Reward semantics. |
Run | string (command) | optional | → {"Type":"RunCommand","Command":value}. A sibling Once is not auto-consumed by this bare sugar form - use explicit Actions or Do for a one-shot RunCommand. |
Open | string (page target) | optional | → {"Type":"OpenPage","Target":value}. Any MmoUiRouter destination: npcquests:@self, npcquests:<npcId>, hub, shop, a board id, ... |
Close | boolean true | optional | → {"Type":"Close"}. |
Do | array of single-key objects | optional | The escape hatch when order matters or a step repeats. Each atom desugars in ARRAY order (keys within an atom in authored order) - e.g. Talk then TurnIn then Goto. |
{ "LabelKey": "...", "Do": [ { "Talk": "" }, { "TurnIn": "wilds_call_of_the_dunes" }, { "Goto": "desert_lore" } ] }Style is not sugar
Style is a separate top-level option key, never sugar for an action.Actions
Generic actions come from ziggfreed-common: Goto{Node}, Close{}, SetFlag{Flag}, Talk{Target?} (carrier only - the MMO overrides the handler), OpenPage{Target}.
| Field | Type | Default | Description |
|---|---|---|---|
AcceptQuest | { Quest } | optional | Accepts through the normal QuestService gate; fromNpc=true semantics; auto-tries a completion check right after. |
TurnInQuest | { Quest } | optional | Turns in the active TURN_IN objective, matched to the context NPC when the objective names a turnInNpcId. |
Reward | { Once, Reward } | optional | One-shot grant of a unified content/reward object. See Reward semantics below. |
CompleteQuest | { Quest } | optional | Force-completes even with unmet objectives, via the same authority as /mmoquestadmin complete. Idempotent for a finished non-repeatable quest. |
RunCommand | { Command, Once=false } | optional | Runs an arbitrary console command with {player} substituted. Same once-flag shape as Reward but not a reward-model grant (no offline-retry queue) - the generic "run anything" escape hatch. |
Talk (override) | { Target? } | optional | Fires a TALK_TO_NPC objective via NpcTalkObjectiveUtil.fire; Target defaults to the context NPC. |
Runtime conditions
Generic conditions ship Flag/NotFlag/AllOf/AnyOf/Not combinators. Combine them freely with the MMO-registered conditions below to express state-varied beats without duplicating nodes.
| Field | Type | Default | Description |
|---|---|---|---|
QuestState | { Quest, State } or { Quest, States[] } | optional | Reads the quest's effective state (a completed off-cooldown repeatable re-reads NOT_STARTED). States: ["ACTIVE","NOT_STARTED"] is the OR-shorthand - replaces the old two-Start-row duplication for "greet while active or fresh". Unknown quest id passes silently (never hides content); the validator flags it. |
Gate | { Requirements } | optional | Evaluated through the unified content/gate GateEvaluator - features/skills/minCombatLevel/permission. |
HasActiveBounties | (none) | optional | True when the viewer can reach an accessible board with at least one active bounty. |
HasOfferableQuests | (none, NPC-aware) | optional | True when the context NPC currently has at least one offerable quest (visible, NOT_STARTED, acceptable now) - no quest-id enumeration needed. |
HasReadyToTurnIn | (none, NPC-aware) | optional | True when any active quest's remaining step is a turn-in/talk at the context NPC. |
ReadyToTurnIn | { Quest } | optional | True when THIS quest is ACTIVE and its remaining step resolves at the context NPC. Unknown quest id fails closed (unlike QuestState). |
Fragments and IncludeOptions
Payload.Fragments is a map of reusable option lists a node splices in via IncludeOptions (spliced AFTER the node's own Options, so a node's own lead trails into a shared footer). Fragments resolve within the same body only - a fragment defined in a Parent is not visible to a child. Keep shared footers in the body that uses them, or in a base the child fully inherits.
"Fragments": {
"footer": [
{ "LabelKey": "dialogue.my_scout_dialogue.opt.open", "Style": "neutral", "Open": "hub" }
]
}Reward action semantics
The Reward action grants a unified content/reward object, decoded directly through Reward.CODEC. Once (default true) guards an implicit per-player flag reward:<dialogueId>:<node>:<optionIndex>- keyed on the dialogue, not the NPC, so a shared dialogue's gift can't be farmed across every NPC it's attached to. This flag is a normal dialogue flag: it can be read with NotFlag/Flag in another Conditions block, which is real shipped-pack usage (hiding a gift option after it's claimed).
Native Parent inheritance Since 1.5.0
The old extends / params DSL is gone
extends/params/nodeOverrides/ PruneIfEmpty template DSL that Masteries, Quests, Achievements, CommandRewards, and Shop still use. As of 1.5.0 a dialogue body sets a top-level "Parent": "<id>" (another dialogue, or a DialogueTemplates/ base) resolved through the engine's real decodeAndInheritJson. There is no {{param}} substitution and no prune - state-varied beats are runtime Conditions/AnyOf/QuestState.Statesinstead.How resolution works, in order:
DialogueBodyResolverresolves theParentchain (topological, memoized, cycle-guarded with a warning) before the option-sugar pre-pass.Start/NodesareappendInheritedat the top level: an omitted field inherits the parent's value wholesale.Nodeskeyed-merges per entry: a childNodesentry overrides/adds by key while parent-only node ids inherit unchanged. EachDialogueNode's own fields (TextKey/Conditions/Options) are themselvesappendInherited, so a child node can override justTextKeyand still inherit the parent'sOptions.Fragments/IncludeOptionsresolve within the same body only (repeated from above, since it matters most here: a base'sFragmentsis invisible to its children).- Base skeletons should omit
TextKey/LabelKeyso each concrete child gets its own convention loc-namespace for free (dialogue.<childId>.<node>.text), rather than hardcoding a namespace the base's own id would produce.
{
"Name": "Guide_Sands_Dialogue",
"Payload": {
"Parent": "questgiver_standard",
"Nodes": { "lore": { "TextKey": "dialogue.guide_sands.desert_lore.text" } }
}
}Parent chain in production - the shipped pack trees (guide_wilds_dialogue, guide_sands_dialogue, quartermaster_wilds_dialogue) are all standalone. Treat the example above as the confirmed-but-unexercised mechanism shape.Self-heal convention
Never gate quest progress with a story flag
QuestState (or Start/nodeConditions precedence) - never a parallel SetFlag/NotFlag story flag for quest progress. A story flag survives a quest reset and soft-locks the dialogue at a stale branch, because QuestState resets with the quest but a flag does not. Reserve SetFlag/NotFlag for genuine one-shot non-quest state (a lore-seen toggle, or the implicit reward:<dialogue>:<node>:<idx> once-guard the engine already manages for you via Reward Once:true).Localization keys
Convention: dialogue.<dialogueId>.<node>.text (node text) and dialogue.<dialogueId>.<node>.opt.<index> (index is the option's position in the decoded node, after IncludeOptions splicing - so a fragment option's key namespace belongs to whichever body it lives in). Author these in the pack .lang file; never author a raw Text/Label. A Parent-inheriting child gets its own namespace from its own filename id.
Minimal dialogue example
{
"Name": "my_hermit_dialogue",
"Payload": {
"Start": [ { "Node": "greet" } ],
"Nodes": {
"greet": {
"TextKey": "dialogue.my_hermit_dialogue.greet.text",
"Options": [
{ "LabelKey": "dialogue.my_hermit_dialogue.greet.opt.leave", "Close": true }
]
}
}
}
}Kitchen-sink dialogue example
Fragments, node self-gating, style, a once-only gift reward read back through a flag condition, and a gate condition:
{
"Name": "my_scout_dialogue",
"Payload": {
"Start": [
{ "Node": "greet_active",
"Conditions": [ { "Type": "QuestState", "Quest": "scout_favor", "State": "ACTIVE" } ] },
{ "Node": "greet" }
],
"Nodes": {
"greet": {
"TextKey": "dialogue.my_scout_dialogue.greet.text",
"Options": [
{ "LabelKey": "dialogue.my_scout_dialogue.greet.opt.accept",
"Conditions": [ { "Type": "Gate", "Requirements": { "minCombatLevel": 5 } } ],
"Style": "accept",
"Do": [ { "Accept": "scout_favor" }, { "Goto": "greet_active" } ] }
],
"IncludeOptions": [ "footer" ]
},
"greet_active": {
"TextKey": "dialogue.my_scout_dialogue.greet_active.text",
"Options": [
{ "LabelKey": "dialogue.my_scout_dialogue.opt.gift",
"Conditions": [ { "Type": "NotFlag", "Flag": "reward:my_scout_dialogue:greet_active:0" } ],
"Presentation": { "Color": "#5ab0ff", "Icon": { "Item": "hytale:iron_arrow" } },
"Reward": { "Type": "COMMAND", "Command": "/give {player} Item_Arrow_Iron --quantity=8" },
"Once": true }
],
"IncludeOptions": [ "footer" ]
}
},
"Fragments": {
"footer": [
{ "LabelKey": "dialogue.my_scout_dialogue.opt.open", "Style": "neutral", "Open": "hub" }
]
}
}
}
Quest-giver authoring
Folder: Server/MMOSkillTree/QuestGivers/<id>.json (id = filename). Pattern A - QuestGiverAsset.CODEC decodes typed PascalCase fields directly (no Payload wrapper), the same codec used for the owner layer mods/mmoskilltree/quest-givers.json.
| Field | Type | Default | Description |
|---|---|---|---|
Name | string | - | Echo only; the id is the filename. |
Enabled | boolean | true | Master on/off. |
Role | string | (generated) | An explicit NPC role id. Omit to let the jar GENERATE MMO_Giver_<id> from the base MMO_QuestGiver role at runtime when Appearance is set - zero hand-authored role JSON needed. Set explicitly only to ship a custom hand-authored role instead. |
Appearance | string | - | A vanilla appearance id. When set and Role is omitted, QuestGiverRoleGenerator clones MMO_QuestGiver into MMO_Giver_<id> with this appearance plus the nameplate/hint below, registered as a runtime asset pack at start() (before worldgen streams). |
NameKey | string | npcs.<npcId>.name | Nameplate lang key on the generated role. |
HintKey | string | npcs.questgiver.hint | Press-F hint lang key on the generated role (shared default). |
NpcId | string | - | The NPC's own quest-objective/turn-in id (used by turnInNpcId, Talk targets, etc). |
Dialogue | string | - | A Dialogues/<id> id this giver attaches; pressing F opens it. Omit to fall back to the plain NPC quest list. |
Placement | string enum | MANUAL | SPAWN | ZONE_DISCOVERY | STRUCTURE | MANUAL. STRUCTURE anchors beside a matching worldgen SpawnMarker (via Match), one per structure instance (persistent seen-set). |
Zone | string | - | Zone-discovery scoping, used with Placement: "ZONE_DISCOVERY". |
Worlds | string array | - | World-name allow-list (exact/prefix/suffix/contains/* matching). |
Offset | object { X, Y, Z } | 0,0,0 | Spawn-position offset relative to the anchor (hub Guide / matched marker). |
Yaw | double | 180.0 | Facing. |
Match | object { MarkerIds, Roles, KeyContains } | - | STRUCTURE-placement allow-list; any listed value hit anchors. All three fields are string arrays. |
SpawnChance | double | 1.0 | Roll chance per eligible spawn point. |
MaxPerWorld | int | 0 (unlimited) | 1 = a unique giver per world. |
Practical minimum: Appearance plus the npcs.<id>.name lang value it reuses for objective text - NameKey/HintKey are optional overrides.
Minimal quest-giver example
{
"Name": "My_Camp_Elder",
"NpcId": "my_camp_elder",
"Appearance": "Feran_Windwalker",
"Placement": "manual"
}Kitchen-sink quest-giver example
Structure-anchored placement, a per-world allow-list, and a dialogue attach - the real shipped shape:
{
"Name": "Guide_Wilds",
"NpcId": "guide_wilds",
"Dialogue": "guide_wilds_dialogue",
"Appearance": "Feran_Windwalker",
"NameKey": "npcs.guide_wilds.name",
"HintKey": "npcs.questgiver.hint",
"Placement": "structure",
"Worlds": ["default"],
"Match": { "MarkerIds": ["Kweebec"] },
"SpawnChance": 1.0,
"MaxPerWorld": 1,
"Offset": { "X": 2.0, "Y": 0.0, "Z": 1.5 },
"Yaw": 180.0
}Pack folder layout
MyQuestGiverPack/
├── manifest.json
└── Server/
└── MMOSkillTree/
├── Dialogues/
│ └── My_Scout_Dialogue.json
├── DialogueTemplates/
│ └── Questgiver_Standard.json
└── QuestGivers/
└── My_Camp_Elder.jsonSee Content Types for the full pack asset-type catalog and feature-gating, and Quests for the quest objectives a dialogue's Accept/TurnIn/Complete actions drive.