How-To: Author a Quest (manifest → stamp → dialogue → validate → overview)¶
last-verified: 2026-07-10 against
1c31ca4d0
This is the follow-along recipe for turning a quest design (paper, sheet, or CSV from the
narrative designer) into working game content through the narrative pipeline. Reference docs:
Tools/Narrative/SPEC_FORMAT.md (full manifest schema) and
Documentation/08_Progression/01_Quest_System.md (how quests work at runtime).
The pipeline in one line:
What you need before starting¶
- The quest design: name, description, the story beats, objectives, rewards, compendium entries, and the dialogue (speakers, lines, branches, and where quest progress happens).
- The editor closed for headless stamping, or open with the MCP bridge for live stamping.
Step 1 — Derive the tag graph¶
Quests are tag graphs (no checklist engine). From the design, list every moment the game must remember, and give each a tag under one root:
| Design says | Tag |
|---|---|
| the quest itself | Quest.RedDesert (root — goes in quest.tag, NOT in tags[]) |
| "after talking to Martha about the Red Bane" | Quest.RedDesert.Discovery.RedBaneExplained |
| "when entering the Dead Mines" | Quest.RedDesert.Objective.EnteredMines |
| an objective is done | one tag per objective — this becomes its completionTag |
| quest complete | Quest.RedDesert.Completed — do not author it; the stamp always creates <root>.Completed for you |
Rules of thumb: Discovery.* for lore/knowledge beats, Objective.* for progress beats. Every
compendium entry's unlock condition must map to a tag that something grants with an exact
match — a child grant does NOT unlock a parent-tagged entry.
Step 2 — Write the manifest¶
Create Tools/Narrative/Quests/Quest_<Name>.quest.json. Copy an existing one as a template
(Quest_Martha.quest.json is small; SPEC_FORMAT.md documents every field). Filling it from a
designer sheet/CSV maps like this:
| Sheet section | Manifest section | Notes |
|---|---|---|
| Quest name/description | quest.name / quest.description |
asset: /Game/DataAssets/Quests/Quest_<Name> (created if missing) |
| Objectives | objectives[] |
completionTag from Step 1; optional: false |
| Dialogue | dialogues[] (paths only) |
line text is authored in Step 4, never in the manifest |
| Compendium / lore | compendium[] |
entryId globally unique — prefix with quest name (RedDesert_Cinob); "unlocked when" prose → requiredTag; relatedQuestTag: null defaults to the root |
| Journal tile the entries live under | compendium[].category + categories[] |
thematic/locational (Compendium.Category.RedDesert — "The Red Desert"), NOT one-tab-per-quest; declare display data (title, banner) in categories[], the stamp creates the display asset. Reuse an existing category when the lore belongs there |
| "The entry grows when the player later does X" | deepen[] |
stage = entryId + exact requiredTag + new content (mode: append/replace); entry flips back to unread, no toast. Target may be another quest's entry |
| Items (rewards) | rewards[] |
itemTemplate must resolve to a UItemManifestDataAsset (not a TPL_) |
| Every "when X happens" that is NOT in a dialogue | granters[] |
declare verb + tag + via (item/map/blueprint) + source. This is how kill/location/pickup triggers pass validation before the content exists |
| Recurring lore keywords | redText[] |
rendered red in compendium/dialogue text |
Also add one tags[] entry (with a comment — it becomes the ini DevComment) for every tag
from Step 1 except the root and .Completed.
Never write a _meta block by hand — it is tool-owned; a manifest without one stamps
unconditionally on first run.
Step 3 — Stamp¶
Editor closed (headless):
& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
"C:\Projects\Project-Eternal\ProjectEternal.uproject" -run=pythonscript `
-script="Tools/Narrative/quest_stamp.py stamp Tools/Narrative/Quests/Quest_<Name>.quest.json"
Editor open: run the same quest_stamp.py via MCP system_control execute_python.
Always read the report at Saved/Narrative/last_stamp.json — the process exit code lies
(-run=pythonscript exits 0 even on failure). What you want: "ok": true with per-output
wrote. What you might see instead:
status: "stale"— someone hand-edited the quest asset since the last stamp. Runquest_stamp.py extract <AssetPath>to pull the edits back into the manifest (review the diff), or re-stamp with--forceto knowingly overwrite.- Re-stamping an unchanged manifest is a no-op (all outputs
skipped) — that's normal.
The stamp writes the tag block into Config/Tags/GameplayTags.ini (via the live registry, so
the tags exist in-session), creates/updates the UQuestDataAsset, and merges red-text rules.
Step 4 — Author the dialogue graphs¶
In the editor, create one DlgSystem dialogue asset per conversation (path must match what you
put in dialogues[]). For each:
- Participant name = the NPC's
NPCID(the C++ participant identity onAEternalNPC), not the display name. - Write the nodes/branches — line text lives here, in-graph.
- Quest progress happens through the custom nodes:
QuestDialogueEvent(StartQuest / CompleteQuest / AddQuestTag / RemoveQuestTag) on the node where the beat lands, andQuestDialogueCondition(QuestTag+ require/forbid) to gate branches. - Never bind quest verbs to the NPC participant — NPC-bound quest verbs are nondeterministic with 2 players and the validator errors on them. Bind to the player.
- Save. Saving writes the committed
.dlg.jsonsidecar next to the asset — commit it with the asset.
Step 5 — Validate¶
& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
"C:\Projects\Project-Eternal\ProjectEternal.uproject" -run=NarrativeValidation
(-ExportSidecars rewrites all sidecars first if you suspect they're stale.)
This checks the whole graph: every checked/required tag is granted somewhere (statically
visible OR declared in granters[]), exact-match compendium unlocks, unique entry IDs,
resolvable rewards, fresh sidecars, no NPC-bound quest verbs. Fix errors before moving on —
trust the validator over any document, including this one.
Step 6 — Generate the overview¶
& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\ThirdParty\Python3\Win64\python.exe" `
Tools/Narrative/quest_overview.py
No editor needed (pure Python). Open Saved/Narrative/QuestOverview.html in a browser: per-quest
map (granters → tags → dialogue flow → objectives → compendium), transcripts, and the cross-quest
map. This is the "does the whole quest hang together" review artifact — read it as the designer
would.
Step 7 — Play it¶
PIE from L_Default. Talk through the dialogue, watch the quest start, check compendium
entries unlock at their beats and the category shows its name/banner. For anything granted
via: map/item/blueprint that doesn't exist yet, grant the tag manually to test downstream
flow (the granters[] declaration is the reminder of what still needs building).
Troubleshooting¶
| Symptom | Cause / fix |
|---|---|
Stamp report status: "stale" |
Hand edit in the editor since last stamp → extract, review, re-stamp |
New tag is None when used in an asset |
Tag wasn't registered through the stamp/editor — hand-written ini lines are invisible until restart; always let the stamp author tags |
| Compendium entry never unlocks | requiredTag isn't granted with an exact match (child grant ≠ parent unlock) |
| Validator: unexplained checked tag | The grant is invisible to the static scan → declare it in granters[] |
| Sidecar mismatch error | Dialogue asset saved without its sidecar committed, or sidecar hand-edited → -ExportSidecars, commit both |