Skip to content

JSON Shop Components ECS EN

Bogdan edited this page Aug 5, 2026 · 1 revision

JSON: Shop Components (ECS)

In SDM Shop 2, a shop and each offer are assembled from components. Components can be combined: multiple costs, multiple conditions, multiple rewards, promos, visual settings, and script logic.

Basic offer shape:

{
  "uuid": "a94d5c7c-efb0-4c7e-a527-9e11b609151d",
  "components": [
    {
      "type": "sdm:name",
      "name": "Epic Sword"
    }
  ]
}

If uuid is omitted, it is generated automatically. For stable limits and admin commands, it is better to set UUIDs explicitly.

How a Purchase Is Processed

  1. All ConditionComponent entries are checked.
  2. Limits are checked for the requested purchase amount.
  3. A cost group group_id is selected.
  4. Active promos are collected and promo effects are applied by priority.
  5. The cost is charged.
  6. Rewards are granted.
  7. Cooldowns/limits are updated and the UI is synced.

If at least one condition fails, the purchase is cancelled before any money is charged.

Conditions

Conditions decide whether a purchase is available. If an offer has sdm:hide_render, the offer is hidden in the UI whenever at least one condition fails.

Purchase Limit: sdm:condition_limiter

Limits the number of purchases. The limiter is now a full ConditionComponent, so it works both as server-side purchase protection and as a condition for sdm:hide_render.

{
  "type": "sdm:condition_limiter",
  "limiter_type": "Player",
  "count": 5,
  "reset_interval_ms": 86400000,
  "limit_key": "daily_player_limit"
}
Parameter Type Required Description
limiter_type string Yes Player — separate counter per player. World — shared counter for the server/world.
count int Yes Maximum purchases before blocking. Minimum is 1.
reset_interval_ms long No Auto-reset interval in milliseconds. 0 disables automatic reset.
limit_key string No Stable storage key for this limiter. Needed when one offer has multiple limiters of the same type or when you do not want history to be lost after reordering components.

Important:

  • You can attach multiple limiters to one offer, for example World + Player.
  • A purchase succeeds only when all limiters pass.
  • UI cards and the purchase modal use the minimum available value across all limiters.
  • If reset_interval_ms > 0, an offer hidden by sdm:hide_render returns to the UI automatically after the limit resets.
  • Without limit_key, the first limiter of an old type uses the offer UUID for compatibility, and additional limiters receive a computed key based on order. For new complex offers, explicitly set limit_key.

Purchase Cooldown: sdm:condition_cooldown

Prevents repeat purchases until the timer expires after a successful purchase.

{
  "type": "sdm:condition_cooldown",
  "limiter_type": "Player",
  "cooldown_ms": 3600000
}
Parameter Type Required Description
cooldown_ms long Yes Time to wait before the next purchase, in milliseconds.
limiter_type string No Player or World. Defaults to Player.

Cooldown stores the last purchase time in limiter storage. A separate sdm:condition_limiter is not required for it. If the offer is hidden with sdm:hide_render, the UI returns the card automatically when the cooldown expires.

Script Condition: sdm:condition_script

The check runs on the server through a KubeJS/CraftTweaker/Java listener.

{
  "type": "sdm:condition_script",
  "script_id": "has_stage_vip"
}
Parameter Type Required Description
script_id string No Check ID used by your script to select the logic to run.

Script conditions are not checked on the client. The UI requests the result from the server and can hide the offer through sdm:hide_render.

Costs

Costs define what the player must pay. Multiple CostComponent entries with the same group_id are charged together. Different group_id values are alternative payment options.

Money Cost: sdm:cost_money

{
  "type": "sdm:cost_money",
  "money_id": "sdm:coins",
  "amount": 150.5,
  "group_id": "default"
}
Parameter Type Required Description
money_id resource location Yes Currency ID.
amount double Yes Price for one item. Multiplied by amount when buying multiple items.
group_id string No Payment group. Empty string means the default group.

Example of alternative payment: one offer can be bought either with coins or diamonds if the UI/purchase logic passes the selected group_id.

Rewards

Rewards are granted after conditions pass and costs are charged. If granting a reward fails, already charged costs are rolled back.

Money Reward: sdm:reward_money

{
  "type": "sdm:reward_money",
  "money_id": "sdm:coins",
  "amount": 100.0
}
Parameter Type Required Description
money_id resource location Yes Currency ID.
amount double Yes Amount granted per purchased item.

Item Reward: sdm:reward_item

