Authoring Achievements
The shared achievement file, categories and point milestones, and the older shape
Achievements share a schema with quests: the same file layout, the same Text / Listing / Requires / Rewards groups, the same Parent inheritance, the same objective shape. What is different is what an achievement adds - scoring, a keyed map of criteria, and a capstone over a set. For the sub-schemas both share, see Shared Schemas.
The shared achievement file
Server/ZiggfreedCommon/Achievements/<Namespace>/<Area>/<Name>.jsonAchievements/No Payload envelope and no Id field: the file IS the achievement, and its id is the filename, lower-cased. A folder whose name starts with an underscore prefixes the id; every other folder is organization only.
Prefer a content pack: author one file per achievement under Server/ZiggfreedCommon/Achievements/ inside a pack folder, zip it, and drop the .zip into your server Mods/ folder. Building the zip.
{
"Text": { "TitleKey": "achievement.wilds_hunter_t1.title",
"FlavorKey": "achievement.wilds_hunter.desc",
"TextArgs": { "Flavor": ["@amount"] } },
"Listing": { "Category": "combat", "Subcategory": "melee", "SortOrder": 80,
"Chains": [ { "Id": "wilds_hunter", "Tier": 1 } ] },
"Scoring": { "Points": 10 },
"Criteria": {
"kill-entity": { "Kind": "KILL_ENTITY", "Target": "", "MatchMode": "CONTAINS",
"Zone": "Emerald_Wilds", "Amount": 50 }
}
}| Field | Type | Default | Description |
|---|---|---|---|
Text | group | optional | TitleKey, FlavorKey, DisplayName (an untranslated fallback only), TextArgs. One written line can serve a whole ladder: write @amount in TextArgs and each rung supplies its own number. |
Listing | group | optional | Category, Subcategory, SortOrder, Icon, Tags, Chains, plus two visibility leaves: Hidden keeps it out of browse listings until it is earned, and RequirePrerequisites keeps it out until its Requires block passes. |
Scoring | group | optional | Points (default 10) and CountsTowardTotal (default true) - see Scoring. |
Requires | group | ungated | The shared gate, honoured exactly as it is on a quest: an achievement whose Requires block does not pass makes no progress. See The one Requires block. |
Criteria | map of criterion id to criterion | optional | A MAP, not an array: every criterion must be done, and the key is what that criterion's progress is filed under. Same shape as a quest objective, including STAT_THRESHOLD. See Objectives and criteria. |
MetaChildren | string[] | [] | Achievement ids that must all be earned, for a capstone over a set. An achievement with these needs no Criteria of its own. |
Rewards | group of two lists | none | Auto lands the instant it is earned; Claim (the default) waits in the menu to be collected, which is where anything needing bag room belongs. See the two buckets. |
Meta | map of namespace to extras | optional | Per-namespace knobs - see Meta.mmoskilltree. |
Enabled | bool | true | False takes it out of circulation. |
Abstract | bool | false | Marks a file that exists only to be a Parent. It never carries down. |
A criterion's key is its identity
kill-entity, complete-quest, the way the shipped files do. Criteria also merges per key under Parent: a child can retune one criterion by name and inherit the rest untouched.Scoring
Scoring.Points (default 10) is what this achievement adds to a player's lifetime total. CountsTowardTotal is the one does-not-count switch, and it is honoured exactly as authored: write false for something nobody can earn any more, so a long-standing player's total stays comparable with a newcomer's. Nothing else flips it behind your back. A feat of strength (Meta.mmoskilltree.Feat) is a listing treatment - its own earned-only shelf, unpinnable, out of sight until somebody does it - so an author who wants a feat kept out of the points total writes the two together, which is what every shipped feat does:
"Scoring": { "Points": 0, "CountsTowardTotal": false },
"Meta": { "mmoskilltree": { "Feat": true } }Meta.mmoskilltree
A key nothing here claims is reported by name at boot and the rest of the block still loads, so a typo costs one knob rather than the whole file.
| Field | Type | Default | Description |
|---|---|---|---|
ServerFirst | bool | false | Only the first player on the server may earn it. Everybody else keeps their progress and is told, once, that they were beaten. Use it sparingly, for a handful of genuinely notable milestones. |
Feat | bool | false | A feat of strength: celebrated, kept off the list until earned, given its own earned-only shelf and never pinnable. It is a LISTING treatment and nothing more - to keep it out of the points total, author Scoring.CountsTowardTotal: false beside it. |
Feature | string[] | [] | Server feature ids that must ALL be on for it to exist here. See Feature gating. |
Class | string | null | References the unreleased Class System - do not author it on shipped pack content yet. |
LegacySince | string | null | The version it stopped being earnable in, shown on the listing so a player can tell a retired one from one they have not got to. |
Announce | {TitleKey, BodyKey} | no broadcast | The server-wide banner. Authoring TitleKey is what turns it on; both lines are translation keys so everybody reads them in their own language (a key no language file ships is shown as written, so a one-server line can be the sentence itself). A TitleKey template takes the skill name and the number asked for; a BodyKey takes who earned it and the achievement's own title. How the banner is drawn is authored once for every achievement, in the Achievement_Unlocked feedback moment; this group only says what this one achievement announces. |
PerSkill | {IdPattern, Criterion} | none | The two stencils - see below. |
The two per-skill stencils. A server's skill roster is not known when a file is written, so the two shapes that have to cover "every skill" are written once and stamped out against the live roster. PerSkill.IdPattern gives ONE ACHIEVEMENT per skill ("{skill}_10" yields mining_10, woodcutting_10, ...); PerSkill.Criterion gives ONE CRITERION per skill inside a single achievement, which is the shape of "do it in every skill". In any authored text, {skill} stands for a skill's id and {skillName} for its readable name.
Reuse and generators
A file may name another as "Parent": "<id>" and inherit every field it does not mention; leaves and groups merge per leaf and Criteria merges per criterion key, while Rewards.Auto, Rewards.Claim, Listing.Tags and Listing.Chains are each one leaf that replaces whole. Mark a base "Abstract": true so it never appears as content in its own right. The full rules, and the matching generator file, are on the quests page - they are identical for both stores.
Categories and point milestones as assets
Both are ordinary shared-schema content, so a pack declares its own category or its own points ladder and a server owner still outranks it.
A category
Server/ZiggfreedCommon/AchievementCategories/<Namespace>/<Category>.json is the presentation half of an achievement's Listing.Category leaf. The filename IS the category word content writes, lower-cased, so Combat.json is the category combat.
{ "Order": 40,
"Icon": "Tool_Fishing_Rod",
"TitleKey": "achievement.category.sailing",
"Subcategories": ["harbour", "deep_water"] }| Field | Type | Default | Description |
|---|---|---|---|
Order | int | 0 | Sort key among categories. Leave gaps so a new category slots in without renumbering. |
Icon | string (item id) | none | The fallback shown for an achievement in this category that authored none of its own. |
TitleKey | string | achievement.category.<id> | The heading localization key. Omit it and the convention key is used. |
Subcategories | string[] | [] | The reading order inside the category. One left off still shows, it just sorts after the named ones. |
A point milestone
Server/ZiggfreedCommon/AchievementMilestones/<Namespace>/<Name>.json is one rung of the achievement-points ladder. Threshold is the milestone's identity, not the filename, so a file re-authoring 3000 replaces the shipped 3000 rung outright.
{ "Threshold": 3000,
"TitleKey": "achievement.milestone.3000.title",
"Rewards": {
"Auto": [ { "Kind": "Mmo_Xp", "Params": { "Skill": "ALL", "Amount": "1500" } } ],
"Claim": [ { "Kind": "Item", "Params": { "Item": "Ingredient_Bar_Iron", "Count": "5" } } ]
} }| Field | Type | Default | Description |
|---|---|---|---|
Threshold* | int | - | The point total that fires this rung, and the milestone identity. |
TitleKey / DescriptionKey | string | none | What the rung reads as on the achievements page. |
Rewards.Auto | reward[] | [] | Lands the instant the total is crossed, wherever the player is. |
Rewards.Claim | reward[] | [] | Waits on the achievements page, which is where anything needing bag room belongs. |
Both layer
Parent. The five built-in point milestones ship this way, and the sanctioned owner override is still mods/mmoskilltree/achievements/point-milestones.json.Server-first claims
Meta.mmoskilltree.ServerFirst enables a first-come-first-served race claim: the first player to unlock the achievement gets a permanent, server-wide first-claim record, surfaced in the achievement UI, and every later unlocker gets the normal achievement without the claim. When somebody who was racing loses the claim, what they are told is an authored feedback moment, so a server owner can re-word it or broadcast it.
The older shape (still read)
Everything written before 1.6.0 still loads: Server/MMOSkillTree/Achievements/<id>.json, the AchievementTemplates extends / params DSL, and a server owner's own mods/mmoskilltree/achievements/ files all go through a compat reader that registers them into the same engine. Author anything new in the shared shape above.
{
"Name": "First_Kill",
"Payload": {
"id": "first_kill",
"triggerType": "KILL_ENTITY",
"target": "*",
"requiredAmount": 1,
"category": "combat",
"points": 10,
"autoClaimRewards": [ { "type": "XP", "skill": "SWORDS", "amount": 100 } ]
}
}Top level: triggerType, not type
triggerType only - type is not read there at all. Inside criteria[] both keys work as aliases. The shared shape has neither problem: a criterion's key is Kind, everywhere.| Field | Type | Default | Description |
|---|---|---|---|
id* | string | - | The runtime id. Rejects the reserved delimiters. |
triggerType / target / requiredAmount / matchMode / qualifier / zone | - | optional | The single-criterion form. Present only when no criteria array is authored. |
criteria | criterion[] | [] | When present, the top-level trigger fields are ignored entirely. |
metaChildren | string[] | [] | Presence alone makes this a capstone over a set and skips trigger parsing. |
titleKey / descriptionKey | string | null | Localization keys; the raw displayName and description literals still parse as fallbacks. |
category / subcategory / sortOrder / hidden / enabled | - | optional | The same jobs as the Listing group and the Enabled leaf above. |
points | int | 10 | The same job as Scoring.Points above. |
serverFirst / featOfStrength / legacySince / classRequired | - | optional | The same jobs as the Meta.mmoskilltree knobs above, with one difference: on this older shape featOfStrength still forces countsTowardTotal false on its own. |
chainId / tier | string, int | optional | The same job as one Listing.Chains membership above. |
requiresFeatures | string[] (or bare string) | [] | The same job as Meta.mmoskilltree.Feature above. |
announcementCommand | string | null | Retired in 1.6.0: a file still carrying it gets one warning at start naming the replacement (a Command group on the Achievement_Unlocked moment file, or a Variants entry there scoped to this achievement) and the leaf is otherwise ignored. |
autoClaimRewards / manualClaimRewards | reward[] | [] | The older shape's spelling of the Auto and Claim buckets, in that order. |
See also
- Shared Schemas - objectives, rewards, and the one Requires block.
- Authoring Quests & Bounties - the matching quest half, Parent inheritance and generators in full.
- Achievements - the player-facing UI, pinning, and progress guide.
- Feedback - the authored moment behind the unlock toast and the server-first notice.