Authoring Shops & Currencies

Wallets, storefronts, shelves, offers, and the files that write a whole family of offers at once

The economy is shared library content, so a wallet, a storefront or an offer authored for this mod works for any mod reading the same library, and one screen serves them all. Five asset types cooperate here, all with the file name as the id and all supporting "Parent": "<id>" reuse - and two more, boards and the contracts on them, are documented with quests instead, because a contract IS a quest: see Boards and contracts.

Content pack assetServer/ZiggfreedCommon/{Currencies,Shops,ShopPools,ShopEntries,ShopEntryGenerators}/<ns>/*.json

The folder under a type root is plain organization; the FILE NAME is the id, so two files of the same name in different folders are one id and the store reports the clash.

defaults<pack<owner
MyEconomyPack/
├── manifest.json
└── Server/ZiggfreedCommon/
    ├── Currencies/MyStudio/Bounty_Token.json
    ├── Shops/MyStudio/General.json          # a storefront
    ├── ShopPools/MyStudio/Featured.json     # a rotating shelf
    ├── ShopEntries/MyStudio/Boost_Mining.json
    └── ShopEntryGenerators/MyStudio/Xp_Packets.json

The shipped MMO Skill Bounty Pack is the real-world reference for this shape, laid out the same way under its own namespace. Zip your pack folder with the manifest at the root and drop the .zip in your mods directory - see Building the zip.

The owner's last word lives in mods/ziggfreedcommon/currencies.json, shops.json and shop-pools.json. Each one overrides by id, one leaf at a time ({ "general": { "Enabled": false } } closes a storefront), and an unknown id is simply a new entry. See ZiggfreedCommon.

The six old mod-side stores are retired

Server/MMOSkillTree/Currencies, Shops, ShopPools, ShopEntries, ShopTemplates and BountyBoards no longer load. They are read at boot only so a pack still shipping them can be named in the log and pointed at the folder above. The owner files mods/mmoskilltree/currencies.json, token-shop.json and bounty-board.json are renamed .legacy at boot with one warning. The web Migration Converter rewrites a pack that still ships them.

The storefront

Server/ZiggfreedCommon/Shops/<ns>/<Id>.json. The storefront is the page: what it is called, which wallets show in its header, which shelves it has and in what order.

FieldTypeDefaultDescription
Text{TitleKey, FlavorKey, TextArgs}optionalWhat the storefront is called. Display text is keys the player's own client resolves.
Iconstring (item id)none
Orderint0Sort order among storefronts, and the default-storefront tiebreak.
Currenciesstring[] (wallet ids)noneThe balances shown in the header while the player is browsing.
CategoryOrderstring[]alphabeticalThe sequence the shelves appear in. A category left off follows the named ones alphabetically, and an offer's own sort order sorts only within one shelf.
Categories{category: {TitleKey, ...}}library defaultsWhat each SHELF is called, keyed by the category word, merging per category under Parent - see Shelf words.
RequiresgroupungatedWho may open the storefront at all. The shared Requires block.
Where{Match, GameplayConfig, ExcludeMatch}every worldWhich worlds this storefront applies to.
EnabledbooltrueFalse closes it.
* required
Server/ZiggfreedCommon/Shops/MMOSkillTree/General.json
{
  "Text": { "TitleKey": "shop.general.title", "FlavorKey": "shop.general.desc" },
  "Icon": "Ore_Iron",
  "Order": 0,
  "Currencies": ["Bounty_Token", "Life_Essence"],
  "CategoryOrder": ["items", "boosts", "conversion", "featured"]
}

The rotating shelf

Server/ZiggfreedCommon/ShopPools/<ns>/<Id>.json. A shelf that re-draws its offers on a cadence, instead of listing everything it holds. Its rotation vocabulary is the same one a board uses, so a shelf and a board can never drift apart about what a cadence means.

FieldTypeDefaultDescription
TextgroupoptionalWhat the shelf is called.
Shopstring (storefront id)the default storefrontWhich storefront this shelf belongs to.
Orderint0
Rotation{Period, Every, OffsetMinutes, Weekday}noneWhen it re-draws: "Period": "Daily" or "Weekly" for a calendar boundary, or "Every": { "Hours": 2 } for a rolling interval. Authoring both is a validation error rather than a silent precedence rule.
Selection{Type, Seed}none"Type": "Weighted_Random" is the shipped strategy; the draw is deterministic within a period.
Slots[{Tier, Count, Optional}][]Tier is a free content word matched case-insensitively against an offer's membership; Count defaults to 1; an Optional slot is skipped silently when it cannot be filled.
Reroll{Cost, MaxPerPeriod}no paid rerollCost is the shared price object.
Enabledbooltrue
* required
Server/ZiggfreedCommon/ShopPools/MMOSkillTree/XpExchange.json
{
  "Text": { "TitleKey": "shop.xpexchange.featured" },
  "Shop": "XpExchange",
  "Order": 0,
  "Rotation": { "Period": "Daily" },
  "Selection": { "Type": "Weighted_Random" },
  "Slots": [ { "Tier": "lesser" }, { "Tier": "greater" }, { "Tier": "master" } ],
  "Reroll": { "Cost": { "Currencies": { "Bounty_Token": 30 } }, "MaxPerPeriod": 2 }
}

The offer

Server/ZiggfreedCommon/ShopEntries/<ns>/<Id>.json. One purchasable thing: what it costs, what it pays out, where it sits, and who may buy it.

FieldTypeDefaultDescription
Text{TitleKey, FlavorKey, TextArgs}optionalWhat the offer is called. One written line can serve a whole family through TextArgs.
Iconstring (item id)resolvedAn offer paying XP into one skill resolves that skill's own icon when none is authored.
Shopstring (storefront id)the default storefrontWhich storefront lists it.
Listing{Category, SortOrder, Chains}optionalWhich shelf it sits on and where. Chains makes a family of near-identical offers draw as ONE climbing entry.
Cost*{Currencies, Items, Combine}-The shared price object. There is no scalar shorthand.
Limits{Daily, Total}unlimitedPer-player purchase caps, tracked server-side.
RequiresgroupungatedThe shared Requires block - the same gate a quest carries, with no shop-only vocabulary.
Rewardsreward[][]What the purchase pays out, in the shared reward vocabulary. A plain LIST here, not the Auto / Claim group progression content uses: a purchase is handed over at the counter, so there is no completion to wait for. A purchase owed to an offline player waits for them rather than vanishing.
Pool{Id, Tier, Weight}staticEnrols the offer in a rotating shelf: which shelf, which slot word it fills, and how often it comes up. Without it the offer is always listed.
Parentstring (offer id)noneStart from another offer and author only what changes. Every leaf inherits individually, so a child can retune a price and keep the payout.
AbstractboolfalseMarks a skeleton that exists only to be a Parent. It is the one field that never inherits.
Enabledbooltrue
* required
Server/ZiggfreedCommon/ShopEntries/MMOSkillTree/Boost_Mining.json
{
  "Text": { "TitleKey": "shop.boost_mining.title", "FlavorKey": "shop.boost_mining.desc" },
  "Icon": "Tool_Pickaxe_Crude",
  "Shop": "General",
  "Listing": { "Category": "boosts", "SortOrder": 20 },
  "Cost": { "Currencies": { "Bounty_Token": 150 } },
  "Limits": { "Daily": 3 },
  "Requires": {
    "Factors": [ { "Factor": "hytale:stat", "Param": "MMO_Level_MINING", "Min": 1 } ]
  },
  "Rewards": [
    { "Kind": "Mmo_Boost_Token",
      "Params": { "Skill": "MINING", "Multiplier": "3.0", "DurationMinutes": "20" } }
  ]
}

An id is what a player's purchase count is filed under

Renaming an offer - including by widening a generator's id pattern - starts that count over, so a daily limit a player has already spent comes back.

Offer generators: one file writes a family

Server/ZiggfreedCommon/ShopEntryGenerators/<ns>/<Id>.json writes a whole family of offers from one table. It loads after the offers themselves, and it merges nothing: it writes ordinary child bodies carrying Parent and lets the same decode do the rest.

FieldTypeDefaultDescription
Base*string (offer id)-The offer every child inherits, so the shape lives in one ordinary (usually Abstract) offer file.
IdPattern*string-Names each child, with the axis tokens substituted.
ForEach*[{Token, Values} or {Token, Source}]-The axes. Values is a table of rows written out; Source draws from a registered axis, such as the live skill roster.
Child*offer body-An offer body with {token} placeholders substituted anywhere - every string value, every object KEY, and the id pattern.
* required
Server/ZiggfreedCommon/ShopEntryGenerators/MMOSkillTree/Xp_Packets.json (abridged)
{
  "Base": "Xp_Packet",
  "IdPattern": "shop_xp_{tier}_{skill}",
  "ForEach": [
    { "Token": "skill", "Source": "mmoskilltree:skills" },
    { "Token": "tier", "Values": [
        { "tier": "lesser",  "tokens": 75,  "essence": 30, "xp": "1500",  "minLevel": 1,  "order": 40, "daily": 3 },
        { "tier": "greater", "tokens": 165, "essence": 65, "xp": "7500",  "minLevel": 30, "order": 42, "daily": 3 }
    ] }
  ],
  "Child": {
    "Text": { "TitleKey": "shop.xp_packet.{tier}.title", "TextArgs": { "Title": ["{skill}"] } },
    "Listing": { "Category": "conversion", "SortOrder": "{order}",
                 "Chains": [ { "Id": "{skill}", "Tier": 1 } ] },
    "Cost": { "Currencies": { "bounty_token": "{tokens}", "life_essence": "{essence}" } },
    "Limits": { "Daily": "{daily}" },
    "Pool": { "Id": "XpExchange", "Tier": "{tier}" },
    "Requires": { "Factors": [
        { "Factor": "hytale:stat", "Param": "MMO_Level_{skill}", "Min": "{minLevel}" } ] },
    "Rewards": [ { "Kind": "Mmo_Xp", "Params": { "Skill": "{skill}", "Amount": "{xp}" } } ]
  }
}
  • Substituting a KEY is load-bearing, not a nicety: it is how a per-skill requirement names its own stat channel (MMO_Level_{skill}) with no escape hatch on the schema.
  • A row value keeps its own TYPE, so write it the way the field it fills wants it: an XP amount is quoted because a reward's parameters are text, while a price is not because a price is a number. Get it the wrong way round and the whole family fails to decode and is reported.
  • A token nothing binds is an error and that one offer is skipped, rather than shipping something wrong.

Shelf words and band words

A shelf says something too, and the STOREFRONT names it: Categories is keyed by the category's own word and merges per category under Parent. It is deliberately separate from CategoryOrder - one decides what a shelf says, the other where it sits - and it lives on the storefront rather than on each offer, because a dozen offers in one category would otherwise be a dozen chances to name that shelf differently.

The word a screen shows is resolved in this order: the storefront's own Categories entry, then the key shop.category.<id> from a mod that ships it, then the library's own default, then the raw category word. The library ships a word for the common shelves (items, boosts, conversion, featured) in 9 languages, so a storefront using those needs nothing. Boards work the same way with Grades and board.grade.<id>.

The wallet

Server/ZiggfreedCommon/Currencies/<ns>/<Id>.json. The one real choice is what backs it, and nothing else changes with that choice.

FieldTypeDefaultDescription
Backing{Item}a number this server keepsAuthor an item and the balance IS an inventory count - carried, tradable, and subject to whatever the world already does with items on death. Leave it out for a counter. Nothing that spends a wallet branches on which kind it is.
Iconstring (item id)the backing itemThe item whose picture stands for the wallet. An item-backed wallet authors none at all.
Colorstring (hex)white
Caplong0 (uncapped)
OnDeath{LossPercent}no lossA FRACTION between 0 and 1: 0.1 takes a tenth. Anything outside the range is clamped and reported, because a 10 meaning "ten per cent" would otherwise wipe a wallet.
Decay{PerDayPercent}no decayA fraction as above, compounding over offline days.
Requiresgroupalways visibleGates VISIBILITY, never earning: it decides whether a player is shown the balance at all. A balance nobody can see still has to be correct when it is finally revealed.
Textgroupcurrency.<id>.nameThe wallet's name. Omit it and the convention key is used; an item-backed wallet falls through to the backing item's own name.
Meta.mmoskilltree{ShowOnSidebar, ShowOnMasteryPage, XpConversionPercent}optionalThis mod's own knobs: whether the balance rides the sidebar, whether it shows on the mastery page, and what share of earned XP converts into it. Nothing else interprets them, which is what lets one wallet file load on a server running only one of the two mods that authored it.
Enabledbooltrue
* required
Server/ZiggfreedCommon/Currencies/MMOSkillTree/Bounty_Token.json
{
  "Icon": "Ingredient_Bar_Gold",
  "Color": "#ffcc44",
  "Cap": 0,
  "Meta": { "mmoskilltree": { "ShowOnSidebar": true,
                              "ShowOnMasteryPage": false,
                              "XpConversionPercent": 0 } }
}
An item-backed wallet, in full
{ "Backing": { "Item": "Ingredient_Life_Essence" }, "Color": "#a7e0a7" }

A wallet nothing defines does not exist

Everything priced in an unknown wallet is unaffordable rather than free - the safe direction - and an unknown currency id is a reported warning at every price site rather than a silence.

A pack shipping a wallet ships two lang keys, not one: currency.<id>.name for the name itself, and the counted key a reward chip reads ("+{0} Bounty Tokens"), composed from that same translated name. A wallet with only the name key still renders on a reward chip, just as the raw key until the counted one is shipped too.

Checking your work

/zigcommerce validate runs the whole economy audit - wallets, storefronts, shelves, offers, boards, contracts - and /mmoshop validate and /mmobounty validate are the aliases over it. It reports a price in a wallet nobody defines, an offer on a storefront nobody ships, a shelf that can never be drawn, a slot word no offer fills, a gate naming a factor nothing answers, and a reward naming a kind nothing pays out. Unknown means warning (some mod may supply it later); impossible means an error.

The catalogue itself ships in the standalone MMO Skill Bounty Pack, so both the shop and the boards are dormant on a server with no economy content installed. For the player-facing side, see Token Shop, Currencies and Bounty Board.