Loot & Rewards
Rewards are never handed to the player directly inside a dungeon. Mob kills and reward pots drop owner-locked items that carry their payoff (commands) in the item's PDC; picking one up queues that payoff onto the player's DungeonPlayerData instead of running it. Experience works the same way — it just accumulates on DungeonPlayerData as a number. Everything queued is only actually paid out once the player leaves the instance and reconnects to the lobby, where the bridge module claims it. See Dungeon & Room Blueprints for how blueprints are authored and loaded, and Placeholders for %dungeons_experience%.
Loot tables
A DungeonLootTable (DungeonLootTable) is the shape used by both mobs-loot-tables and components-loot-tables on a dungeon blueprint:
Field | Type | Description |
|---|---|---|
|
| Map of entry id → |
| int | Minimum number of loot rolls per |
| int | Maximum number of loot rolls (inclusive) per |
Each Entry is:
Field | Type | Description |
|---|---|---|
|
| The dropped item's appearance. See item-stack for the field reference. |
|
| Commands queued for the player once they pick up this reward and later claim it (see Picking up rewards). |
| double | Relative weight for this entry, fed into a |
generateLoot() rolls a random count in [minimum, maximum], then draws that many entries one at a time via generateSingleLoot() (a single weighted pick). The results are collected into a Set<Entry>, so if the same entry is drawn more than once in one generateLoot() call, it only yields one drop — the actual number of items dropped can be lower than the roll count when the table has few high-weight entries.
Mob loot & experience
Both mob-experiences and mobs-loot-tables on the blueprint are keyed by mob blueprint id (the id registered with AstralMobs, not the entity type). MobListener#onMobDeath handles EntityDeathEvent: it only acts when the killer (getDamageSource().getCausingEntity()) is a Player, the mob died inside a dungeon world the player participates in, and that instance's state is ACTIVE.
On a qualifying kill:
The mob's blueprint id is resolved via
MobsAPI.getBlueprint(entity.getUniqueId()). If it isn't found, ormob-experienceshas no entry for that id, the kill is logged and skipped (no XP, no loot).The configured experience value is added to the killer's
DungeonPlayerData.experience.If
mobs-loot-tableshas an entry for that mob id,generateLoot()is rolled and each resultingEntryis spawned withRewardService.spawnItem(killer, itemStack, entity.getLocation(), entry.commands())(see Picking up rewards) — i.e. at the mob's death location, the corpse. The item'sdisplayis resolved through the killer's placeholder container (AstralPaperAPI.createPlaceholderContainer(player)), so name/lore can reference player placeholders. A failure spawning one entry is caught and logged per-entry; it doesn't stop the rest of the loot table.
A mob with no mob-experiences entry drops no XP and is logged as a warning; a mob with no mobs-loot-tables entry simply drops no loot (silent, no warning). Every drop is owner-locked to the killer (Item#setOwner) with setCanMobPickup(false), so mobs at the corpse can't pick items back up — see Picking up rewards.
Reward pots
components-loot-tables is a general per-blueprint map, but only one key is currently consumed: pots. PotListener reads blueprint().componentsLootTables().get("pots") — any other key in that map is inert.
Reward pots are placed as vanilla DecoratedPot blocks inside room schematics. PlayerInteractEvent (main hand only) on such a block, while the interacting player is alive and participating in that pot's dungeon instance, is always cancelled and instead:
Draws one entry from the pots table with
generateSingleLoot()— the table'sminimum/maximumare not used here; a click always yields exactly one entry (or nothing, ifgenerateSingleLoot()returnsnull).Reads the remaining-reward counter from the pot block's PDC, key
astral-dungeons:pot_rewards(PersistentDataType.INTEGER), defaulting to 3 when absent — so each pot starts with 3 rewards.Wobbles the pot (
DecoratedPot.startWobble, randomlyPOSITIVEorNEGATIVE) and spawns 5Particle.CLOUDparticles above it.Decrements and writes back the counter, plays
Sound.BLOCK_DECORATED_POT_INSERT, and updates the block state.Spawns the reward via
RewardService.spawnItemat the pot's location with a small random upward/outward velocity (a parabolic toss), trailingParticle.SMALL_GUSTeach tick until it lands or dies.Removes the block from the instance's
alivePots()tracking set.If the counter reached
0, breaks the pot: sets the block toAIRand playsSound.BLOCK_DECORATED_POT_SHATTER.
So each pot yields up to 3 rewards (one per click) before it shatters, regardless of how the pots loot table's minimum/maximum are configured. Unlike mob loot, a pot's display is resolved with no placeholder container (ItemStackWrapper#get(), no-arg).
Picking up rewards
Every reward item — from a mob kill or a pot — is dropped through the same RewardService.spawnItem(owner, itemStack, location, commands, …):
The dropped
Itementity's owner is set to the player it belongs to (Item#setOwner) andsetCanMobPickup(false), so only that player (not mobs, and not other players) can pick it up.The entry's
commandsare written to the item entity's PDC underastraldungeons:commandas aPersistentDataType.LIST.strings()(RewardService.getCommandsreads it back).
RewardListener#onPickup handles EntityPickupItemEvent at MONITOR priority:
If the picking-up player isn't currently tracked in a dungeon instance, or has no
DungeonPlayerDatain the paper module's cache, the pickup is cancelled and a warning is logged (defensive guard — should not happen in normal play).If the picked-up item carries a
astraldungeons:commandPDC list, the event is cancelled and the item entity removed; its commands are added toplayerData.commands(), aCHALLENGE-type toast is shown ("You received a reward!" / "Check your rewards with /dungeon rewards", with the item as the toast icon), andminecraft:entity.player.levelupplays at the player's location. The vanilla pickup (into the player's inventory) never happens — the reward only exists as queued commands from this point on.An item with no commands PDC (i.e. not one of these reward drops) is picked up normally.
Experience
mob-experiences values are added straight onto DungeonPlayerData.experience (a running double) on each qualifying kill — there's no separate leveling or currency conversion inside the dungeon itself. While a player is inside an instance, their current total is exposed live as %dungeons_experience% (DungeonsPlaceholders, namespace dungeons); see Placeholders.
Cross-server reward claim
Rewards accumulate per-player on DungeonPlayerData (experience + a Set<String> commands) and are only turned into effects once the player is back on the lobby:
On the dungeon server —
PlayerDataService(paper) keeps an in-memoryMap<UUID, DungeonPlayerData>.add(player)seeds an empty entry on join (when the player's party has an active instance);MobListener/RewardListenermutate it in place while the player is inside. On quit,flush(playerId)removes it from the cache and persists it to the shared cache under keydungeons:players_data:<uuid>(DungeonPlayerDataRepository, JSON viaGson).On the lobby — the bridge module's
PlayerConnectionListenerlistens for AstralSync'sPlayerDataLoadedEventand calls the bridge'sPlayerDataService#claimRewards(player).claimRewardsreadsdungeons:players_data:<uuid>from the cache (DungeonPlayerDataRepository#get). If the read fails it logs an error and stops; if there's no cached data for that player it silently does nothing (nothing was ever earned, or it was already claimed).Otherwise it builds the player's placeholder container and registers the
DungeonPlayerDataonto it (namespacedata, exposing%data_experience%), then runs the configuredreward-actions— aPaperActionList— against that player and container.It then dispatches every queued command from console, substituting
%player_name%for the player's name, on the main thread.Finally the cached entry is deleted (
DungeonPlayerDataRepository#delete). Any exception while running reward actions or deleting is caught and logged; a failure at this stage does not re-queue the claim — the player would need anotherPlayerDataLoadedEvent(e.g. a relog) to retry if the delete failed partway.
reward-actions lives in the bridge config.yml:
Authoring notes
Loot and experience are defined per blueprint, on the dungeon (paper) server —
mobs-loot-tables,components-loot-tables, andmob-experiencesare all blueprint fields, loaded fromplugins/AstralDungeons/blueprints/. See Dungeon & Room Blueprints.The payoff for experience is not decided on the dungeon server at all: a kill only adds a number to
DungeonPlayerData.experience. What that number actually does (a chat message, a currency grant, an item, …) is entirely up to the bridge'sreward-actions, evaluated once, on the lobby, with%data_experience%available.commandson a lootEntryare plain console command strings (no placeholder substitution beyond%player_name%at claim time) — build any dynamic values (amounts, item ids) directly into the string per entry.chanceis a relative weight, not a percentage — pad or shrink the table by adding/removing entries or adjusting other entries' weights, not by trying to make every table sum to a fixed total.The reward item's PDC command key (
astraldungeons:command) and the pot's remaining-reward key (astral-dungeons:pot_rewards) use different namespace spellings (no hyphen vs. hyphenated) — this is a genuine inconsistency in the source, not a typo in this page; the two keys are unrelated to each other so it has no functional effect.