Quests

Configure the quest system

Overview

A quest is a task for players to complete: gather resources, reach a level, kill something, deliver something. Quests are written as JSON, loaded at startup, and hot-reloadable with /mmoquestadmin reload. Players interact with them through the Quests tab, a character in the world, or /quest.

The current format is one file per quest, in the shared format quests, achievements and bounties all speak, and your own quests live in mods/ziggfreedcommon/quests/, the shared library's folder, one file per quest named after the quest id. The older "a file holding a quests array" shape under mods/mmoskilltree/quests/ is migrated into that folder once, on the first start after upgrading, and never read again - see The older shape below for what the first start does and how the old fields map across.

Content pack assetServer/ZiggfreedCommon/Quests/<Namespace>/<Feature>/<Name>.json

One file per quest, id = filename. The mod's own quests ship this way in the jar.

Override configmods/ziggfreedcommon/quests/<Id>.json

The server owner's folder, read by the shared library. One quest per file in the same shape a pack uses; a file named after a quest the jar or a pack ships merges leaf by leaf over it, and a new name is a quest of your own.

defaults<pack<owner

A quest ships as an installable add-on by living in a content pack: a folder you zip and drop into Mods/ - see Building the zip. Inside it, quests go at Server/ZiggfreedCommon/Quests/<YourPack>/...; see the quest pack authoring reference for the full field set and worked bounty examples. For a single server, mods/ziggfreedcommon/quests/ is the quick-tweak alternative to a pack.

The quest file

There is no envelope and no id field: the file is the quest, and its id is its filename, lower-cased. Trork_Hunter.json is the quest trork_hunter. Folders under the type root are yours to arrange and change nothing about the id, so keep basenames unique across the whole store; two files landing on one id are reported at load, naming both.

An id is a database key

A player's progress is filed under the quest id. Renaming a file starts that quest over for everybody who is part-way through it. Pick it as carefully as you would pick a database key.

Every worked example on this page carries a "Name" leaf, but that field is a human-readable label kept in the file for readability - it sets nothing. The id is still the filename, and a Name that disagrees with the filename changes nothing about which quest it is.

// Server/ZiggfreedCommon/Quests/MyPack/Mining/Novice_Miner.json
{
  "Name": "Novice_Miner",
  "Text":     { "TitleKey": "quest.novice_miner.title",
                "FlavorKey": "quest.novice_miner.flavor" },
  "Listing":  { "Category": "main", "SortOrder": 10 },
  "Flow":     { "AutoAccept": true },
  "Requires": { "Quests": ["getting_started"] },
  "Npc":      { "ViewId": "Mmo_Hub" },
  "Objectives": {
    "mine_stone": { "Kind": "BREAK_BLOCK", "Target": "Rock", "Amount": 150,
                    "MatchMode": "CONTAINS" }
  },
  "Rewards": {
    "Claim": [
      { "Kind": "Item",   "Params": { "Item": "Tool_Pickaxe_Iron", "Count": "1" } },
      { "Kind": "Mmo_Xp", "Params": { "Skill": "MINING", "Amount": "200" } }
    ]
  },
  "Meta": { "mmoskilltree": { "NpcOnly": false } }
}

The same shape from the owner folder, retuning one shipped quest by naming it: every leaf you leave out keeps the shipped value, so this file changes one amount and nothing else.

// mods/ziggfreedcommon/quests/Novice_Miner.json
{
  "Objectives": {
    "mine_stone": { "Amount": 300 }
  }
}

The groups, and where each is documented

The schema itself has ONE home: the quest authoring reference (with the shared sub-schemas on Shared Schemas). The same file you author there is the file you drop in mods/ziggfreedcommon/quests/, so nothing on those pages is pack-only. In brief, with each group linked to its reference:

  • The groups - Text, Listing, Flow, Npc, Objectives, Rewards, Meta: every group on a quest file.
  • Listing - the category and sort order, plus three independent visibility switches: Hidden (off every open list), RequirePrerequisites (listed once it can be taken) and ShowWhen (listed once a requirement block of its own passes, whatever is needed to take it).
  • Requires - the one gate: factor bounds (a skill level is a hytale:stat bound), finished quests, permission, and the AllOf/AnyOf/Not combinators.
  • Repeat - what brings a quest back around: calendar windows of any length (a day, a week, a fortnight, eight hours), rolling cooldowns, allowances and lifetime caps, and the one rule that decides whether a quest reads as daily or weekly.
  • Meta.mmoskilltree - the one knob specific to this mod, NpcOnly. A feature a quest needs is a mmoskilltree:feature factor in Requires.
  • Quest generators - one file that writes a whole family of quests.

