Supported NPC Types

Any entity in the world can be a quest giver in QuestLines. Two NPC types are supported:

  • HyCitizens NPCs — managed by the HyCitizens plugin. QuestLines listens to the CitizenInteractEvent to intercept interactions. Each HyCitizens NPC has a unique citizen ID (e.g. f48f7eaa-512e-415d-bfdb-f53d2ea0b991) which is used as the key in npcs.json.
  • Standard Hytale NPCs — any non-HyCitizens entity. QuestLines intercepts interactions via the SyncInteractionChains packet watcher. The entity's UUID is used as the key in npcs.json.

Both NPC types use the same npcs.json mapping format and identical quest logic. You don't need to configure anything in HyCitizens or elsewhere — all the linkage lives in QuestLines.

Finding an NPC Key

Use /ql wand <questId> and punch the NPC. Wand mode reads the citizen ID (for HyCitizens NPCs) or entity UUID (for standard NPCs) automatically and stores it in npcs.json. For HyCitizens NPCs you can also look up citizen IDs via /citizens.

HyCitizens Overview

HyCitizens is the NPC framework for Hytale servers. It manages NPC spawning, appearance, and interaction events. QuestLines listens to the CitizenInteractEvent emitted by HyCitizens to intercept player interactions and open the appropriate dialogue.

Each NPC in HyCitizens has a unique citizen ID (e.g. f48f7eaa-512e-415d-bfdb-f53d2ea0b991). This ID is used as the key in QuestLines' npcs.json to map that NPC to one or more quests. You don't need to configure anything in HyCitizens itself — all the linkage lives in QuestLines.

Linking Quests to an NPC

There are two methods to link a quest to an NPC:

Method 1 — Wand Mode (Recommended)

  1. Enter wand mode.

    Run /ql wand <questId> in chat. You'll receive a confirmation that wand mode is active.

  2. Punch the target NPC.

    Left-click (punch) the HyCitizens NPC in the world. QuestLines reads the citizen's ID and adds it to npcs.json automatically.

  3. Wand mode ends.

    The assignment is saved immediately. Run /ql wandcancel at any time to abort without making changes.

Method 2 — Editor UI

Open the editor with /ql ui and navigate to the NPC tab. From there you can manually type the citizen id in.

npcs.json Format

The npcs.json file is the source of truth for NPC-to-quest mappings. It has two top-level maps: Npcs for direct quest assignments and RandomGroups for random quest pools.

// npcs.json
{
  "Npcs": {
    "citizen_42": "lost_sword,daily_reward",
    "citizen_07": "goblin_king"
  },
  "RandomGroups": {
    "citizen_10": "questA|questB;questC|questD"
  }
}

Multiple Quests on One NPC

The value in Npcs is a comma-separated list of quest IDs. When a player interacts with that NPC, QuestLines evaluates the quests in this order — first quest first. The page resolution algorithm stops as soon as a matching page is found, across all quests and their pages.

Quest Order Matters for Multi-Quest NPCs

When an NPC has multiple quests, the evaluation order is: Quest 1 pages (in order), then Quest 2 pages (in order), and so on. If Quest 1 has a catch-all page with no requirements, it will always match and Quest 2 will never be reached. Put more specific quests earlier in the comma-separated list, or ensure every quest's final fallback page has requirements that exclude other quest states.

Random Quest Pools

The RandomGroups assigns quests randomly per player. This is used to create variety — different players get different bounties, tasks, or story paths from the same NPC.

Format

"questA|questB;questC|questD"
  • Semicolon (;) separates independent pools. One quest is chosen per pool.
  • Pipe (|) separates quest options within a pool. One is selected at random.

In the example above, the player is assigned one quest from pool 1 (either questA or questB) and one from pool 2 (either questC or questD), for a total of two assigned quests.

Assignment Tags

When a player is assigned a random quest, QuestLines stores the result as a tag:

// Tag format for random assignment
"assigned_quest:questId:citizenId"

// Example: player was assigned questA from citizen_10
"assigned_quest:questA:citizen_10"

Use hasTag:assigned_quest:questA:citizen_10 in page requirements to show quest-variant-specific content. See Quest Patterns → Random Quest Pool for a full worked example.

💡
Re-Rolling Assignments

Assignment tags persist until explicitly removed. To allow a player to receive a new random assignment (e.g. daily bounties), add removeTag:assigned_quest:questId:citizenId to the quest completion actions. The next interaction will trigger a new random selection.

Wand Mode

Wand mode is the fastest way to link quests to NPCs in-world without editing JSON directly.

CommandDescription
/ql wand <questId> Enter wand mode for the specified quest. Persists until you punch an NPC or cancel.
/ql wandcancel Cancel the active wand mode without making any changes.

While wand mode is active, a pending assignment is stored in memory keyed to your player UUID. Punching any NPC (HyCitizens or standard) will:

  1. Read the NPC's citizen ID.
  2. Append the quest ID to that NPC's entry in Npcs.
  3. Save npcs.json to disk.
  4. Exit wand mode and confirm the link in chat.
Wand Mode is Per-Player

Multiple admins can be in wand mode simultaneously without interfering with each other. Each pending assignment is stored against the admin's own UUID.

Talk Tracking

