Opening & Rewards
This page covers what happens when a player interacts with a placed crate block — key validation, the spin-menu animation, quick opening, the crate preview, and the vanilla Vault block hooks. For the YAML shape of a crate (rewards, key, locations, actions) see Crate Files; for crates-enabled and other global settings see Configuration.
All of this lives in CrateListener, CrateOpenMenu, CrateService, and KeyListener.
Interacting with a crate
CrateListener#onInteract handles PlayerInteractEvent at EventPriority.LOWEST for every block that matches a configured crate location (CrateService#findByLocation, matched by world + block coordinates). Any interaction with that block — left or right click, either hand — cancels the vanilla event first, so the block never opens a real container or triggers other block behavior. From there:
Interaction | Condition | Result |
|---|---|---|
Any click | Off-hand ( | Ignored entirely — only main-hand ( |
Left-click (or any non-right-click action), main hand | — | Opens the crate's preview menu. |
Right-click, main hand | Held item is not a valid key for this crate |
|
Right-click, main hand, valid key | Player is sneaking | Quick opening — consumes all held keys at once. |
Right-click, main hand, valid key | Player is not sneaking | Normal open: runs |
A key is valid for a crate when its PersistentDataContainer carries the astralcrates:crate key (CrateService.CRATE_KEY) with a string value equal to the crate's id (case-insensitive) — CrateService#isKeyFor.
The spin menu
The normal (non-sneaking) right-click flow, after the key is validated:
The crate's
open-actionsrun against the player (plainPaperActionList.run(player)— no reward context yet).1 key is removed from the item in hand (the whole stack is removed if the amount is
1, otherwise the amount is decremented by1).A
CrateOpenMenuis constructed and opened — a fixed 27-slot inventory titled from the crate'sopen-menu-title.
The winner is predetermined the instant the menu is constructed, before any animation runs: CrateOpenMenu calls CrateService#pickRandomReward, a weighted pick (RandomCollection, weight = each reward's chance) over the crate's rewards, skipping any reward whose requirements fail for this player. The rewards actually shown in the GUI are built from that same filtered (and then shuffled) list, so what the player watches spin is cosmetic — the outcome never changes based on when they close the inventory.
The spin only occupies the middle row (inventory slots 9–17); the winning item always lands on the center slot (slot 13, the 5th of the middle row):
The row shifts one item at a time, playing
Sound.UI_BUTTON_CLICK(volume0.5, pitch1.2) on every shift.The animation runs for a random number of shifts between 20 and 35 (at least one full lap of the reward pool, extended to stay in that range).
Shift delay is 1 tick for the first 70% of shifts, then decelerates — linearly ramping up to 5 ticks per shift — for the final 30%, so the spin visibly slows down before landing.
Once the target shift count is reached, the row pauses 5 ticks on the final result, then
Sound.ENTITY_PLAYER_LEVELUPplays and the 8 non-center slots are cleared, and after another 10-tick delay the reward is actually handed out (see Reward payout).
If the player closes the inventory before the animation finishes, the win is not lost: an InventoryCloseEvent listener registered for that specific menu instance calls the same reward-granting logic immediately, guarded by a rewardGiven flag so the reward is only ever paid out once (whichever of the animation-completion path or the early-close path fires first). Clicking inside the spin menu (InventoryClickEvent) is always cancelled — items can't be dragged out mid-spin.
Reward payout
When a winning CrateReward is resolved (from the spin menu or quick opening):
The reward's own
actionsrun against the player.Only for the spin-menu path, the crate's
win-actions(if configured) then run against the player with the winning reward registered on the placeholder container under namespacereward— the reward exposes%reward_chance%and%reward_item%(the reward's item, chainable with the standarditem_*sub-keys) to those actions.open-actionsare not re-run at this point — they already ran before the menu opened.The player's inventory closes.
Failures in either action list are caught and logged per-list; a broken win-actions entry does not prevent the reward's own actions from having already run.
No eligible reward
If every reward's requirements filter out the current player (or the crate has no rewards at all), pickRandomReward returns no winner. The intended fallback — reachable from the early-close path — sends NO_REWARDS and refunds one key item (freshly built from the crate's key template) to the player's inventory instead of granting a reward.
Quick opening (sneak + right-click)
Sneaking while right-clicking with a valid key skips the spin animation entirely:
The entire held stack is consumed at once —
keyCountis the fullgetAmount()of the item in hand, and the main-hand slot is cleared in one call, regardless of how many keys were held.CrateService#pickMultipleRewardsdrawskeyCountindependent weighted picks (pickRandomRewardcalled once per key).For each non-null pick: the reward's own
actionsrun, then the crate'sopen-actionsrun again (once per reward won, not once for the whole batch).win-actionsare not run in this path.If a given draw comes back with no eligible reward, that key is simply consumed with no reward, no actions, and no refund — quick-opening never sends
NO_REWARDSor gives a key back.
There is no animation, no CrateOpenMenu, and no sounds played for quick opening — rewards are granted instantly.
Crate preview (left-click)
Left-clicking (or any non-right-click interaction with) a crate block calls CrateService#openCratePreview, which opens the crate's configured menu.id — a standard AstralCore menu blueprint from plugins/AstralCrates/menus/, computed with the crate bound to the crate parameter. If the crate has no menu configured, nothing happens. This does not consume a key and does not touch rewards; it's purely a look-but-don't-open GUI, defined the same way as any other AstralCore menu (see Menu Items).
Wrong or missing key
Message key | Sent when | Placeholders |
|---|---|---|
| Right-click with an item that isn't a valid key for this crate (including empty hand). |
|
| See No eligible reward. | None. |
Vault block integration
If a crate's location block is a vanilla Vault (org.bukkit.block.data.type.Vault), CrateListener also hooks two Paper events on it:
VaultDisplayItemEvent— whenever the vault needs an item to display floating above it, the listener picks one of the crate's reward items at random (ThreadLocalRandom, uniform over all configured rewards) and sets it as the display item. This pick ignores each reward'srequirements— the display can show a reward the interacting player isn't currently eligible for.VaultChangeStateEvent— cancelled whenever the vault's new state isVault.State.EJECTING. This stops the vanilla vault from ejecting its own loot item; the crate always hands out rewards through the flows above instead.
Both handlers only apply to blocks already registered as a crate location — a bare, unconfigured Vault block is unaffected.
Keys cannot be placed
KeyListener#onPlace cancels BlockPlaceEvent whenever the item being placed is a crate key (CrateService#isKey — any item whose PDC carries the astralcrates:crate key, for any crate, not just a specific one). This listener is always registered on enable, independent of crates-enabled.
Enabling crate interactions
CrateListener (opening, preview, and the Vault hooks above) is only registered when crates-enabled: true in config.yml. If it's false, crate blocks behave like any other block — no cancellation, no key check, no menu — though key placement is still blocked by KeyListener regardless of this setting. See Configuration.