-
Notifications
You must be signed in to change notification settings - Fork 1
JSON Shop Components ECS EN
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.
- All
ConditionComponententries are checked. - Limits are checked for the requested purchase amount.
- A cost group
group_idis selected. - Active promos are collected and promo effects are applied by
priority. - The cost is charged.
- Rewards are granted.
- 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 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.
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 bysdm:hide_renderreturns 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 setlimit_key.
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.
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 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.
{
"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 are granted after conditions pass and costs are charged. If granting a reward fails, already charged costs are rolled back.
{
"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. |
{
"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. |
{
"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.
{
"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. |
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. |
{
"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.
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:00 → 02:00 activates on the evening of the selected day and continues into the next night.
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. |
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. |
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. |
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%. |
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-25for a 25% discount -
MULTIPLY:price * value -
SET: replace the price withvalue -
MIN: use the smaller value between current price andvalue -
MAX: use the larger value between current price andvalue
After all effects, the price is sanitized: negative, infinite, and NaN values become safe for the transaction.
{
"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. |
{
"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. |
{
"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_limitersdm:condition_cooldownsdm: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.
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.
Internal shop component for categories. Usually added automatically.
/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>-
worldresets world limits for the offer. -
playerresets player limits for the selected player. -
offerresets both world and player limits for the offer. - After a successful reset, the server sends sync to clients, so the UI updates without reopening.
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"]
}
]
}