District Quest Chain Builder Guide
This guide describes how to add a district quest chain like the Foreign Quarter and Puddles chains. A chain consists of one parent writ and an ordered set of child quests. Finishing each child quest contributes an equal share toward the parent.
Working examples:
Golarion-Chronicles/src/Quests/q_fqsewers.csGolarion-Chronicles/src/Quests/q_puddlesdistrict.csGolarion-Chronicles/src/Quests/q_uppersiphons.cs
Shared implementation:
Golarion-Chronicles/src/Quests/DistrictQuestChain.csGolarion-Chronicles/src/Quests/DistrictQuestChainRegistry.cs
1. Plan the chain
Choose:
- One unique parent quest resref.
- An ordered list of child quest resrefs.
- The area tag or original resref used by each objective.
- The objective type and targets for each child.
- Any exceptional behavior that requires a callback.
The objective order in the definition should match the dialogue progression. All child objectives contribute equally to the parent. Four objectives therefore contribute 25% each when they are turned in and marked finished.
Use area identifiers with CustomFunctions.AreaMatches semantics. This allows the configured value to match either the area tag or resref, including dungeon-reset areas whose reconstructed resref changes.
2. Add the database rows
Add the parent and every child quest to the quests table in gc_quests.sqlite3.
Required fields include:
Name: player-facing quest name.resref: exact identifier used by code and dialogue.level: intended quest level. Used by the modern condition evaluator to gate options by player level (±2 range).questtext: journal description.type: normallyWRITfor the parent andTASKfor its children.requiredquest: the resref of the prerequisite quest that must be finished before this one appears. Chain children sequentially (child 2 requires child 1, etc.). The parent writ typically has no prerequisite unless it depends on another district. This column is what drives sequential ordering in NPC dialogues without explicitQUESTCOMPLETEconditions.itemreward: optional item resref granted on turn-in.
Set the appropriate Venture Captain column (sovereigncourt, libertysedge, or theexchange) on the parent writ. Child tasks normally do not need Venture Captain assignments.
The registry validates all configured resrefs against gc_quests during module load and logs an error for missing rows.
3. Create the district class
Create one class under Golarion-Chronicles/src/Quests. Keep all resrefs, objective targets, area identifiers, and unique mechanics for that district in this class.
using Anvil.API;
using Anvil.Services;
[DistrictQuestChain]
[ServiceBinding(typeof(q_newdistrict))]
public class q_newdistrict
{
public static readonly DistrictQuestChainDefinition Definition = new(
"q_newdistrict",
[
DistrictObjective.Waypoints(
"q_newdistrict_scout",
"newdistrict_main",
"New District Scouted",
"NewDistrictMarket", "NewDistrictTemple", "NewDistrictWatch"),
DistrictObjective.Kills(
"q_newdistrict_threats",
20,
"newdistrict_main",
["newdistrict_thug", "newdistrict_bandit"]),
DistrictObjective.Kills(
"q_newdistrict_elites",
8,
"newdistrict_interior",
["newdistrict_elite"]),
DistrictObjective.Boss(
"q_newdistrict_boss",
"newdistrict_interior",
"newdistrict_boss"),
]);
}
Requirements:
- Add
[DistrictQuestChain]; this enables automatic module-load discovery. - Expose the definition as
public static readonlywith the exact nameDefinition. - Do not register the class manually in
QuestFunctionsorWritQuestProgress. - Do not pass event-data objects between methods. Extract the required game object first.
4. Select objective types
Waypoint objective
DistrictObjective.Waypoints(
"q_newdistrict_scout",
"newdistrict_main",
"New District Scouted",
"WaypointOne", "WaypointTwo", "WaypointThree")
The arguments are:
- Child quest resref.
- Area tag or resref.
- Floating progress-message text.
- Every unique waypoint tag that must be found.
Each waypoint is counted once. Discovery state is stored on the player's skin under the child quest resref. Progress is calculated from the number discovered, so any number of waypoints is supported.
Kill objective
DistrictObjective.Kills(
"q_newdistrict_threats",
20,
"newdistrict_main",
["newdistrict_thug", "newdistrict_bandit"])
The arguments are:
- Child quest resref.
- Number of qualifying kills required.
- Area tag or resref.
- Accepted creature resrefs.
Kill objectives use the existing reward-kill path. Anyone who qualifies for kill experience and has the active quest can receive progress.
Boss objective
DistrictObjective.Boss(
"q_newdistrict_boss",
"newdistrict_interior",
"newdistrict_boss")
A boss objective is a kill objective with a required count of one.
Custom heartbeat objective
Use this only when waypoint, kill, or boss behavior is insufficient.
DistrictObjective.Heartbeat(
"q_newdistrict_objects",
"newdistrict_main",
ScanDistrictObjects)
The callback receives the player creature, not event data. It must return a Task because the registry awaits it on each heartbeat:
private static async Task ScanDistrictObjects(NwCreature player)
{
// District-specific nearby-object behavior.
await Task.CompletedTask;
}
The shared registry still controls subscription lifetime. The callback only runs while the player is in the configured area with that child quest active and below 100% progress.
5. Add optional completion behavior
Kill and boss objectives can run a district-specific callback when that kill advances the objective to 100%:
DistrictObjective.Kills(
"q_newdistrict_keyholder",
8,
"newdistrict_interior",
["newdistrict_keyholder"],
OnKeyholderObjectiveCompleted)
private static Task OnKeyholderObjectiveCompleted(NwCreature player, NwCreature killed)
{
killed.SpeakString("*A key falls to the floor.*");
NwItem.Create("newdistrict_key", killed.Location);
return Task.CompletedTask;
}
The callback signature is Func<NwCreature, NwCreature, Task> — it must return a Task. Use return Task.CompletedTask; for synchronous work, or make the method async if it awaits anything. Keep callbacks small and district-specific. Shared progress logic belongs in the objective framework.
6. Build the dialogue chain
All three existing districts use the modern q_queststatusaw contract: QUESTRESREF + QUESTSTATE. The legacy parameters (NOTCOMPLETE, COMPLETEPROGRESS, HIDEINPROGRESS, QUESTINPROGRESS) still work but are superseded. See DIALOGUE_PARAMETERS.md for the full reference.
Parent writ dialogue
The parent writ NPC (e.g. Shristi for Foreign Quarter) has one entry with three reply options gated by the parent's state:
| Condition | Reply action | Meaning |
|---|---|---|
QUESTRESREF=q_newdistrict / QUESTSTATE=ABSENT |
q_startquestat with STARTQUEST=q_newdistrict |
Player accepts the writ |
QUESTRESREF=q_newdistrict / QUESTSTATE=READY |
q_finishquestat with QUESTRESREF=q_newdistrict |
All children done; player turns in the writ |
| (no condition) | (none) | "No, I don't think I will." |
READY means progress ≥ 100% and not yet marked finished. Because children contribute equal shares, the parent reaches READY only after every child is turned in.
District NPC dialogue (child tasks)
Each child task gets its own entry node with two reply options:
| Condition | Reply action | Meaning |
|---|---|---|
QUESTRESREF=<child> / QUESTSTATE=ABSENT |
q_startquestat with STARTQUEST=<child> |
Player accepts the task |
QUESTRESREF=<child> / QUESTSTATE=READY |
q_finishquestat with QUESTRESREF=<child> |
Task complete; player turns it in |
Prerequisites come from the database, not from dialogue conditions. The requiredquest column in gc_quests.quests chains the children sequentially (e.g. q_sewerkill.requiredquest = q_sewerscout). When EvaluateDatabaseBackedQuestCondition runs, it reads metadata.RequiredQuest and hides the option until that prerequisite is finished. This is why the NPC dialogues need no explicit QUESTCOMPLETE gates — the database handles ordering.
To gate a child on the parent being active (not just previously started), add QUESTINPROGRESS as an extra parameter alongside QUESTRESREF/QUESTSTATE:
Script: q_queststatusaw
QUESTRESREF: q_newdistrict_scout
QUESTSTATE: ABSENT
QUESTINPROGRESS: q_newdistrict
This ensures the first child only appears while the parent writ is still in progress, preventing a player who abandoned the parent from picking up orphaned children. Later children normally do not need this because their requiredquest chain implies the parent was started.
Optional overrides
LEVEL=n— override the level check for this specific option (otherwise uses the quest'slevelcolumn).SPEAKERTAG=tag— restrict the option to a particular speaker object.QUESTCOMPLETE=resref— override the database prerequisite with an explicit one.NOREWARD(onq_finishquestat) — suppress XP/gold/item rewards for the parent turn-in when children already reward the player.REMOVE=resref(onq_finishquestat) — destroy matching items from the player's inventory after turn-in.GIVEITEM1/GIVEITEM2(onq_startquestat) — grant optional items when the quest starts.
7. Add module resources
Confirm that:
- Every waypoint tag exists in the configured area.
- Every kill target uses the configured creature resref.
- Area identifiers match either the area's tag or stable/original resref.
- Required keys, triggers, doors, and custom objective objects exist.
- Custom scripts use valid C# script-handler signatures.
Creature tags do not replace creature resrefs in kill definitions. Kill routing currently matches NwCreature.ResRef case-insensitively.
8. Understand subscription behavior
Definitions are discovered once during module load, but handlers are not globally left running.
- Objectives are indexed by area during module load.
- Area entry examines only objectives indexed for that area.
- A player gets one district heartbeat only when an active waypoint or custom-heartbeat objective applies there.
- Multiple applicable heartbeat objectives share that one subscription.
- The heartbeat removes itself when no applicable objective remains.
- Kill and boss objectives do not use heartbeats.
- Parent progress recalculates only when a child is finished or when the parent is accepted after children were already completed.
9. Validate the chain
Before testing in game:
- Build the C# project.
- Check module-load logs for district-definition errors.
- Verify all database resrefs exactly match code and dialogue.
- Verify all waypoint tags and creature resrefs against module resources.
- Inspect every dialogue script and parameter.
In game, test this complete path:
- A player without the parent can accept it.
- The first child appears only while the parent is in progress.
- Each unique waypoint counts once, including after resting or restarting the server.
- Only configured creatures in configured areas grant kill progress.
- Repeated callbacks cannot grant duplicate progress.
- Each child can be turned in and becomes
finished. - Parent progress advances by one equal share per finished child.
- The next child becomes available after the previous child is finished.
- The final child unlocks the parent turn-in.
- Turning in the parent moves it to completed quests and grants only the intended reward.
Existing examples
The Foreign Quarter (q_fqsewers.cs, parent q_foreignquarter) demonstrates:
- Six-waypoint scouting.
- A multi-target kill objective (20 kills).
- A custom heartbeat objective for sewer blockages.
- A boss objective.
- Unique trigger and creature-heartbeat script handlers.
The Puddles (q_puddlesdistrict.cs, parent q_puddlesdistric) demonstrates:
- Five-waypoint scouting.
- Multiple sequential kill objectives across two areas.
- An objective-completion callback that drops a key item.
- A final boss objective.
The Upper Siphons (q_uppersiphons.cs, parent q_uppersiphons) demonstrates:
- Three-waypoint scouting.
- A multi-target kill objective (12 kills).
- A custom heartbeat objective for smuggler caches.
- A boss objective.
- A parent quest with a database prerequisite (
requiredquest = q_puddlesdistric), showing cross-district chaining.