English
Unlock Costs
unlock_costs.yml holds the price tiers for in-GUI paid expansion. It is a standalone business data file: release_default_data releases it only when the file is missing, and a version upgrade never overwrites content you have already edited
The file name comes from unlock.cost_file in config.yml (default unlock_costs.yml), and whether players may purchase at all depends on unlock.purchase_enabled together with the emakistorage.unlock.purchase permission — see Configuration
unlock_costs.yml Structure
yaml
tiers:
- count_range: "1-9"
currency:
type: vault
amount: 1000
- count_range: "10-19"
item:
source: "diamond"
amount: 16
- count_range: "20-49"
currency:
type: vault
amount: 50000
item:
source: "netherite_ingot"
amount: 4
fallback:
currency:
type: vault
amount: "100000 * 1.05 ^ (%count% - 50)"
max_amount: 1000000000
batch:
enabled: true
options: [1, 5, 10, 64]| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
tiers | list<map> | No | see example (3 tiers) | each element uses the key set below | The price tier list. A tier with an invalid count_range or with neither price kind is skipped entirely with a warning |
tiers[].count_range | string | Yes | — (a tier without it is skipped) | "N" or "min-max"; requires min ≥ 1 and max ≥ min | The "which slot number" range this tier covers, such as 1-9. Earlier tiers take priority |
tiers[].currency | section | No | present on some shipped tiers | see the three rows below | The currency cost. At least one of currency and item must be present, or the tier is skipped |
tiers[].currency.type | enum | No | vault | vault / excellent | Currency backend id. An unknown value falls back to vault |
tiers[].currency.currency | string | No | "" (empty) | The backend's own currency identifier | The currency id handed to the economy backend; empty means the backend's default currency. The shipped file does not write this key |
tiers[].currency.amount | number or string | Yes (when currency is written) | — | A non-negative number, or a string the expression engine can evaluate | The price. A number is used as-is; a string is evaluated by CoreLib's ExpressionEngine and may use %count%. When it is neither a number nor a non-blank string the currency block counts as unconfigured |
tiers[].item | section | No | present on some shipped tiers | see the two rows below | The item cost |
tiers[].item.source | string | Yes (when item is written) | — | CoreLib ItemSource token | When it is missing or blank the item block counts as unconfigured |
tiers[].item.amount | int | No | 1 (when absent or not a number) | > 0; <= 0 makes the item block count as unconfigured | Required item count |
fallback | section | No | see example | see the three rows below | Fallback tier: the price of every slot beyond all tiers |
fallback.currency | section | No | see example | same key set as tiers[].currency | The fallback tier's currency cost |
fallback.item | section | No | none | same key set as tiers[].item | The fallback tier's item cost. The fallback block is inert when both price kinds are absent |
fallback.max_amount | number | Yes (to use the fallback tier) | — | > 0 | Per-slot price cap. When it is missing or non-positive the fallback tier is disabled entirely and slots beyond tiers cannot be bought |
batch | section | No | enabled: true, options: [1, 5, 10, 64] | see the two rows below | Batch purchase options |
batch.enabled | boolean | No | true | true / false | When false one click always buys exactly 1 slot |
batch.options | list<int> | No | [1, 5, 10, 64] | Only positive integer elements; an empty result falls back to the default | The slot counts one click may buy. The current GUI only reads the first entry: clicking the unlock button always buys options[0] slots, and the remaining options have no entry point inside this module — they are kept as reserved capability |
Per-Slot Pricing
Understand the pricing model first: a batch purchase is settled by pricing each slot and summing, not by unit price times count. When a player buys slot N, the count_range that N falls into decides that slot's price; N starts at purchasedSlots + 1 and increments. Charging the current unit price times the count would let a player buy a large batch at the cheapest tier and lock in that price for slots that belong in far more expensive tiers. The shipped batch.options: [1, 5, 10, 64] combined with a GUI that only reads the first entry means an actual order is normally a single slot, so the per-slot sum degenerates into a single quote; raise options[0] to buy several slots at once and the per-slot accumulation becomes visible
In expressions, %count% stands for "the slot being bought this time". When no tier matches, fallback is used
For example, coins for slots 1–5, diamonds for everything after, buying 10 slots at a time
yaml
tiers:
- count_range: "1-5"
currency:
type: vault
currency: "coin"
amount: 500
fallback:
item:
source: "diamond"
amount: 2
max_amount: 100000
batch:
enabled: true
options: [10]The max_amount Guard Rail
Note: fallback.max_amount is required. When it is missing or non-positive the fallback tier is disabled entirely and slots beyond tiers cannot be bought (reported as no_price_defined). Its purpose is to stop an exponential expression from overflowing into Infinity
Scope check: the guard rail only applies to the fallback tier. Slots matched by tiers are not limited by max_amount and are only constrained to a finite, non-negative amount (violations report price_overflow)
When a slot's computed price exceeds max_amount, the implementation explicitly refuses the quote (price_over_cap) instead of silently clamping to the cap: your own formula produced a price outside the guard rail you declared, so the purchase must not proceed
Purchase Entry Point and Pre-checks
There is exactly one purchase entry point: the unlock button in the GUI. Each click is checked in this order:
unlock.purchase_enabledistrue, otherwise the click reports the feature as disabled- The player holds
emakistorage.unlock.purchase, otherwise it reports no permission unlock_costs.ymlprovides a usable tier or a usable fallback, otherwiseno_price_defined- The purchase will not push past
capacity.max_slots(effectiveSlots + slots > max_slotsreportsmax_slots_reached)
The order size comes from the first entry of batch.options, see the batch.options row above
Payment Ordering and Reverse Compensation
One purchase runs in this order: quote, fire the cancellable StorageUnlockEvent, take currency, take items, then increase purchasedSlots
Nothing over-charges: any failure after a successful charge is compensated in reverse order, applied item removal plans are rolled back last-to-first, then the currency is refunded, and only then does the call return a failure. purchasedSlots is never touched on the compensation path
Failures are fail-closed: when no tier matches and no usable fallback exists, the purchase is refused, never treated as free
Related Pages
- Configuration:
unlock.purchase_enabledandunlock.cost_file - Capacity: where
purchasedSlotssits in the four-source capacity model - GUI and Display: the slot configuration of the
unlockbutton - Event API: the fields and cancellation semantics of
StorageUnlockEvent