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

Content pack assetServer/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.

Content pack assetServer/MMOSkillTree/DialogueTemplates/<id>.jsonDialogueTemplates/

Native Parent bases for dialogues. This type loads before Dialogues, so a dialogue's Parent can target one.

Content pack assetServer/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).

Server/MMOSkillTree/Dialogues/Guide_Wilds_Dialogue.json (shape)
{
  "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" } ] }
  }
}
FieldTypeDefaultDescription
NamestringoptionalHuman-readable echo of the filename-derived id. Ignored on decode, emitted on export.
Payload.Parent1.5.0stringoptionalNative Parent inheritance target (a dialogue or a DialogueTemplates/ id). Stripped before decode; not visible to the codec. See Native Parent inheritance.
Payload.Startarray of DialogueEntryoptionalOrdered greeting-node candidates; the first whose Conditions (AND-combined) pass wins. Inherits from Parent if omitted.
Payload.Nodesmap: nodeId → DialogueNodeoptionalThe node graph. Keyed-merges against Parent if set.
Payload.Fragmentsmap: fragmentId → DialogueOption[]optionalReusable option lists a node splices in via IncludeOptions. Stripped before decode; the codec never sees it. See Fragments.
* required

A DialogueEntry (one Start row) has Node (string, required) and an optional Conditions array (AND-combined).

DialogueNode fields

FieldTypeDefaultDescription
TextKeystringoptionali18n key for the NPC's line, resolved client-side onto #NodeText.TextSpans. Rich markup works here (<color is="#hex">, <b>, \n).
TextstringoptionalDeprecated raw fallback. Never author this - it is not localized.
Conditionsarray of DialogueConditionoptionalNode 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).
Optionsarray of DialogueOptionoptionalThe selectable lines.
IncludeOptionsstring array (fragment ids)optionalSplices 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.
* required

DialogueOption fields

FieldTypeDefaultDescription
LabelKeystringoptionali18n key for the button label. Buttons have no TextSpans - rich markup does not render here; use Presentation/Style for color instead.
LabelstringoptionalDeprecated raw fallback.
Conditionsarray of DialogueConditionoptionalAND-combined; the option hides while any fails. Re-evaluated on every render AND again on click (server-authoritative).
Actionsarray of DialogueActionoptionalOrdered; run in array order on click.
Presentationobject { Color, Icon }optionalPer-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).
StylestringoptionalOne of accept / turnin / continue / neutral / farewell (case-insensitive; unknown values are ignored). Overrides the action-derived style.
* required

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.

