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.
Server/ZiggfreedCommon/{Currencies,Shops,ShopPools,ShopEntries,ShopEntryGenerators}/<ns>/*.jsonThe 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.
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.jsonThe 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.
| Field | Type | Default | Description |
|---|---|---|---|
Text | {TitleKey, FlavorKey, TextArgs} | optional | What the storefront is called. Display text is keys the player's own client resolves. |
Icon | string (item id) | none | |
Order | int | 0 | Sort order among storefronts, and the default-storefront tiebreak. |
Currencies | string[] (wallet ids) | none | The balances shown in the header while the player is browsing. |
CategoryOrder | string[] | alphabetical | The 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 defaults | What each SHELF is called, keyed by the category word, merging per category under Parent - see Shelf words. |
Requires | group | ungated | Who may open the storefront at all. The shared Requires block. |
Where | {Match, GameplayConfig, ExcludeMatch} | every world | Which worlds this storefront applies to. |
Enabled | bool | true | False closes it. |
{
"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.
| Field | Type | Default | Description |
|---|---|---|---|
Text | group | optional | What the shelf is called. |
Shop | string (storefront id) | the default storefront | Which storefront this shelf belongs to. |
Order | int | 0 | |
Rotation | {Period, Every, OffsetMinutes, Weekday} | none | When 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 reroll | Cost is the shared price object. |
Enabled | bool | true |
{
"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.
| Field | Type | Default | Description |
|---|---|---|---|
Text | {TitleKey, FlavorKey, TextArgs} | optional | What the offer is called. One written line can serve a whole family through TextArgs. |
Icon | string (item id) | resolved | An offer paying XP into one skill resolves that skill's own icon when none is authored. |
Shop | string (storefront id) | the default storefront | Which storefront lists it. |
Listing | {Category, SortOrder, Chains} | optional | Which 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} | unlimited | Per-player purchase caps, tracked server-side. |
Requires | group | ungated | The shared Requires block - the same gate a quest carries, with no shop-only vocabulary. |
Rewards | reward[] | [] | 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} | static | Enrols 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. |
Parent | string (offer id) | none | Start from another offer and author only what changes. Every leaf inherits individually, so a child can retune a price and keep the payout. |
Abstract | bool | false | Marks a skeleton that exists only to be a Parent. It is the one field that never inherits. |
Enabled | bool | true |
{
"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
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.
| Field | Type | Default | Description |
|---|---|---|---|
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. |
{
"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.
| Field | Type | Default | Description |
|---|---|---|---|
Backing | {Item} | a number this server keeps | Author 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. |
Icon | string (item id) | the backing item | The item whose picture stands for the wallet. An item-backed wallet authors none at all. |
Color | string (hex) | white | |
Cap | long | 0 (uncapped) | |
OnDeath | {LossPercent} | no loss | A 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 decay | A fraction as above, compounding over offline days. |
Requires | group | always visible | Gates 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. |
Text | group | currency.<id>.name | The 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} | optional | This 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. |
Enabled | bool | true |
{
"Icon": "Ingredient_Bar_Gold",
"Color": "#ffcc44",
"Cap": 0,
"Meta": { "mmoskilltree": { "ShowOnSidebar": true,
"ShowOnMasteryPage": false,
"XpConversionPercent": 0 } }
}{ "Backing": { "Item": "Ingredient_Life_Essence" }, "Color": "#a7e0a7" }A wallet nothing defines does not exist
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.