{
  "type": "sdm:reward_item",
  "item": "minecraft:diamond_sword",
  "amount": 1,
  "nbt": "{Damage:0}"
}
Parameter Type Required Description
item resource location Yes Item ID.
amount int No Number of items. Defaults to 1.
nbt string No Item SNBT tags.

Command Reward: sdm:reward_command

{
  "type": "sdm:reward_command",
  "name": "Grant VIP",
  "command": "lp user {player} parent add vip"
}
Parameter Type Required Description
name string No Reward name shown in the UI.
command string No Server command. Supports {player} as the buyer name.

The command is executed on the server. Usually the leading / is not needed.

Script Reward: sdm:reward_script

{
  "type": "sdm:reward_script",
  "script_id": "give_custom_bundle"
}
Parameter Type Required Description
script_id string No Reward ID used by the script to select the action.

Promos

Promo components do not change prices by themselves. They only declare an active promo_id. Price changes are performed by PromoEffectComponent.

All promo components share these fields:

Parameter Type Required Description
promo_id string No Promo ID. Effects use it to decide which promo they apply to.
scope string No GLOBAL or PLAYER. Defaults to GLOBAL. Used by stateful components such as promo_trigger.

Time Promo: sdm:promo_time

{
  "type": "sdm:promo_time",
  "promo_id": "weekend_sale",
  "mode": "REAL_TIME_EPOCH",
  "start_time": 1713000000000,
  "end_time": 1714000000000
}
Parameter Type Required Description
mode string Yes REAL_TIME_EPOCH, SERVER_TICKS, or DAY_TIME.
start_time long Yes Window start.
end_time long Yes Window end. If it equals start_time, the promo is always active.
promo_id string No Promo ID.
scope string No Inherited from promo. For a simple time window, GLOBAL is usually enough.

For DAY_TIME, crossing midnight is supported: if start_time > end_time, the active interval wraps through 24000.

Weekly Time Promo: sdm:promo_weekly_time

The promo is active on selected weekdays. The time inside the day is optional — if omitted, the promo works for the entire selected day.

{
  "type": "sdm:promo_weekly_time",
  "promo_id": "monday_morning_sale",
  "days": ["MONDAY"],
  "start_time": "6",
  "end_time": "8"
}

Full-day Monday promo:

{
  "type": "sdm:promo_weekly_time",
  "promo_id": "monday_sale",
  "days": ["MONDAY"]
}
Parameter Type Required Description
days string[] Yes Weekdays when the promo can be active: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY. Multiple days are supported.
start_time string No Daily window start. Supports hour ("6"), time ("06:00"), or minutes from midnight ("360"). If both start_time and end_time are omitted, the promo works for the entire day.
end_time string No Daily window end. Uses the same format as start_time.
promo_id string No Promo ID.
scope string No Shared promo field. GLOBAL is usually enough when the promo depends only on the calendar.

If start_time is greater than end_time, the interval crosses midnight. For example, 22:0002:00 activates on the evening of the selected day and continues into the next night.

Cooldown Promo: sdm:promo_cooldown

The promo is active when the offer has not been purchased for a long enough time.

{
  "type": "sdm:promo_cooldown",
  "promo_id": "return_discount",
  "side": "Player",
  "cooldown_ms": 604800000
}
Parameter Type Required Description
cooldown_ms long Yes Time that must pass since the last purchase.
side string Yes Player — per player. World — shared idle time for the offer.
promo_id string No Promo ID.
scope string No Shared promo field.

Triggered Promo: sdm:promo_trigger

The promo is activated by code/script/addon for a limited duration.

{
  "type": "sdm:promo_trigger",
  "trigger_id": "boss_killed",
  "promo_id": "boss_sale",
  "duration_ms": 1800000,
  "scope": "GLOBAL"
}
Parameter Type Required Description
trigger_id string No External event ID. If empty, promo_id is used.
duration_ms long No Active duration after trigger. 0 depends on storage/activation implementation.
promo_id string No Promo ID for effects.
scope string No GLOBAL or PLAYER.

Promo Effects

All promo effects share these fields:

Parameter Type Required Description
target_promo_id string No If set, the effect works only when a promo with this ID is active. If empty, it works with any active promo.
priority int No Application order. Lower values are applied earlier.
apply_groups string[] No List of group_id values affected by the effect. Empty list means all groups.

Discount: sdm:discount

Reduces the price by a fraction of the current price.