QuestLines automatically tracks how many times each player has interacted with each NPC. No setup or tracking tag is required — the counter increments on every interaction, before the page resolution algorithm runs.

Use the talk:citizenName:qty requirement to gate content on visit count. Use the NPC's display name — the same name shown above their head (e.g. Elder Maren):

// This page only shows after the player's third visit
"Requirements": ["talk:Elder Maren:3"]

// A "returning visitor" page for visits 2+
"Requirements": ["talk:Elder Maren:2"]

Text Variables

Variables can be embedded in any Dialog or response Text field. They are substituted at render time with live player data.

VariableDescription
{username} The player's username.
{npcname} The Name field of the current page (the NPC's display name).
{questname} The title of the first quest currently in the player's started quests list.
{track:id} The player's current count for the named tracker with that ID.
{cooldown:key:totalSeconds} Remaining cooldown time as a human-readable string (e.g. "4 hours 23 minutes"). Renders as "ready" when the cooldown has elapsed.
{timeleft:key:totalSeconds} Remaining time on a countdown timer as a human-readable string. Renders as "expired" when time is up.
{variable:name} The current value of a named numeric variable. Whole numbers are shown without a decimal point (e.g. 3 not 3.0). Defaults to 0 if never set.
{balance} The player's current economy balance. Requires VaultUnlocked with a compatible economy provider. Shows 0 if unavailable.
{distancefrom:x:y:z} The Euclidean distance from the player's current position to (x, y, z), rounded to one decimal place.
{mmo:skillId} The player's current level in the named MMO skill (e.g. {mmo:MINING}). Returns 0 if MMOSkillTree is not active. Case-insensitive.
{rpglevel} The player's current RPG level. Returns 0 if RPGLeveling is not active.
{rpgxp} The player's current total RPG XP. Returns 0 if RPGLeveling is not active.
{rpgxpneeded} XP needed for the player's next RPG level. Returns 0 if RPGLeveling is not active or the player is at max level.
{catchprogress:fishItemId} The player's fish catch count by item ID. Requires HyFishing and tracking:catchfish:fishItemId tag to be active.
{catchrarityprogress:rarity} The player's fish catch count by rarity. Requires HyFishing and tracking:catchfishrarity:rarity tag to be active.
{catchbiomeprogress:biomeName} The player's fish catch count by biome. Requires HyFishing and tracking:catchfishbiome:biomeName tag to be active.
{catchzoneprogress:zoneName} The player's fish catch count by zone. Requires HyFishing and tracking:catchfishzone:zoneName tag to be active.
{releaseprogress:fishItemId} The player's fish release count by item ID. Requires HyFishing and tracking:releasefish:fishItemId tag to be active.
{fishbagcount:fishItemId} Live count of fish currently in the player's bag by item ID. Use * for a total count of all fish. Requires HyFishing.
{pickupprogress:itemId} The player's pickup count for the specified item ID. Requires tracking:pickup:itemId tag to be active.
{global:variable:name} The current value of a server-wide global variable. Defaults to 0 if never set.
{regioncount} The number of OrbisGuard regions the player is currently inside. Returns 0 if OrbisGuard is not active.
{regionnames} Comma-separated list of OrbisGuard region IDs the player is currently inside. Returns an empty string if OrbisGuard is not active or the player is in no regions.

Example Usage

// In a kill quest progress page
"Dialog": "You've slain {track:goblin_kills} of 10 goblins. Keep going, {username}!"

// In a cooldown waiting page
"Dialog": "You've already claimed your reward. Come back in {cooldown:daily_reward:86400}."

// In a timed quest active page
"Dialog": "Hurry! You have {timeleft:courier_timer:300} to make the delivery."

// Showing a named variable
"Dialog": "Your score is {variable:score}. Reach 5 to complete the challenge!"

Inline Text Formatting

Dialog and response text supports inline rich-text formatting tags. These are processed by QuestLines' TextFormatter before the text is rendered in the UI.

TagEffectExample
{b}...{/} Bold text {b}important{/}
{i}...{/} Italic text {i}emphasis{/}
{m}...{/} Monospace text {m}code-like{/}
{#RRGGBB}...{/} Colored text (hex color) {#FF4444}danger{/}

Example

"Dialog": "The {b}Sunstone Amulet{/} glows with a {#FFD700}golden light{/}. Bring it to me, {username}."

"Text": "{i}I've already lost one today — I won't risk another.{/}"

Quick Setup Checklist

When creating a new quest and linking it to an NPC for the first time, run through this checklist:

  1. Create the quest file with a title and ordered page IDs via /ql create or manually.
  2. Create all page files. Ensure the most specific requirements come first in the Pages list.
  3. Run /ql wand <questId> and punch the NPC in-world to link it.
  4. Verify the NPC entry in /ql ui shows the quest ID correctly.
  5. Test the full quest flow by interacting with the NPC as a player. Check all stages.
  6. Confirm tracking tags are added on quest start and removed on quest complete.
  7. Run /ql reload if you edited JSON files directly on disk.
💡
Test With a Fresh Player Profile

To test the full quest lifecycle from scratch, create a test account with no quest progress. Alternatively, use the Player tab in /ql ui to manually clear quest state and tags for your own account during development.