Dungeon Inventory & Presets
Players do not take their survival inventory into a dungeon. They fight with a separate dungeon inventory: a 24-slot gear grid stored as an AstralSync snapshot, edited from a menu on the lobby, and applied to the player when they land on a dungeon server. Gear can be organised into named presets — alternative loadouts the player can switch between.
The model and the sync adapter live in the shared common module, so both the bridge and the dungeon server read the same object; all the editing (menus, dialogs, actions, requirements) is bridge-side.
The grid
A DungeonInventory is an ItemStack[24] with a fixed meaning per index:
Index | Holds |
|---|---|
| Helmet |
| Chestplate |
| Leggings |
| Boots |
| Main hand |
| Off hand |
| Storage / accessory slots (the shipped menu uses them for amulets, shields, trinkets, …) |
Nothing in the plugin assigns meaning to slots 6–23 — they are whatever the menu says they are. The shipped dungeons-inventory menu maps slot 6 to an amulet and slot 7 to a shield, both gated by AstralItems tags.
Alongside the array a DungeonInventory carries its presets collection and the id of the activePreset, if any.
Presets
A preset is a uniqueId + name + its own ItemStack[24], laid out identically to the inventory.
The rule that shapes everything else: a preset owns its gear whether or not it is equipped. Equipping one moves no items at all — it only points equippedContent() at the preset's array instead of at the inventory's. Nothing is ever copied, so nothing can be duplicated, and the dungeon inventory is empty for as long as a preset is equipped, holding the gear worn outside of any preset the rest of the time.
equippedContent() is what everything reads: the equipped preset's content when there is one, the inventory's own array otherwise. If the active preset id points at a preset that no longer exists, it falls back to the inventory — a stale id costs the player their preset, not their ability to wear anything.
Operation | Action | What happens to the gear |
|---|---|---|
Create |
| The gear currently in the dungeon inventory is moved into the new preset (not copied), and the preset is equipped — so the player keeps wearing exactly what they were, now under a name. |
Equip / unequip |
| Nothing moves. Equipping first hands any loose (non-preset) gear back to the player, then repoints |
Rename |
| Nothing moves. |
Delete |
| The preset's gear is handed back to the player. If it was equipped, |
Limits and validation
Name: 3–16 characters, non-blank, and unique per player (case-insensitive). Violations send
preset-invalid-name/preset-already-existsand abort — the dialog that collects the name cannot validate it itself.Count: 9 presets maximum (
preset-limit-reached). This is the number of cells the shipped menu's preset area has; the layout does not paginate, so a tenth preset would exist but be unreachable.Creating while a preset is equipped is refused (
preset-must-unequip). A new preset takes the gear being worn, and that gear already belongs to the equipped preset — taking it would leave that one an empty shell the player still believes holds their set.
Handing gear back
Equipping and deleting both have to dispose of real items. PresetSwaps.returnToPlayer puts what fits into the player's normal inventory there and then, and only the overflow into their AstralMailbox.
The mailbox service is looked up before anything is cleared: without it there is nowhere for the overflow to go, and clearing first would destroy the gear. If MailboxService is unavailable the whole operation fails with an ExecutableRunException rather than proceeding — so on a network without AstralMailbox running, equipping a preset over loose gear and deleting a non-empty preset both refuse. (Equipping with nothing to hand back still works: no items, nothing that can fail.)
A mailbox write that fails after the slots were cleared is not retried — retrying would hand the whole list over twice and duplicate what already landed. It is logged at ERROR with the item list itself, which is what a staff member needs to restore it manually.
Equip conflicts
EquipConflicts enforces one rule on both the inventory and preset grids: a grid may hold at most one item of any given AstralItems blueprint. Two copies of the same ring or amulet do not stack their effects, so wearing both is refused rather than silently wasted.
The check skips the slot being written to (so replacing an item with another of its own kind is fine) and skips the item itself seen from another slot (identified by ItemInstance#uniqueId, so moving an item between slots is a move, not a duplicate). An item AstralItems does not recognise has nothing to conflict with and is always allowed.
It is surfaced as two menu requirements:
Requirement | Signature | Checks against |
|---|---|---|
|
|
|
|
| The named preset's own array, for the preset editor menu. |
Editing UI
Two menus and two dialogs ship in the bridge's menus/ and dialogs/ folders.
Blueprint | Kind | Purpose |
|---|---|---|
| Menu | The main screen, opened by |
| Menu | Editor for a stored (unequipped) preset. Opened with |
| Dialog | Collects a name, then runs |
| Dialog | Collects a name for the preset passed as a parameter. |
Both menus are built from AstralCore input slots — one group per cell, seeded with copy-from and writing back through a sync action — so the player drags, shift-clicks and splits stacks the way they would in a chest.
Each cell follows the same shape:
Three conventions matter here:
return-on-close: falseis required. These items already belong to the dungeon inventory and are already recorded in it; handing them back when the menu closes would duplicate them.add-actionstake%item%,remove-actionstake none.[sync-inventory-slot] 0with no item clears slot 0. Passing%item%in the remove form would write back the stack the player just took out, and the slot would never empty.The two menus write to different places.
dungeons-inventoryuses[sync-inventory-slot], which targetsequippedContent();dungeons-presetuses[sync-preset-slot], which targets one named preset.[sync-inventory-slot]actively refuses to run from a menu that was opened with apresetparameter — a config making that mistake would quietly file the player's gear in a grid nobody is looking at.
Only an unequipped preset opens in dungeons-preset. An equipped preset's content is already the grid on the main screen; opening it in both would put two editors on the same array. The main menu's preset icons reflect this: the equipped one offers unequip / rename / delete, a stored one offers equip / edit content / rename / delete.
Equipping or unequipping from the menu is always followed by [open-menu] dungeons-inventory. Input slots are seeded when the menu is computed, so the screen has to be rebuilt for the swap to be visible.
Actions reference
Registered by the bridge module. All of them require the executor to have a dungeon inventory (SyncAPI.findData), and fail with an ExecutableRunException if not.
Action | Arguments | Description |
|---|---|---|
|
| Writes |
|
| The same, against a named preset's own array. |
|
| Writes directly into the dungeon inventory's own array, ignoring any equipped preset. The low-level form; prefer |
|
| Writes directly into a preset's array. Does not bounds-check. |
|
| See Presets. |
|
| Renames, after the same name validation. A rename to the preset's current name is a no-op. |
|
| Deletes and returns the gear. |
|
| Equips the preset if it is not the active one, unequips it otherwise. |
|
| Removes the executor's main-hand stack (the whole stack when |
Entering the dungeon with it
On the dungeon server, EquipmentListener reacts to AstralSync's PlayerDataLoadedEvent (one tick later, so a player who disconnected in the meantime is skipped):
The player's Bukkit inventory is cleared outright.
Their
DungeonInventorysnapshot is loaded (created empty if absent) andequippedContent()read.Slots 0–5 become helmet / chestplate / leggings / boots / main hand / off hand. For every non-empty slot from index 5 on, a
PlayerArmorChangeEventis fired so AstralItems' equip pipelines run for accessories too.All active potion effects are removed.
The
inventory.itemsmap fromconfig.ymlis applied on top, and the held slot is set toinventory.hand-slot.Five ticks later the player is healed to their max-health attribute and flight is revoked.
A preset saved by an older build with fewer slots than the current layout simply leaves the extra slots unset, rather than throwing the whole re-equip away.
While in a dungeon the player cannot rearrange any of this: every InventoryClickEvent is cancelled, and scrolling off hand-slot snaps straight back.
Storage
DungeonInventorySnapshotAdapter registers the snapshot with AstralSync under the key dungeons:inventory (registered by both modules). It is serialized as a binary message: the 24-slot array via ItemStack.serializeItemsAsBytes, then the preset collection (uuid, name, content), then the optional active preset id.
apply and update are intentionally empty — the dungeon server reads the snapshot itself in EquipmentListener rather than having the adapter push it into the Bukkit inventory, and the bridge's menus mutate the model object in place, which AstralSync serializes on its own schedule.