{
  "type": "sdm:discount",
  "target_promo_id": "weekend_sale",
  "discount": 0.5,
  "priority": 0,
  "apply_groups": ["default"]
}
Parameter Type Required Description
discount double Yes Discount fraction: 0.3 = 30%, 0.5 = 50%.

Price Modifier: sdm:price_modifier

Universal effect for modifying prices.

{
  "type": "sdm:price_modifier",
  "target_promo_id": "weekend_sale",
  "operation": "ADD_PERCENT",
  "value": -25.0,
  "target_money_ids": ["sdm:coins"],
  "priority": 10
}
Parameter Type Required Description
operation string No ADD_FLAT, ADD_PERCENT, MULTIPLY, SET, MIN, MAX. Defaults to ADD_PERCENT.
value double No Operation value.
target_money_ids resource location[] No If empty, applies to all money costs. If filled, applies only to the specified currencies.

Operations:

  • ADD_FLAT: price + value
  • ADD_PERCENT: price * (1 + value / 100); use -25 for a 25% discount
  • MULTIPLY: price * value
  • SET: replace the price with value
  • MIN: use the smaller value between current price and value
  • MAX: use the larger value between current price and value

After all effects, the price is sanitized: negative, infinite, and NaN values become safe for the transaction.

Misc Components

Name: sdm:name

{
  "type": "sdm:name",
  "name": "Epic Sword"
}
Parameter Type Required Description
name string No Visible offer name. If the string is a localization key, the UI can show the translated text.

Catalog/Category: sdm:catalog

{
  "type": "sdm:catalog",
  "catalog_id": "weapons",
  "uuid": "0b2dfdcc-e9a8-4d6f-97c4-3ae18f711111",
  "order": 10
}
Parameter Type Required Description
catalog_id string No Category ID.
uuid uuid No Stable category UUID.
order int No Category sort order.

Hide When Unavailable: sdm:hide_render

{
  "type": "sdm:hide_render"
}

Component without parameters. If present on an offer, the UI does not render the offer until all offer conditions pass. Works with:

  • sdm:condition_limiter
  • sdm:condition_cooldown
  • sdm:condition_script
  • any other component extending ConditionComponent

For cooldowns and limiters with reset intervals, the UI can return the offer automatically without reopening the shop.

Offers Container: sdm:offers_container

Internal shop component that stores the offer list.

{
  "type": "sdm:offers_container",
  "offers": []
}

Usually created by the core during shop initialization; manual creation is not required.

Categories Container: sdm:categories_manager

Internal shop component for categories. Usually added automatically.

Admin Limit Commands

/sdm_shop limiter reset world <shop_id> <offer_id>
/sdm_shop limiter reset player <target> <shop_id> <offer_id>
/sdm_shop limiter reset offer <shop_id> <offer_id>
  • world resets world limits for the offer.
  • player resets player limits for the selected player.
  • offer resets both world and player limits for the offer.
  • After a successful reset, the server sends sync to clients, so the UI updates without reopening.

Complex Offer Example

The offer costs 1000 coins, grants a sword, has a personal limit of 1 purchase per day, a global limit of 100 purchases, hides when unavailable, and receives a 50% weekend discount.

{
  "uuid": "a94d5c7c-efb0-4c7e-a527-9e11b609151d",
  "components": [
    {
      "type": "sdm:name",
      "name": "shop.offer.epic_sword"
    },
    {
      "type": "sdm:catalog",
      "catalog_id": "weapons",
      "order": 10
    },
    {
      "type": "sdm:cost_money",
      "money_id": "sdm:coins",
      "amount": 1000.0,
      "group_id": "default"
    },
    {
      "type": "sdm:reward_item",
      "item": "minecraft:diamond_sword",
      "amount": 1,
      "nbt": "{Damage:0}"
    },
    {
      "type": "sdm:condition_limiter",
      "limiter_type": "Player",
      "count": 1,
      "reset_interval_ms": 86400000,
      "limit_key": "daily_player"
    },
    {
      "type": "sdm:condition_limiter",
      "limiter_type": "World",
      "count": 100,
      "limit_key": "global_stock"
    },
    {
      "type": "sdm:hide_render"
    },
    {
      "type": "sdm:promo_time",
      "promo_id": "weekend_sale",
      "mode": "DAY_TIME",
      "start_time": 0,
      "end_time": 24000
    },
    {
      "type": "sdm:discount",
      "target_promo_id": "weekend_sale",
      "discount": 0.5,
      "apply_groups": ["default"]
    }
  ]
}

Clone this wiki locally