Kits and crates#
Two independent reward systems share this page. Kits are item bundles defined in a JSON file and claimed on a cooldown. Crates are weighted prize tables backed by virtual keys, spent one key at a time.
Kit definitions#
Kits live in config/vonix_server_utilities/kits.json. The file is created on
first launch with three working kits, so the feature is usable before you edit
anything.
The root is an object with a single kits array:
{
"kits": [
{
"name": "starter",
"group": "starter",
"cooldown_seconds": 3600,
"one_time": false,
"items": [
{ "item": "minecraft:stone_sword", "count": 1 },
{ "item": "minecraft:bread", "count": 16 }
]
}
]
}
Schema#
| Key | Type | Default | Meaning |
|---|---|---|---|
name |
string | required | Kit identifier. Trimmed and lower-cased; used by /kit <name>. |
group |
string | kit name | Claim-window group. Missing, blank, or non-text values fall back to the kit name. |
cooldown_seconds |
integer | 3600 | Seconds before the group can be claimed again. Non-numeric values fall back to 3600. |
one_time |
boolean | false | When true, the group may be claimed once ever instead of on a cooldown. |
items |
array | empty | List of item objects. An empty list produces a claimable kit that grants nothing. |
items[].item |
string | required | Namespaced item id, e.g. minecraft:iron_pickaxe. Entries without a string id are skipped. |
items[].count |
integer | 1 | Stack size to give. Non-numeric values fall back to 1. |
An entry with no usable name is skipped with a warning. An entry naming an
item id that does not exist in the registry is also skipped, item by item, so one
bad id does not discard the rest of the kit.
Group claim windows#
group is the field that makes kits interesting. The cooldown is enforced per
group, not per kit name. A player may hold at most one active claim inside a
group, while kits in different groups stay independently claimable.
That gives you two useful patterns:
- Class kits. Give
warrior,mage, andrangerthe samegroupvalue. A player picks one and cannot claim another until the shared cooldown expires. - One-time bundles. Give a starter pack
"one_time": true. The first claim writes a row; every later claim in that group returns "already claimed".
Because a missing group defaults to the kit name, existing single-kit files keep
their old per-kit behaviour with no edits. That default is also why the seeded
starter, tools, and food kits are independent despite sharing a file.
Seeded defaults#
On first launch VSU writes three kits:
| Kit | Cooldown | One time | Contents |
|---|---|---|---|
starter |
3600 s | false | stone sword, stone pickaxe, stone axe, 16 bread, 32 torches |
tools |
7200 s | false | iron pickaxe, axe, shovel, and hoe |
food |
1800 s | false | 32 cooked beef, 2 golden apples, 1 cake |
If kits.json is unreadable or its root is not a JSON object, the kit manager
logs the failure and seeds the same three kits in memory. A broken file therefore
degrades to the defaults rather than leaving the server with no kits.
Using kits#
| Command | Effect |
|---|---|
/kits |
List loaded kit names |
/kit |
Same as /kits |
/kit <name> |
Claim a kit |
/kit reload |
Re-read kits.json from disk |
/vonixsu reload |
Re-read the properties file, then kits.json |
Claim outcomes are explicit:
- Success — items are added to the inventory; anything that does not fit is dropped at the player's feet.
- Not found — the name is not loaded.
- On cooldown — the remaining time is printed in a compact
1h 12mform. - Already claimed — the group is one-time and this player has used it.
The eligibility check and the write happen in one transaction against the database, under a process-wide lock, so two simultaneous claims cannot both succeed.
Cooldown storage#
Claims persist in vsu_kit_cooldowns:
CREATE TABLE vsu_kit_cooldowns (
uuid TEXT NOT NULL,
kit_name TEXT NOT NULL,
claim_group TEXT NOT NULL,
last_used INTEGER NOT NULL,
PRIMARY KEY (uuid, kit_name)
);
The lookup that decides a claim is SELECT MAX(last_used) ... WHERE uuid=? AND
claim_group=?, so the group is what actually gates the window. claim_group was
added after the table's first release; databases created before it are upgraded
in place on startup:
- The column is added with an empty default if it is missing.
- Rows with a null or blank group are backfilled from
lower(kit_name). - An index on
(uuid, claim_group)is created.
The migration never drops the table, never rewrites last_used, and never
touches rows that already carry a group. It is safe to run on every start, which
is exactly what happens.
Crates#
Crates are virtual prize boxes. A crate has a key type, a set of weighted prizes, and an enabled flag. Opening a crate consumes one key of that type and runs the winning prize's command.
Two crates are created on first start: playtime with key type playtime, and
event with key type event. Both are enabled and start with no prizes, so an
/crate open on a fresh install reports crate_empty until you add one.
Player commands#
| Command | Node | Effect |
|---|---|---|
/crate list |
vsu.command.crate |
List crates, key type, enabled state |
/crate open <crate> |
vsu.command.crate |
Spend one key, draw a prize, run its command |
/crate prizes <crate> |
vsu.command.crate |
List prizes with weight and computed percentage |
/crate key balance <type> |
vsu.command.crate |
Your balance of one key type |
Administration#
All of these require vsu.admin.crate at operator level 2.
| Command | Effect |
|---|---|
/crate create <crate> <key_type> |
Create a crate; fails if the name already exists |
/crate delete <crate> |
Delete a crate and its prizes |
/crate enable <crate> |
Allow the crate to be opened |
/crate disable <crate> |
Refuse opens without deleting anything |
/crate prize add <crate> <label> <weight> <command> |
Add a prize; a new id is allocated per crate |
/crate prize remove <crate> <id> |
Remove one prize by id |
/crate key give <player> <type> <amount> |
Add keys, 1 to 1,000,000 |
/crate key take <player> <type> <amount> |
Remove keys; refuses to go below zero |
Names and key types must match [a-z0-9_-]{1,32} and are lower-cased. Prize
commands are stored without a leading slash, and {player} in the command string
is replaced with the claiming player's name at execution time.
Prize percentages are derived, not stored: /crate prizes divides each weight by
the sum of positive weights. Weights are integers from 1 to 1,000,000.
Playtime keys#
A tick task runs once per minute and compares each online player's vanilla
playtime statistic against playtime_key_interval_minutes. Each completed
interval grants one playtime key.
Progress is tracked in vsu_playtime_key_progress by the number of intervals
already awarded, so restarts do not double-grant and a player who plays for three
hours at a 60-minute interval receives three keys in one pass rather than one.
The award cannot be turned off through the config. playtime_key_interval_minutes
is clamped to a minimum of 1 when the file is read, so a value of 0 or a
negative number behaves as 1 minute — the most aggressive setting, not the
disabled one. Pick a large interval such as 100000 if you want the system
effectively dormant.
Prize execution and refund#
Opening a crate is transactional and crash-safe:
- Check the crate exists, is enabled, and the player holds at least one key.
- Read prizes with a positive weight and draw one by weight.
- Debit the key and insert a
PENDINGclaim row in the same transaction. - Run the prize command on the main thread with suppressed output.
- On success, mark the claim
COMPLETED. On failure or a parse error, refund the key and mark itREFUNDED.
A crash between steps 3 and 5 leaves a PENDING row. On the next start VSU
refunds every pending claim it finds, so a server that dies mid-open costs the
player nothing.
Crate tables#
| Table | Purpose |
|---|---|
vsu_crates |
Crate name, key type, enabled flag, updated timestamp |
vsu_crate_prizes |
Prize id, label, command, weight, per crate |
vsu_player_keys |
Key balance per player per key type |
vsu_crate_claims |
One row per open, with status and completion time |
vsu_playtime_key_progress |
Intervals already awarded per player |
Column detail for these tables is on Database & storage.
Worked example#
/crate create vote vote
/crate prize add vote "1x diamond" 70 "give {player} minecraft:diamond 1"
/crate prize add vote "32x bread" 25 "give {player} minecraft:bread 32"
/crate prize add vote "netherite ingot" 5 "give {player} minecraft:netherite_ingot 1"
/crate key give Steve vote 3
/crate prizes vote then reports roughly 70.00%, 25.00%, and 5.00%. A player runs
/crate open vote three times, spending one key each time; the fourth attempt
reports missing_key:vote.
Reload behaviour#
/kit reload re-reads kits.json only. /vonixsu reload re-reads the properties
file and then kits.json. Neither touches crate data, because crates live in the
database rather than a file.
Editing kits.json while the server runs is safe: nothing watches the file, so
your edit takes effect on the next reload or restart, not mid-claim. Cooldowns
survive a reload because they are keyed by group name; renaming a group resets
the effective window for everyone.