Crate Files
A crate is a single YAML file under crates/ in the plugin's data folder. Each file is deserialized into a CrateConfiguration record, and the file's top-level keys map 1:1 to that record's fields (camelCase Java field → kebab-case YAML key). The plugin ships one example, crates/example.yml.
Loading walks the crates/ folder recursively for any .yml/.yaml file (configurationManager().loadFolder("crates", CrateConfiguration.class)), so sub-folders are fine and the filename is irrelevant — the crate's identity comes entirely from its id field, not the path it was loaded from. A file that fails to parse is logged and skipped; it does not stop the rest of the folder from loading.
Top-level fields
YAML key | Java field | Type | Required | Description |
|---|---|---|---|---|
|
| String | Yes | Unique crate identifier (matched case-insensitively). See |
|
| ItemStackWrapper | Yes | The key item, tagged and handed out via the crate's item-supplier key. See |
|
|
| Yes | Named world coordinates the crate is interactable at. See |
|
|
| Yes | The weighted reward pool. See |
|
| Menu (nested) | Yes | Left-click preview-menu reference + metadata. See |
|
| ComponentWrapper | Yes | Title of the 27-slot spin-animation GUI. See |
|
| ComponentWrapperList | Yes | Lines rendered above each location. See |
|
|
| No | Actions run whenever a key is consumed. See |
|
|
| No | Actions run only when the spin animation lands on a winner. See |
Only open-actions and win-actions are @Nullable in the record — every other field is expected to be present. CrateConfiguration itself is also a ComplexPlaceholder under the crate namespace (%crate_id%, %crate_key%, %crate_rewards%, %crate_menu_title%, %crate_menu_size%) — see Placeholders.
id
The crate's unique identifier, matched case-insensitively (CrateService#findById). It drives three things:
The item-supplier key. The plugin registers itself under the
cratesnamespace (AstralPaperAPI.registerItemStackSupplier("crates", ...)), so any AstralCore item reference of the formcrates:<id>resolves throughCrateItemSuppliertoCrateConfiguration#createItemStack(amount).The value stored in the key's PDC.
createItemStackrenders thekeyitem, then tags it withidunder theastralcrates:cratePersistentDataContainerkey (CrateService.CRATE_KEY). That tag — not the item's material, name, or lore — is whatisKeyFor/getCrateKeycheck to decide which crate a held item opens.Hologram entity IDs. Each rendered hologram is registered as
<id>_hologram_<index>(one per entry inlocations).
key
An ItemStackWrapper — the same schema used by Menu Items → item-stack (material, copy-from, name, lore, append-lore, amount, enchantments, item-flags, plus any item component key such as glow, used above to force an enchant glint without an actual enchantment). createItemStack(amount) resolves this template, applies the PDC tag and amount, and clones the result — that resolved ItemStack is what backs the crates:<id> item-supplier reference and the key-needed message's %item_name% placeholder.
Any copy of a tagged ItemStack opens the crate — key identity is the PDC tag, not object identity, appearance, or amount.
locations
A map of named entries → Location (world, x, y, z, optional yaw/pitch, defaulting to 0). The map key (1 above) is a free-form label used only for YAML organization — it is never read back. Only the block coordinates matter: CrateListener and CrateService#findByLocation compare a clicked block's (rounded) coordinates against every location across every loaded crate.
Vault blocks are recommended — placing the crate on a vanilla Vault block lets AstralCrates fill its built-in item-preview slot (VaultDisplayItemEvent) and cancel its EJECTING state (VaultChangeStateEvent) so the block never dispenses vanilla loot on its own. The click-to-open logic itself does not check block type — it matches purely on coordinates — but the Vault-specific display/eject overrides only apply to actual Vault blocks.
menu
A nested record: id (String), title (ComponentWrapper), size (int). Left-clicking (or any non-right-click interaction) opens this preview via CrateService#openCratePreview, which calls plugin.menus().computeAndOpen(player, menu.id(), Map.of("crate", configuration)) — menu.id must match the id of an actual menu blueprint registered separately, under the plugin's own menus/ folder (the standard AstralCore menu system; see Menu Items). title and size here are metadata only — they are exposed as %crate_menu_title%/%crate_menu_size% (see Placeholders) but do not define the opened menu's real title or size; that is entirely up to the blueprint registered under menu.id. openCratePreview defensively no-ops (opens nothing) if menu is null.
open-menu-title
Not present in the shipped crates/example.yml (shown above for reference only) — this key is not truly optional in practice even though nothing enforces it at the type level beyond the record component not being @Nullable: ComponentWrapperTypeSerializer reads it with a plain node.getString() and passes the result straight to Adventure's MiniMessage#deserialize, which does not tolerate a null input, so omitting the key entirely is expected to fail the whole file's load rather than fall back to an empty title.
A ComponentWrapper — the MiniMessage title of the 27-slot spin-animation GUI opened by CrateOpenMenu when a key is consumed right-click-not-sneaking (Bukkit.createInventory(this, 27, configuration.openMenuTitle().get(...))). This is a different menu from the menu preview block above: this one is the actual reward-spin animation, always exactly 27 slots, built entirely in code (there is no separate blueprint to register). See Opening & Rewards for the spin/shuffle mechanics.
hologram
A ComponentWrapperList — one MiniMessage line per entry. On load, CrateService#createHolograms renders it once per locations entry via AstralHologramAPI.registerTextHologram, positioned 0.5, 1.2, 0.5 blocks above the location's block corner (i.e. roughly 1.2 blocks above the block, centered on it), center-aligned (TextAlignment.CENTER), and BillboardType.FIXED (does not rotate to face viewers). Holograms are only created/cleared when crates-enabled: true in config.yml — see Configuration.
open-actions and win-actions
Both are @Nullable PaperActionList — omit either key entirely for no extra behavior. Neither appears in crates/example.yml.
open-actionsrun whenever a key is actually consumed to open the crate:Right-click, not sneaking: once, immediately after removing the key, before the spin menu opens.
Right-click, sneaking (bulk-open): once per key held, run immediately after that key's picked reward's own
actions— so with 3 keys,open-actionsruns 3 times, interleaved with 3 reward payouts.
win-actionsrun only on the non-sneaking path, and only once the spin animation lands on a winner (never for the bulk/sneak path, and never if there were no eligible rewards to win). They run with the winningCrateRewardregistered as a placeholder on the action context, so%reward_item%and%reward_chance%resolve insidewin-actionsstrings (seerewardsbelow for that namespace).
See Opening & Rewards for the full open/win/no-rewards flow and message keys.
rewards
A map of <free-form-id> → reward entry, deserialized by CrateRewardTypeSerializer. Unlike a menu item, a reward's item fields sit directly on the reward node — material, name, lore, etc. are siblings of chance/actions/requirements, not nested under an item-stack key — because the serializer parses the whole reward node as an ItemStackWrapper (node.get(ItemStackWrapper.class)) alongside three other fields read from the same node:
Field | Type | Required | Description |
|---|---|---|---|
(item fields) | ItemStackWrapper | Yes | Same top-level node as |
| double | Yes | Relative weight used to pick this reward — see Reward Selection. Not a percentage. |
| PaperActionList | Yes | Run when this specific reward is picked (both the spin-win and bulk/sneak paths). See Actions. |
|
| No | If present and it fails for the opening player, the reward is excluded from the pool entirely (not merely hidden or greyed out). See Requirements. |
Because the ItemStackWrapper deserializer only reads the keys it recognizes, any other key placed at the reward's top level (like slot above) is silently ignored — see the walkthrough below; the shipped example ships a slot on every reward that has no effect on anything. The map key itself (1, 2, 3) is likewise free-form and never read back.
CrateReward is a ComplexPlaceholder under the reward namespace: %reward_item% and %reward_chance% resolve to the reward's item and its chance weight respectively — usable from win-actions (see above) and from the crate opening menu/placeholders.
Reward Selection
CrateService#pickRandomReward(player, configuration) is called fresh on every open (nothing is cached between opens). It builds a RandomCollection<CrateReward>, iterating every entry in rewards and:
Skipping any reward whose
requirementsfails for the opening player (requirements are evaluated once per pick, so results can differ between a preview render and the actual open if player state changes between them).Adding every reward that passes (or has no
requirements) with itschanceas the weight.Drawing one reward with
random.nextDouble() * totalWeightagainst the cumulative-weight table.
chance is a relative weight, not a percentage — RandomCollection never normalizes against 100. Scaling every reward's chance by the same factor (e.g. 50/30/20 → 100/60/40) has no effect on the odds; only the ratio between rewards matters. A chance of 0 or negative throws an IllegalArgumentException during the pick (RandomCollection#add rejects non-positive weights), so every reward that can be reached needs a strictly positive chance.
If every reward is filtered out by requirements (or rewards is empty), pickRandomReward returns null — CrateOpenMenu treats that as "no eligible rewards": the key is handed back to the player and the no-rewards message is sent instead of spinning. See Opening & Rewards.
CrateService#pickMultipleRewards (the sneaking/bulk-open path) simply calls pickRandomReward once per key held, independently each time — the pool is not depleted between picks, so the same reward can be won more than once from a single bulk-open.
Annotated walkthrough: example.yml
The plugin ships exactly one crate definition, reproduced below in full:
id: "example"— registers the crate at the item-supplier referencecrates:example, and is the value stamped into every key'sastralcrates:cratePDC tag.key— a glowingTRIPWIRE_HOOKnamed "Example Crate", the item players must hold to right-click-open.menu— setstitle/sizemetadata, but omitsid.Menu.idhas no default; on this shipped file it deserializes tonull, socomputeAndOpen(player, null, ...)cannot resolve a blueprint and its returned future completes exceptionally.openCratePreviewnever attaches a.exceptionally()/.join()handler to that future, so the failure is swallowed — left-clicking this example crate silently opens nothing. Add anidundermenu(and a matching blueprint file undermenus/) to make the preview functional.locationshas one entry (map key1, unused beyond organization) atworld100.5, 65.0, -200.5.hologramrenders two centered, fixed-billboard lines 1.2 blocks above that location.rewardsdefines three entries with weights50 / 30 / 20(i.e. 50%/30%/20% odds only because they happen to sum to 100 — see Reward Selection). Each carries aslotkey (1,3,5) that is not a recognizedItemStackWrapperorCrateRewardfield and is silently ignored — the spin menu places rewards into its own 9-slot strip by shuffle order, not by thisslotvalue.Reward
1and3'sactionsuse the required[action-id] ...bracket syntax ([console],[message]) — see Actions. Reward2'sactions—"give %player% gold_ingot 1"and"msg %player% &6You won a gold ingot!"— omit the brackets entirely.PaperActionListparsing requires a leading[and a matching](DefaultPlatformActionFactory#createthrowsExecutableParseExceptionotherwise), so this reward's action list fails to parse, which fails the wholeCrateRewarddeserialization, which fails the wholeCrateConfigurationdeserialization for the file — as shipped,example.ymlfails to load at all and is skipped with a logged error. Fix reward2's actions to use the bracket syntax (e.g."[console] give %player_name% gold_ingot 1","[message] You won a gold ingot!") before using this file as a starting point.No
open-actionsorwin-actionsare set on this crate.
See also
Overview — the click → key → preview/spin flow this file's fields drive.
Opening & Rewards — the full open/win/no-rewards pipeline and message keys.
Configuration — the
crates-enabledswitch that gates loading this folder at all.Placeholders — the
%crate_*%and%reward_*%placeholder namespaces.Developer API —
CrateService, theastralcrates:cratePDC key, and thecrates:<id>item-supplier namespace.