Skip to content

Unlock Costs

unlock_costs.yml defines 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 existing admin content.

The file name comes from unlock.cost_file in config.yml (default unlock_costs.yml), and whether purchasing is offered 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]
FieldTypeDescription
tiers[].count_rangestringThe "which slot number" range this tier covers, such as 1-9. Earlier tiers take priority.
tiers[].currency.typeenumCurrency backend: vault / excellent.
tiers[].currency.amountnumber or stringThe price. A string is evaluated by CoreLib's ExpressionEngine and may use %count%.
tiers[].item.sourcestringCoreLib ItemSource token.
tiers[].item.amountintRequired item count.
fallback.currencysectionPrice for every slot beyond all tiers.
fallback.max_amountnumberPer-slot price cap. Required.
batch.enabledbooleanWhether batch purchase is available. When false, only the single-slot option is offered.
batch.optionslistBatch purchase options.

Per-Slot Pricing

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.

In expressions, %count% is the ordinal of the slot about to be bought.

The max_amount Guard Rail

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.

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: the admin's formula produced a price outside their own declared guard rail, so the purchase must not proceed.

Payment Ordering and Reverse Compensation

Purchase flow: quote, fire the cancellable StorageUnlockEvent, take currency, take items, then increase purchasedSlots.

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.

Failure is fail-closed: when no tier matches and no usable fallback exists, the purchase is refused rather than treated as free.

  • Configuration: unlock.purchase_enabled and unlock.cost_file.
  • Capacity: where purchasedSlots sits in the four-source capacity model.
  • GUI and Display: the slot configuration of the unlock button.
  • Event API: the fields and cancellation semantics of StorageUnlockEvent.