Golarion Chronicles Wiki

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:

Shared implementation:

1. Plan the chain

Choose:

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:

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:

4. Select objective types

Waypoint objective

DistrictObjective.Waypoints(
    "q_newdistrict_scout",
    "newdistrict_main",
    "New District Scouted",
    "WaypointOne", "WaypointTwo", "WaypointThree")

The arguments are:

  1. Child quest resref.
  2. Area tag or resref.
  3. Floating progress-message text.
  4. 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:

  1. Child quest resref.
  2. Number of qualifying kills required.
  3. Area tag or resref.
  4. 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

7. Add module resources

Confirm that:

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.

9. Validate the chain

Before testing in game:

  1. Build the C# project.
  2. Check module-load logs for district-definition errors.
  3. Verify all database resrefs exactly match code and dialogue.
  4. Verify all waypoint tags and creature resrefs against module resources.
  5. Inspect every dialogue script and parameter.

In game, test this complete path:

  1. A player without the parent can accept it.
  2. The first child appears only while the parent is in progress.
  3. Each unique waypoint counts once, including after resting or restarting the server.
  4. Only configured creatures in configured areas grant kill progress.
  5. Repeated callbacks cannot grant duplicate progress.
  6. Each child can be turned in and becomes finished.
  7. Parent progress advances by one equal share per finished child.
  8. The next child becomes available after the previous child is finished.
  9. The final child unlocks the parent turn-in.
  10. 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:

The Puddles (q_puddlesdistrict.cs, parent q_puddlesdistric) demonstrates:

The Upper Siphons (q_uppersiphons.cs, parent q_uppersiphons) demonstrates: