Astral Realms Documentation Help

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

id

id

String

Yes

Unique crate identifier (matched case-insensitively). See id.

key

key

ItemStackWrapper

Yes

The key item, tagged and handed out via the crate's item-supplier key. See key.

locations

locations

Map<String, Location>

Yes

Named world coordinates the crate is interactable at. See locations.

rewards

rewards

Map<String, CrateReward>

Yes

The weighted reward pool. See rewards.

menu

menu

Menu (nested)

Yes

Left-click preview-menu reference + metadata. See menu.

open-menu-title

openMenuTitle

ComponentWrapper

Yes

Title of the 27-slot spin-animation GUI. See open-menu-title.

hologram

hologram

ComponentWrapperList

Yes

Lines rendered above each location. See hologram.

open-actions

openActions

@Nullable PaperActionList

No

Actions run whenever a key is consumed. See open-actions and win-actions.

win-actions

winActions

@Nullable PaperActionList

No

Actions run only when the spin animation lands on a winner. See open-actions and win-actions.

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

id: "example"

The crate's unique identifier, matched case-insensitively (CrateService#findById). It drives three things:

  • The item-supplier key. The plugin registers itself under the crates namespace (AstralPaperAPI.registerItemStackSupplier("crates", ...)), so any AstralCore item reference of the form crates:<id> resolves through CrateItemSupplier to CrateConfiguration#createItemStack(amount).

  • The value stored in the key's PDC. createItemStack renders the key item, then tags it with id under the astralcrates:crate PersistentDataContainer key (CrateService.CRATE_KEY). That tag — not the item's material, name, or lore — is what isKeyFor/getCrateKey check to decide which crate a held item opens.

  • Hologram entity IDs. Each rendered hologram is registered as <id>_hologram_<index> (one per entry in locations).

key

key: material: TRIPWIRE_HOOK glow: true name: "<red>Example Crate" lore: - "<gray>This is an example crate." - "<gray>Right-click to open!"

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

locations: 1: world: "world" x: 100.5 y: 65.0 z: -200.5

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: title: "<blue>Example Crate Menu" size: 27

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

open-menu-title: "<gold><bold>Opening Example Crate..."

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

hologram: - "<gold><bold>Example Crate" - "<gray>Right-click to open!"

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-actions run 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-actions runs 3 times, interleaved with 3 reward payouts.

  • win-actions run 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 winning CrateReward registered as a placeholder on the action context, so %reward_item% and %reward_chance% resolve inside win-actions strings (see rewards below for that namespace).

See Opening & Rewards for the full open/win/no-rewards flow and message keys.

rewards

rewards: 1: chance: 50 material: DIAMOND slot: 1 name: "<aqua>Diamond Reward" lore: - "<gray>You have a chance to win this!" actions: - "[console] give %player_name% diamond 1" - "[message] You won a diamond!"

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 nodematerial, 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 chance/actions/requirements — see Menu Items → item-stack for the field list.

chance

double

Yes

Relative weight used to pick this reward — see Reward Selection. Not a percentage.

actions

PaperActionList

Yes

Run when this specific reward is picked (both the spin-win and bulk/sneak paths). See Actions.

requirements

@Nullable PaperRequirementList

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:

  1. Skipping any reward whose requirements fails 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).

  2. Adding every reward that passes (or has no requirements) with its chance as the weight.

  3. Drawing one reward with random.nextDouble() * totalWeight against the cumulative-weight table.

chance is a relative weight, not a percentageRandomCollection never normalizes against 100. Scaling every reward's chance by the same factor (e.g. 50/30/20100/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 nullCrateOpenMenu 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" key: material: TRIPWIRE_HOOK glow: true name: "<red>Example Crate" lore: - "<gray>This is an example crate." - "<gray>Right-click to open!" menu: title: "<blue>Example Crate Menu" size: 27 locations: 1: world: "world" x: 100.5 y: 65.0 z: -200.5 hologram: - "<gold><bold>Example Crate" - "<gray>Right-click to open!" rewards: 1: chance: 50 material: DIAMOND slot: 1 name: "<aqua>Diamond Reward" lore: - "<gray>You have a chance to win this!" actions: - "[console] give %player_name% diamond 1" - "[message] You won a diamond!" 2: chance: 30 material: GOLD_INGOT slot: 3 name: "<yellow>Gold Ingot Reward" lore: - "<gray>You have a chance to win this!" actions: - "give %player% gold_ingot 1" - "msg %player% &6You won a gold ingot!" 3: chance: 20 material: EMERALD slot: 5 name: "<green>Emerald Reward" lore: - "<gray>You have a chance to win this!" actions: - "[console] give %player_name% emerald 1" - "[message] You won an emerald!"
  • id: "example" — registers the crate at the item-supplier reference crates:example, and is the value stamped into every key's astralcrates:crate PDC tag.

  • key — a glowing TRIPWIRE_HOOK named "Example Crate", the item players must hold to right-click-open.

  • menu — sets title/size metadata, but omits id. Menu.id has no default; on this shipped file it deserializes to null, so computeAndOpen(player, null, ...) cannot resolve a blueprint and its returned future completes exceptionally. openCratePreview never attaches a .exceptionally()/.join() handler to that future, so the failure is swallowed — left-clicking this example crate silently opens nothing. Add an id under menu (and a matching blueprint file under menus/) to make the preview functional.

  • locations has one entry (map key 1, unused beyond organization) at world 100.5, 65.0, -200.5.

  • hologram renders two centered, fixed-billboard lines 1.2 blocks above that location.

  • rewards defines three entries with weights 50 / 30 / 20 (i.e. 50%/30%/20% odds only because they happen to sum to 100 — see Reward Selection). Each carries a slot key (1, 3, 5) that is not a recognized ItemStackWrapper or CrateReward field and is silently ignored — the spin menu places rewards into its own 9-slot strip by shuffle order, not by this slot value.

  • Reward 1 and 3's actions use the required [action-id] ... bracket syntax ([console], [message]) — see Actions. Reward 2's actions"give %player% gold_ingot 1" and "msg %player% &6You won a gold ingot!" — omit the brackets entirely. PaperActionList parsing requires a leading [ and a matching ] (DefaultPlatformActionFactory#create throws ExecutableParseException otherwise), so this reward's action list fails to parse, which fails the whole CrateReward deserialization, which fails the whole CrateConfiguration deserialization for the file — as shipped, example.yml fails to load at all and is skipped with a logged error. Fix reward 2'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-actions or win-actions are 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-enabled switch that gates loading this folder at all.

  • Placeholders — the %crate_*% and %reward_*% placeholder namespaces.

  • Developer APICrateService, the astralcrates:crate PDC key, and the crates:<id> item-supplier namespace.

Last modified: 25 July 2026