Switching one quest off

To take a shipped quest out of circulation, put a file named after it in mods/ziggfreedcommon/quests/ carrying the flag and nothing else. A disabled quest needs no objectives, and every other leaf stays as shipped:

// mods/ziggfreedcommon/quests/Forgot_Something.json
{ "Enabled": false }

A player who already holds the quest can still finish it. An older-shape file carrying { "id": "forgot_something", "enabled": false } becomes exactly this file on the first start.

What the validator reports

The startup audit and /mmoconfig validate check every quest file, owner and pack alike. Each finding the quest validator raises, and what it means, is documented with the authoring reference on Authoring Quests & Bounties: validation.

Default Quests & Customization

Every quest the mod ships is a readable asset in the jar under Server/ZiggfreedCommon/Quests/MMOSkillTree/, so you can open a built-in, copy it, and override it.

  • Override one by name. Put a file named after the quest in mods/ziggfreedcommon/quests/: the leaves you write win for that quest and the rest stay as shipped.
  • Your own files no longer hide the built-ins. Adding a quest of your own adds to the shipped set instead of replacing it.
  • The generated defaults copy is retired. The old quests/_defaults/default-quests.json echo renames itself to .legacy on the first start after upgrading, with one line in the log saying where to read the built-ins instead. It used to be regenerated every start and then read straight back in as the owner layer, which meant a stale copy could quietly outrank the real quest.
  • Upgrading from much older versions: an example-quests.json is moved to quests/backups/ automatically.

After editing quest files, run /mmoquestadmin reload to apply changes without restarting the server.

The older shape (migrated on the first start)

A file in mods/mmoskilltree/quests/ holding a schemaVersion and a quests array is the older owner shape. It is not read any more: on the first start after upgrading, every quest in every such file (sub-folders included) is rewritten in the shared shape into mods/ziggfreedcommon/quests/<Id>.json, one file per quest named after the quest id, and the original is renamed .legacy beside itself. One startup notice lists every file that moved, every quest it became, and every field the shared shape has no home for. A second start finds nothing to do.

  • A file moves whole or not at all. A quest the converter refuses (a template no pack provides, a quest with nothing to do, a body the shared codec rejects) or a target file that already exists (a quest you already wrote in the new folder, or an earlier file on the same start) leaves its file exactly where it is, named in the notice with the reason, so you decide which copy wins. Fix it and restart.
  • A file already in the shared shape is moved across untouched, name and all.
  • The .legacy leftovers are never read. Keep them until you have checked the new folder, then delete them. /mmomigrate lists each one as a file the server already handled, and a not-yet-converted quests/ file as one it will handle on the next start.
  • The docs site's Migration Converter performs the same rewrite on a zip of your mods/mmoskilltree/ folder, so either route leaves the same files.

The rest of this section is the old shape as it was written, and what each field became, so a .legacy file and the startup notice can be read side by side.

File Structure

{
  "schemaVersion": 1,
  "quests": [
    {
      "id": "mining_beginner",
      "titleKey": "quest.mining_beginner.title",
      "flavorKey": "quest.mining_beginner.flavor",
      "category": "main",
      "enabled": true,
      "sortOrder": 1,
      "autoAccept": true,
      "autoClaimRewards": true,
      "prerequisites": [],
      "requirements": { "minLevel": 0, "minSkill": "MINING", "minSkillLevel": 5 },
      "objectives": [
        {
          "id": "mine_stone",
          "type": "BREAK_BLOCK",
          "target": "Rock",
          "amount": 150,
          "matchMode": "CONTAINS"
        }
      ],
      "rewards": [
        { "type": "XP", "skill": "MINING", "amount": 200 }
      ]
    }
  ]
}

The folder was scanned recursively, so quests could be split across files; each quest becomes its own file in the new folder whatever file it came from. A titleKey/flavorKey is carried as Text.TitleKey/FlavorKey, and the convention keys quest.<id>.title / .flavor are written when the file named none - see Localization.

Quest Fields

FieldTypeDefaultDescription
idStringrequiredUnique quest identifier. Cannot contain | = : , #
titleKeyStringnullLocalization key for the title. Convention: quest.<id>.title
flavorKeyStringnullLocalization key for the flavor text. Convention: quest.<id>.flavor
titleArgs / flavorArgsString[][]Ordered args substituted into those keys (e.g. @amount, @skill, or a literal)
displayNameStringidUntranslated fallback. It is honored where it is authored, sitting between the convention key and the sentence inferred from the first objective, but prefer titleKey on anything new
descriptionString""Dropped in the conversion, with a note: the flavor line reads from the quest.<id>.flavor key
categoryString"misc"Category for grouping (e.g., main, daily, misc)
enabledBooleantrueWhether the quest is offered. The id plus this flag is the whole off-switch
sortOrderInteger0Sort position within category (lower = first)
tagsString[][]Tags for filtering and organization
variablesObject{}Dropped in the conversion, with a note: the shared shape has no home for it
* required