FieldTypeDefaultDescription
Talkstring (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.
Acceptstring (quest id)optional{"Type":"AcceptQuest","Quest":value}. Accepts through the normal quest gate (unlock/level/prereq); auto-tries a completion check right after.
Gotostring (node id)optional{"Type":"Goto","Node":value}.
Talk / Accept / Complete / TurnIn-optionalSee individual rows; all four expand in this fixed order regardless of authored key order.
TurnInstring (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.
Completestring (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.
Rewardobject (+ sibling Once)optional{"Type":"Reward","Once":<bool, default true>,"Reward":{...}}. See Reward semantics.
Runstring (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.
Openstring (page target)optional{"Type":"OpenPage","Target":value}. Any MmoUiRouter destination: npcquests:@self, npcquests:<npcId>, hub, shop, a board id, ...
Closeboolean trueoptional{"Type":"Close"}.
Doarray of single-key objectsoptionalThe 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.
* required
Ordered sugar via Do
{ "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}.

FieldTypeDefaultDescription
AcceptQuest{ Quest }optionalAccepts through the normal QuestService gate; fromNpc=true semantics; auto-tries a completion check right after.
TurnInQuest{ Quest }optionalTurns in the active TURN_IN objective, matched to the context NPC when the objective names a turnInNpcId.
Reward{ Once, Reward }optionalOne-shot grant of a unified content/reward object. See Reward semantics below.
CompleteQuest{ Quest }optionalForce-completes even with unmet objectives, via the same authority as /mmoquestadmin complete. Idempotent for a finished non-repeatable quest.
RunCommand{ Command, Once=false }optionalRuns 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? }optionalFires a TALK_TO_NPC objective via NpcTalkObjectiveUtil.fire; Target defaults to the context NPC.
* required

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.

FieldTypeDefaultDescription
QuestState{ Quest, State } or { Quest, States[] }optionalReads 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 }optionalEvaluated through the unified content/gate GateEvaluator - features/skills/minCombatLevel/permission.
HasActiveBounties(none)optionalTrue when the viewer can reach an accessible board with at least one active bounty.
HasOfferableQuests(none, NPC-aware)optionalTrue 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)optionalTrue when any active quest's remaining step is a turn-in/talk at the context NPC.
ReadyToTurnIn{ Quest }optionalTrue when THIS quest is ACTIVE and its remaining step resolves at the context NPC. Unknown quest id fails closed (unlike QuestState).
* required

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.

A shared footer fragment
"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

Dialogues do not use the 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:

  • DialogueBodyResolver resolves the Parent chain (topological, memoized, cycle-guarded with a warning) before the option-sugar pre-pass.
  • Start/Nodes are appendInherited at the top level: an omitted field inherits the parent's value wholesale.
  • Nodes keyed-merges per entry: a child Nodes entry overrides/adds by key while parent-only node ids inherit unchanged. Each DialogueNode's own fields (TextKey/Conditions/Options) are themselves appendInherited, so a child node can override just TextKey and still inherit the parent's Options.
  • Fragments/IncludeOptions resolve within the same body only (repeated from above, since it matters most here: a base's Fragments is invisible to its children).
  • Base skeletons should omit TextKey/LabelKey so 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.
Dialogues/Guide_Sands_Dialogue.json - inherits a shared giver base by Parent
{
  "Name": "Guide_Sands_Dialogue",
  "Payload": {
    "Parent": "questgiver_standard",
    "Nodes": { "lore": { "TextKey": "dialogue.guide_sands.desert_lore.text" } }
  }
}
No jar or pack dialogue currently ships a real 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

Quest-driven option/node visibility must derive from 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

Server/MMOSkillTree/Dialogues/My_Hermit_Dialogue.json
{
  "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:

Server/MMOSkillTree/Dialogues/My_Scout_Dialogue.json
{
  "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" }
      ]
    }
  }
}
An in-game NPC dialogue panel, showing the NPC's line and 2-3 colored/styled option buttons (accept in green, a neutral option in steel).
An in-game NPC dialogue panel, showing the NPC's line and 2-3 colored/styled option buttons (accept in green, a neutral option in steel).

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.

FieldTypeDefaultDescription
Namestring-Echo only; the id is the filename.
EnabledbooleantrueMaster on/off.
Rolestring(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.
Appearancestring-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).
NameKeystringnpcs.<npcId>.nameNameplate lang key on the generated role.
HintKeystringnpcs.questgiver.hintPress-F hint lang key on the generated role (shared default).
NpcIdstring-The NPC's own quest-objective/turn-in id (used by turnInNpcId, Talk targets, etc).
Dialoguestring-A Dialogues/<id> id this giver attaches; pressing F opens it. Omit to fall back to the plain NPC quest list.
Placementstring enumMANUALSPAWN | ZONE_DISCOVERY | STRUCTURE | MANUAL. STRUCTURE anchors beside a matching worldgen SpawnMarker (via Match), one per structure instance (persistent seen-set).
Zonestring-Zone-discovery scoping, used with Placement: "ZONE_DISCOVERY".
Worldsstring array-World-name allow-list (exact/prefix/suffix/contains/* matching).
Offsetobject { X, Y, Z }0,0,0Spawn-position offset relative to the anchor (hub Guide / matched marker).
Yawdouble180.0Facing.
Matchobject { MarkerIds, Roles, KeyContains }-STRUCTURE-placement allow-list; any listed value hit anchors. All three fields are string arrays.
SpawnChancedouble1.0Roll chance per eligible spawn point.
MaxPerWorldint0 (unlimited)1 = a unique giver per world.
* required

Practical minimum: Appearance plus the npcs.<id>.name lang value it reuses for objective text - NameKey/HintKey are optional overrides.

Minimal quest-giver example

Server/MMOSkillTree/QuestGivers/My_Camp_Elder.json
{
  "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:

Server/MMOSkillTree/QuestGivers/Guide_Wilds.json
{
  "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.json

See 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.