Template Extension

Collapse repetitive content into reusable skeletons with a shared extends / params / overrides DSL

Why templates

When a pack ships many near-identical entries - the same milestone reward on every skill, a family of near-identical daily quests - a template captures the shared shape once and each concrete entry fills in only what differs. The Mastery Pack's mastery-point milestone rewards collapsed from 2,386 lines to 6 (measured from its shipped MasteryPointMilestones template).

This DSL covers the older raw-payload types only

Everything in the shared library - quests, achievements, conversations, wallets, storefronts, shelves, offers, boards, contracts - reuses content through native "Parent": "<id>" inheritance instead, plus a generator file where a whole family is written from one table. Mastery tracks work the same way: an Abstract base named as Parent, and a whole per-skill family from one MasteryGenerators file. See Reuse on the shared types.

The DSL

Every resolver runs the same pipeline:

  1. Deep-clone the template payload (the cached template is never mutated).
  2. {{paramName}} substitution - every string value is walked and {{key}} tokens are replaced with the matching params value. An empty param drops the holding key entirely, so an entry can opt out of an optional template field by passing "".
  3. Field overlay - every top-level field on the concrete entry except the reserved keys (extends, params, the overrides/extras keys) wins over the template.
  4. Overrides - for each id in the overrides map, find the matching entry in the primary array (by post-substitution id) and deep-merge: object keys merge recursively; primitives and arrays replace wholesale.
  5. Extras - append wholly new entries. A new id must NOT collide with a template id (use overrides to modify existing entries).
A missing param surfaces as a literal {{key}} in the resolved JSON (never silently dropped), so content typos are visible during validation. Unknown template ids log a warning and the entry is dropped. Template id lookup is case-insensitive.

Per-type field names

TypeTemplate storeOverridesExtrasArray
CommandRewardCommandRewardTemplates/levelOverridesextraLevelslevel map
Quest (older shape only)QuestTemplates/objectiveOverridesextraObjectivesobjectives
Achievement (older shape only)AchievementTemplates/criterionOverridesextraCriteriacriteria

The Quest and Achievement rows serve the older Server/MMOSkillTree/Quests and .../Achievements shape, which the mod still loads (a quest converted into the shared shape at every start, an achievement through its compat reader). A shared-schema quest or achievement reuses through native Parent plus a generator file, not this DSL.

ShopTemplates, MasteryTemplates and ClassTemplates are retired

Storefronts, shelves and offers moved into the shared library, where an offer reuses another by naming it as a Parent and a whole family of offers is written from one Server/ZiggfreedCommon/ShopEntryGenerators/ file; the old Server/MMOSkillTree/ShopTemplates/ store, its extends/params resolver and the forEachSkill fan-out flag are gone - see Offer generators. Mastery tracks (and the dormant Class System) left the DSL the same way: a track is a structured file whose shared skeleton is an Abstract base named as Parent, and a whole per-skill family is one MasteryGenerators file. A pack still shipping MasteryTemplates/ or ClassTemplates/ files gets a start-up warning naming each one, and nothing is folded; the Migration Converter and /mmomigrate both point at the replacement.

CommandReward templates & {{ALL_SKILLS}}

A CommandReward template is a level → rewards map (the shape a single skill block carries). A CommandRewards payload then references it per skill - or uses the {{ALL_SKILLS}} sentinel as a top-level key to fan the template out to every known skill that isn't otherwise explicitly listed:

CommandRewardTemplates/MasteryPointMilestones.json
{
  "Name": "MasteryPointMilestones",
  "Payload": {
    "15": [ { "command": "/mmocurrency give --player={player} --currency=mastery_point --amount=1" } ],
    "30": [ { "command": "/mmocurrency give --player={player} --currency=mastery_point --amount=1" } ]
  }
}

// CommandRewards/MyPack.json - fan to every skill in six lines
{
  "Name": "MyPack",
  "Payload": { "{{ALL_SKILLS}}": { "extends": "mastery_point_milestones" } }
}

The sentinel only fans to skill keys - the special TOTAL and GLOBAL_SKILL keys are never touched, and an explicit per-skill entry always wins over the fan-out. Per-skill blocks can also use levelOverrides (replace a level's rewards) and extraLevels (add new levels).

Quest & achievement templates (older shape)

json
{
  "Name": "Pack_Daily_Kill_Goblin_T1",
  "Payload": {
    "extends": "daily_kill_template",
    "params": { "id": "pack_daily_kill_goblin_t1", "displayName": "Goblin Hunter I",
                "mobId": "Mob_Goblin_T1", "count": "10", "reward": "5" },
    "objectiveOverrides": { "kill_target": { "displayText": "Slay 10 Goblins" } }
  }
}

The easiest way to replace a template's rewards is to supply a fresh rewards: [ … ] at the top level (it overlays the whole array).

Class System - unreleased

Classes are a future, unreleased feature - the plugin ships no built-in classes and no class content is live. When they ship, a class is a structured Server/MMOSkillTree/Classes/ file reusing through native Parent, the same paradigm as mastery tracks; the old ClassTemplates/ store is retired alongside MasteryTemplates/.

Conversations don't use this DSL either

Dialogue reuse is native "Parent": "<id>" inheritance (a per-screen keyed merge), not extends/params. A reusable base is an ordinary conversation file marked "Abstract": true. See Dialogue authoring.
Remember to enable the template store in your Control file - add the matching "<Type>Templates": "add" key (e.g. "QuestTemplates": "add") so the templates load before the content that extends them. Then zip the pack folder with the manifest at the root and drop the .zip in your mods directory - see Building the zip.