Behavior Fields

FieldTypeDefaultDescription
autoAcceptBooleanfalseStart the quest automatically when a matching event fires
sequentialBooleanfalseObjectives must be completed in order (top to bottom)
repeatable / cooldownSecondsBoolean / Longfalse / 0The older two-scalar repeat form: repeatable, and how long before it can be taken again. Both reach the engine as the same rolling wait the Repeat group writes
autoClaimRewardsBooleanfalsetrue files this file's rewards into the Auto bucket, so they land the moment the objectives finish. Omitted or false parks the quest at COMPLETED_UNCLAIMED and the player claims it themselves, which is what the Claim bucket does on the current shape. A board bounty always behaves as false whatever this says - bounty rewards are only ever collected at the board
requiresFeaturesString or String[][]Server feature ids the quest needs (e.g. mastery, abilities, currency). Becomes a top-level Requires factor on mmoskilltree:feature, which hides the quest entirely when the feature is off
requiredAchievementsString[][]Achievement ids that must be unlocked
requiresSkillsObject{}A {SKILL: level} map. The map form of the array requirements shorthand below
turnInNpcIdStringnullThe literal value "giver" appends a report-back step at the quest's own giver. Any other value routes the hand-in to a different character. Skipped if you already wrote a TURN_IN objective
completionDialogueStringnullThe conversation that follows the quest settling at a character
autoTrackBooleanfalsePin the quest to the on-screen tracker the moment it is accepted
resetsOnCompleteString[][]Quest ids fully reset when this one is claimed, so they become re-acceptable immediately. Unknown ids and self-references are ignored with a warning, and chains do not cascade
permissionStringnullA permission node required to accept the quest. Means the same as Requires.Permission
npcViewIdStringnullThe character who offers this quest. When set, the quest is hidden from the quest log until accepted and appears on that character's own tab
npcDisplayNameStringnullDropped in the conversion, with a note: a placed character reads its name from its own role
npcRequiredToAcceptBooleanfalseThe quest can only be accepted in person, not from the quest log
incompleteMarkdown / activeMarkdown / completeMarkdownStringnullWhat the character says before, during, and after. Become Text.Lore (Incomplete, Active, Complete), read only when no quest.<id>.md.<state> key ships
* required

Requirements (older shorthand)

requirements takes a single object or an array of objects, and every entry can set a total-level gate, a skill gate, or both. The highest minLevel across the entries is the total-level gate, and every skill requirement must be met:

"requirements": [
  { "minLevel": 25 },
  { "minSkill": "MINING", "minSkillLevel": 15 },
  { "minSkill": "CRAFTING", "minSkillLevel": 20 }
]
FieldTypeDefaultDescription
minLevelIntegeroptionalMinimum total level across all skills
minSkillStringoptionalWhich skill the level gate is about
minSkillLevelIntegeroptionalMinimum level in that skill
* required

These are level bounds by another name

requirements, requiresSkills and minLevel all reach the engine as ordinary level bounds in the Requires vocabulary - minLevel as a bound on total level, a skill entry as a bound on that skill's own level. permission folds in the same way, and requiresFeatures becomes a mmoskilltree:feature factor. All of them are read together with any Requires block on the same quest, and all of them must pass.

Prerequisites

A plain array of quest ids means all of them must be finished and their rewards collected:

"prerequisites": ["mining_beginner", "combat_initiate"]

The object form is now written in the Requires vocabulary. Where an older file wrote mode and groups, write the leaves and combinators instead - AnyOf is the OR:

"prerequisites": {
  "Quests": ["chapter1_intro"],
  "AnyOf": [
    { "Quests": ["path_warrior_complete"] },
    { "Quests": ["path_mage_complete"] }
  ]
}

The player must complete chapter1_intro and either path. Keys are read case-insensitively, so quests and Quests are the same leaf. The quest UI labels an either-or as "Unlocked by: A or B", and a permission requirement as its own line.

The old prerequisite leaves are gone

mode, groups, and the leaves playtimeSeconds, kills, deaths, achievementPoints, currencyBalances and lifetimeCurrencySpent were removed in 1.6.0. Their replacements are factor bounds: an achievement point total is ziggfreedcommon:achievement_points, and a companion mod's own reading is its own factor id. skillLevels, totalLevel and minCombatLevel are level bounds on the channels in the table above.

Tags & Variables

tags categorize a quest (they render as chips under the description and drive the filters on the docs site Quest Gallery), and variables was a free key-value map for your own metadata. Tags become Listing.Tags (and Listing.Chains is the leaf for saying a quest is one rung of a questline); variables has no home in the shared shape and is dropped with a note.

{
  "id": "dragon_slayer",
  "titleKey": "quest.dragon_slayer.title",
  "tags": ["epic", "combat", "chapter-2"],
  "variables": { "difficulty": "hard", "region": "mountains" }
}

Visibility

The optional visibility object controls whether a quest appears in the quest log before it can be taken. By default every quest is visible. Once a quest is started it is always visible, whatever this says.

FieldTypeDefaultDescription
hiddenBooleanfalseMaster switch. When true, the quest is hidden until the conditions below are met
permissionStringnullOnly visible to players holding this permission
requirePrerequisitesBooleanfalseOnly visible once the prerequisite quests are done
requireLevelBooleanfalseOnly visible once the level requirements pass
* required

All the conditions set must pass for the quest to show. On the current shape the same answers live on Listing, and the conversion writes them there: hidden alone, or with requirePrerequisites, becomes RequirePrerequisites; hidden with requireLevel becomes a ShowWhen block carrying the quest's level floors; hidden with permission becomes ShowWhen with that node. A permission or requireLevel on a quest that was never hidden gated nothing, and is dropped with a note saying so.

Objectives

A quest step is the same objective shape every content kind shares: a Kind, a Target matched case-insensitively under a MatchMode, an optional Qualifier and Zone, and an Amount. The field-by-field reference, the one matching rule, and the full objective-kind roster live on Shared Schemas: objectives and criteria; the quest-file authoring page walks the same fields in place on Authoring Quests & Bounties.

Rewards

A quest pays through the one shared reward model: an open vocabulary of kinds under an Auto bucket (paid the instant the quest settles) and a Claim bucket (parked until collected, the default). The kind roster, the two buckets, writing a reward kind of your own, and the offline spool are documented once on Shared Schemas: rewards.

Quest States

StateDescription
NOT_STARTEDNot accepted
ACTIVEIn progress, objectives being tracked
COMPLETED_UNCLAIMEDAll objectives done, reward not yet collected
COMPLETEDFinished and the reward collected. This is the state a prerequisite means
ON_COOLDOWNRepeatable quest waiting for its next run

Quest notices

Every quest notice, a progress tick, an objective finishing, the quest itself completing, parking, or being claimed, is its own editable file under Server/ZiggfreedCommon/FeedbackMoments/ (Quest_Objective_Progressed, Quest_Completed, Quest_Parked, Quest_Claimed). Reword, recolour, silence, or add a console broadcast to any of them by dropping a same-named file into a pack. See Authored feedback moments.

Admin Commands

/mmoquestadmin requires OP or mmoskilltree.admin. Except reload and list, it is an alias over the shared library's /zigprogress quest verbs (and resetdialogues over /zigprogress memory forget): same names, same arguments, same wording, and every verb speaks to the one quest engine the library owns, so a script may use either spelling. Two behaviours worth knowing: give settles a step the player already satisfies the moment the quest is taken on (a quest given to somebody past its finish line pays or parks at once), and reset wipes a record for a quest id the catalogue no longer knows instead of refusing it.

CommandDescription
/mmoquestadmin reloadRe-lay the older pack quest layer, then republish the shared quest store, owner folder included (the same republish /zigprogress reload does)
/mmoquestadmin list [--args=<category>]List this mod's quests, optionally filtered by category. The merged catalogue from every mod is /zigprogress quest list
/mmoquestadmin give --args=<player>|<questId>Give a quest to a player (use * for everybody)
/mmoquestadmin reset --args=<player>|<questId>Reset a quest for a player (use all for every quest). A reset wipes the completion record too
/mmoquestadmin resetdialogues --args=<player>Forget everything every conversation remembers about a player, so first-visit beats play again (the same thing as /zigprogress memory forget)
/mmoquestadmin complete --args=<player>|<questId>Force-complete a quest and pay its rewards, running the same completion hooks an ordinary finish runs. For a player who is offline the reward spools until they next connect (the shared verb needs them online)
/mmoquestadmin status --args=<player>[|<questId>]Quest states and objective progress for a player

The library's own spelling of the same verbs is named-arg throughout: /zigprogress quest give --quest=<id> --player=<name> (or --everyone), reset --quest=<id|all> --player=<name>, complete --quest=<id> --player=<name>, status [--quest=<id>] [--player=<name>], list [--tag=<tag>], plus /zigprogress reload and /zigprogress memory forget --player=<name>. Those carry the library's own permission nodes rather than mmoskilltree.admin; see ZiggfreedCommon commands.

Player Commands

/quest is the player-side alias over /zigprogress quest accept|claim|abandon|status, under this mod's own mmoskilltree.command.quest node. It adds two bits of MMO policy: a quest authored as handed over only in conversation is refused from chat, and a claim checks for inventory room first so a full bag reads as "make room" rather than a refused button.

CommandDescription
/quest accept <questId>Accept a quest (checks every requirement)
/quest claim <questId|all>Claim rewards for completed quests (checks inventory room first)
/quest abandon <questId>Abandon an active quest and reset its progress
/quest status [questId]Your active quests and objective progress

Examples

A daily quest

Once a day on the calendar, in UTC, taken manually:

// Server/ZiggfreedCommon/Quests/MyPack/Daily/Daily_Lumber.json
{
  "Name": "Daily_Lumber",
  "Text":    { "TitleKey": "quest.daily_lumber.title",
               "FlavorKey": "quest.daily_lumber.flavor" },
  "Listing": { "Category": "daily", "SortOrder": 100 },
  "Repeat":  { "Reset": { "Period": "Daily" } },
  "Objectives": {
    "chop_logs": { "Kind": "BREAK_BLOCK", "Target": "Wood", "Amount": 30 }
  },
  "Rewards": {
    "Claim": [
      { "Kind": "Item", "Params": { "Item": "XpToken_Woodcutting_Fragment", "Count": "2" } }
    ]
  }
}

A weekly that re-arms its dailies

{
  "Name": "Weekly_Master",
  "Text":    { "TitleKey": "quest.weekly_master.title" },
  "Listing": { "Category": "main" },
  "Repeat":  {
    "Reset": { "Period": "Weekly", "Weekday": "Monday" },
    "ResetsOnComplete": ["daily_lumber", "daily_mining", "daily_combat"]
  },
  "Objectives": {
    "kill_boss": { "Kind": "KILL_ENTITY", "Target": "Trork_Chieftain", "Amount": 1,
                   "MatchMode": "EXACT" }
  },
  "Rewards": {
    "Auto": [
      { "Kind": "Mmo_Xp", "Params": { "Skill": "SWORDS", "Amount": "50000" } }
    ]
  }
}

A quest behind either of two routes

{
  "Name": "Chapter2_Start",
  "Text":     { "TitleKey": "quest.chapter2_start.title" },
  "Listing":  { "Category": "main", "SortOrder": 10, "Tags": ["chapter-2"] },
  "Requires": {
    "Quests": ["chapter1_intro"],
    "AnyOf": [
      { "Quests": ["path_warrior_complete"] },
      { "Quests": ["path_mage_complete"] }
    ]
  },
  "Objectives": {
    "talk_elder": { "Kind": "TALK_TO_NPC", "Target": "Elder_Sages", "Amount": 1 }
  },
  "Rewards": {
    "Claim": [
      { "Kind": "Mmo_Boost_Token",
        "Params": { "Skill": "ALL", "Multiplier": "2.0", "DurationMinutes": "60" } }
    ]
  }
}

A delivery quest handed in at a character

The hand-in step removes the items from the player's bag when they deliver. Partial delivery is supported, so a player can hand over what they have and come back with the rest. Because the quest names a giver and asks for something to be delivered, that character offers a hand-in; a quest whose only step is talking to somebody does not, since nothing changes hands.

{
  "Name": "Blacksmith_Delivery",
  "Text":     { "TitleKey": "quest.blacksmith_delivery.title",
                "FlavorKey": "quest.blacksmith_delivery.flavor" },
  "Listing":  { "Category": "main" },
  "Npc":      { "ViewId": "Blacksmith_01" },
  "CompletionDialogue": "Blacksmith_Thanks",
  "Objectives": {
    "deliver_iron": { "Kind": "TURN_IN", "Target": "Ore_Iron", "Amount": 10,
                      "MatchMode": "EXACT" }
  },
  "Rewards": {
    "Claim": [
      { "Kind": "Mmo_Xp", "Params": { "Skill": "MINING", "Amount": "500" } }
    ]
  },
  "Meta": { "mmoskilltree": { "NpcOnly": true } }
}

The character themselves is a placement file, and their conversation is a dialogue file - see Placed NPCs and NPC Dialogue & Quest Givers. If you would rather drive characters through an external dialogue mod such as HyCitizens, point one of its buttons at /mmoquestui <player> <npc_id> to open the same quest panel.