Dungeon & Room Blueprints
The dungeon server reads two folders under its data directory (plugins/AstralDungeons/), both scanned recursively (.yml/.yaml, any subfolder depth) by BlueprintService.load() on enable and on /dungeon reload:
Folder | File type | Java type | Keyed by |
|---|---|---|---|
|
|
| the room's own |
|
|
| the blueprint's own |
A RoomBlueprint is produced by AstralRoomEditor's in-game building tool — it describes a single, self-contained room: its schematic and the components placed inside it (doors, spawn/boss/mob markers, bounding box). A DungeonBlueprint is dungeon-level: it names a pool of rooms, the generation limits that turn that pool into a procedural layout, and the mobs/loot tied to that layout. Rooms load first, then dungeon blueprints — a dungeon blueprint resolves its room references against the room set that was just loaded. See Overview for how a blueprint turns into a live instance.
Room blueprints (rooms/)
Field | Type | Read by AstralDungeons | Description |
|---|---|---|---|
| String | Yes | The room's id. This — not the filename — is what every reference to a room uses: |
| String | No | AstralRoomEditor authoring metadata (the gamemode the room was built in). Not read by AstralDungeons. |
| String | No | AstralRoomEditor authoring metadata. Not read by AstralDungeons. |
| Path | Yes | WorldEdit schematic path (any format |
|
| Yes | Free-form component id → placed component. See Room components. |
Room components AstralDungeons reads
Each entry under components carries a RoomEditor primitive discriminator (component-type: position, typed-position, cuboid, orientation, or oriented-cuboid) plus a semantic type string that AstralDungeons matches on. Only a handful of types are meaningful to AstralDungeons:
| Matched | Where | Required | Purpose |
|---|---|---|---|---|
|
| every room | Yes | The room's block bounds ( |
|
| any room the generator should connect through | 0+, one per doorway | A doorway the grid generator matches doors through. The map key must end in |
|
| start room | Yes, to let players in | Where |
|
| end room | Functionally required | Where the blueprint's |
|
| any room | No | Ambient mob spawn markers. |
Every position-family lookup above only checks instanceof PositionRoomComponent — typed-position (TypedPositionRoomComponent) is a subclass with an extra type-id field that AstralDungeons never reads, so either component-type works for spawn, boss, or mob markers.
Dungeon blueprints (blueprints/)
Field | Type | Description |
|---|---|---|
| String | Unique blueprint identifier. The same string a bridge blueprint must carry for its portal to reach this dungeon, and the id the server-advertisement heartbeat publishes. Duplicates are rejected — see Validation. |
|
| Limits the grid generator uses to turn |
|
| The room pool: start, end, normal rooms, dead-end caps. See |
|
| Room name → set of AstralMobs blueprint ids eligible to spawn there. See |
| String | AstralMobs blueprint id spawned at the end room's |
|
| The two biomes the generated world is painted with. See |
|
| AstralMobs blueprint id → experience awarded on kill. See |
|
| Loot tables keyed by an arbitrary id — currently only |
generation
Field | Type | Description |
|---|---|---|
| int | Lower bound of the random room budget — the number of |
| int | Upper bound of the room budget. Clamped to at least |
| int (blocks) | Size of one grid cell. Every referenced room's bounding box and doors are quantised into |
| int | Upper bound on how many times the same room blueprint may chain parent-to-child in a row while the generator grows a corridor. |
| int | How many full attempts (topology pass + world-space collision check) |
| double, | How strongly growth prefers depth over breadth. Omitting the key yields |
branchiness
At each growth step the generator picks which open doorway to expand from. branchiness is the probability that it picks the newest one rather than the oldest:
Value | Growth | Reads as |
|---|---|---|
| Strict breadth-first — always the oldest open socket | A uniform disc fanning out around the entrance. |
| Always the newest | Long corridors driven out before anything backtracks. |
| Mostly depth, occasional fan-out | Organic. |
0 short-circuits the random draw entirely, so it consumes no randomness — a blueprint that does not set branchiness generates exactly the layouts it did before the option existed, seed for seed.
Raising it changes where the boss lands. The END room claims the open doorway farthest from START by real hop distance over the graph of matched doors, recomputed per layout — not by discovery order — so a socket reached quickly through a cycle the generator closed opportunistically is not mistaken for a deep one. Ties are broken randomly, so the boss does not always land in the same compass direction.
rooms
Field | Type | Description |
|---|---|---|
| String | Room id used as the single |
| String | Room id used as the single |
|
| Room ids available as |
|
| Room ids available as |
|
| Optional relative likelihood per room name during growth. See |
Every id here must resolve against a room's name loaded from rooms/. See Validation for exactly what happens — and what still loads — when one doesn't.
weights
A room not listed — and every room when the map is absent — has weight 1. The weights drive a weighted shuffle of the candidate pool at each placement, not a single weighted draw: the whole pool is ordered, and the generator walks it until one shape actually fits the socket.
That distinction is the point. A socket that only one room in the pool can close must still find it, whatever the weights say — so weight biases preference, never feasibility. Weight 0 does not remove a room from the pool; it sorts it behind every positive-weight room (in random order among the other zeros), making it the "only if nothing else fits" option.
biomes
Field | Type | Description |
|---|---|---|
| Adventure | Biome painted over the whole dungeon world. |
| Adventure | Biome painted over the boss room's footprint. |
The biome is what carries the ambience — fog, sky and water tint, ambient sound and music — so the boss room reads as a different place before its door is opened. Its effect on mob spawn lists is moot here: dungeon worlds run with SPAWN_MOBS off and every mob is placed from a marker.
The boss biome covers the full column over the boss room's X/Z footprint — the whole world height above and below it, not just up to its ceiling. The room it applies to is whichever room carries a boss marker (the same marker the boss spawns from, so the biome cannot land anywhere but the fight); a boss room authored without one falls back to the room named by rooms.end.
Behaviour when a key is absent or wrong:
Block or key omitted →
minecraft:plains, for that key alone. A blueprint may set only one of the two.Both keys equal → the world is painted with one biome and the boss room does not stand out.
Key present but unknown → instance creation fails. This is deliberate: a typo silently painting the wrong ambience is harder to notice than a dungeon that refuses to open.
Because a BiomeProvider is fixed at world creation and needs to know where the boss room landed, the layout is grown before the world is created — see Generating a layout.
room-mobs
Map of room name → set of AstralMobs blueprint ids eligible to spawn in that room. The map key is a room's name — the same id used in rooms.rooms/rooms.caps — not a per-instance concept, so the same room-mobs entry applies to every occurrence of that room blueprint across a generated layout (a NORMAL room can be placed more than once if the generator reuses it while growing).
Spawn locations come from the room's mob markers, activated as participants move through the instance (mobs.detection-range/mobs.activation-range, see Configuration).
Which id is spawned is not random. Each placed room keeps its own set of already-spawned types and the listener takes the first id in the room's room-mobs set that is not in it, so a room cycles through its whole roster before repeating: once every type has been placed the set is cleared and the cycle restarts. Ordering follows the YAML set's iteration order, which is not guaranteed to be the order you wrote.
A room with no room-mobs entry (or an empty set) simply never spawns an ambient mob at its markers. See Dungeon Instances: Mob activation.
boss & mob-experiences
boss is a single AstralMobs blueprint id, spawned once via MobsAPI.spawnMob at the end room's boss marker the first time any participant comes within mobs.activation-range blocks of it (MobListener#trySpawnBoss, gated so it fires at most once per instance). If boss is missing or blank, spawning is skipped and a warning is logged — the marker's location was already required at instance-creation time regardless (see Room components), so a blank boss only means nothing ever appears there. See Dungeon Instances: Boss.
mob-experiences maps an AstralMobs blueprint id to a double experience value. It applies to any dungeon mob a participant kills — MobListener#onMobDeath resolves the killed entity's blueprint id and looks it up here regardless of whether the mob was an ambient room-mobs spawn or the boss. A mob with no entry drops no XP (logged as a warning).
The experience is credited to every alive participant, not just the killer; only the killer's killedMobs counter is incremented. It accumulates on each player's DungeonPlayerData and is paid out on the lobby once they leave the instance. See Loot & Rewards: Mob loot & experience.
components-loot-tables
A map of id → DungeonLootTable. The map is general, but only the pots key is currently consumed, rolled once per right-click on a DecoratedPot block in a room schematic. The full entry shape (entries/minimum/maximum), roll rules, and reward carry-over to the lobby are covered in Loot & Rewards.
Mob drops are not configured here — they come from the AstralMobs blueprint, tagged with the mobs.loot-persistent-data-key payload. See Mob loot.
Generation & the cell grid
Footprints (computed at load)
When a dungeon blueprint loads, BlueprintService converts every room it references into a DungeonRoomBlueprint — its type (START/END/NORMAL/CAP), its extracted doors, and a footprint quantised to that dungeon's generation.cell-size:
The room's bounding box (inclusive block span) is divided by
cell-size, rounded up, giving the footprint's width and length in cells (wCells/lCells).Each door's local anchor — the center of its min/max box, in the room's own schematic-relative coordinates — is mapped to the local cell it falls in and kept alongside the wall edge (
Cardinal) the door was derived to face.
This footprint — not the raw block geometry — is what the grid generator places and rotates.
A room whose block span isn't an exact multiple of cell-size, whose door anchor falls outside the footprint, or whose door isn't roughly centered on the cell edge it's assigned to, is not grid-conforming. This is logged as a [gen] room '<name>' not grid-conforming: ... warning at load time for every such room — it never blocks loading or changes generator behavior; it's a diagnostic for whoever is authoring rooms against that dungeon's cell size.
Generating a layout (instance creation)
At instance creation, DungeonGenerator retries up to generation.max-attempts times, each attempt using a fresh seed:
Topology. A pure, Bukkit-free grid engine (
GridLayout) works entirely in integer cells: it seeds theSTARTroom's footprint at the origin, growsNORMALrooms while the random room budget allows — choosing which open doorway to expand from according tobranchiness, and which room to try there according toweights— then resolves: theENDroom claims the still-open doorway farthest from START by hop distance that it fits, and every remaining open doorway is closed with aCAProom. A placement is only valid under forced exact-edge matching: every footprint cell-edge that abuts a placed room must have a door meeting a door on the neighbour facing back — there are no sealed dead doors.Realization. The accepted topology is converted to world space by chaining door anchors, not grid coordinates: walking the connection tree out from
START, each room is positioned so its connector door's world anchor lands exactly on its already-placed parent's door anchor, so any two connected rooms meet flush at the doorway regardless of footprint size.Collision check. Because door-anchored placement can in principle overlap, the realized layout is checked with a real bounding-box test (a 2-block horizontal inset lets a flush shared wall pass) — a colliding layout is discarded and the whole attempt retried with the next seed.
Loop-edge check. A layout that survives collision is checked for misaligned loop edges — a cycle-closing connection whose two doors do not actually line up in world space. A clean layout is returned immediately; the first layout whose only flaw is misaligned loops is held as a fallback.
If no seed produces a clean layout but a fallback exists, that fallback is used and a warning naming every dead doorway is logged. Connectivity is unaffected — the spanning tree always stitches, so every room is reachable — but those doors open into a wall. Shipping a dungeon with a dead shortcut beats refusing to open it.
Exhausting max-attempts with no fallback at all throws LayoutInfeasibleException, which fails the creation request (logged with the seed, budget and pool size so the run can be replayed; the requesting server replies success=false). See Dungeon Instances: Cross-server creation handshake.
The layout is grown before the world is created, not after: the world's BiomeProvider needs to know where the boss room landed, and a provider is fixed at world creation. The topology search and the marker precompute are pure CPU with no world access, so they run off the main thread; only world creation itself is scheduled onto it.
Validation
BlueprintService.load() clears and fully re-scans both folders every time it runs (enable, or /dungeon reload).
Rooms — a room is genuinely dropped (never enters the room registry, so nothing can reference it):
Its
nameduplicates one already loaded from another file.Its
schematic-pathdoesn't resolve to an existing file on disk.
Dungeon blueprints — the duplicate-id check is a true drop:
A blueprint whose
idduplicates one already loaded is skipped entirely.
The remaining rooms-block checks are logged as warnings that say "Skipping", but they only skip populating that blueprint's processed room pool — the blueprint itself is still registered, still resolvable by /dungeon create, and still advertised on the server heartbeat:
A blueprint whose
rooms.startorrooms.enddoesn't resolve to a loaded room, or whoserooms.roomsorrooms.capsis empty in the YAML, still loads with a completely empty room pool. Nothing surfaces at load time — generation itself requires aSTARTroom in the pool and throwsIllegalStateException("pool has no START room")immediately on the first attempt. That exception is deliberately not caught by the attempt-retry loop (which only retriesLayoutInfeasibleException), so/dungeon createagainst that blueprint fails outright, every time, until the blueprint is fixed and the server reloaded.A blueprint whose
rooms.rooms/rooms.capsset is non-empty in the YAML but contains individual ids that don't resolve still gets itsSTARTandENDroom — only theNORMAL/CAPpool shrinks (each unresolved id is filtered out one at a time, with its own warning; the rest of the set still loads). This can still fail generation if too few rooms/caps remain to place or close every doorway, but as a retriedLayoutInfeasibleException, not an immediate hard failure.
Not caught at load time at all — these throw unhandled exceptions from inside the per-dungeon loop while a dungeon blueprint that references the offending room is being processed. Because nothing in that loop catches them, they abort the rest of load() for every blueprint not yet processed in that call (blueprints already committed in earlier loop iterations stay loaded). On /dungeon reload this surfaces as "Failed to reload dungeon blueprints: ..." to the invoking player; on server start it propagates out of onEnable() entirely.
A room referenced by the dungeon has no
bounding_boxcomponent.A
dungeon_doorcomponent isn't flush against exactly one bounding-box wall, or its map key has no trailing_<number>suffix to sort by.
Finally, two checks aren't part of load() at all — they only fail once a party actually creates or joins an instance of the blueprint (see Room components):
No room in the generated instance has a
boss-typed marker — instance creation throws outright.The start room has no
spawn-id marker — throws when the first player joins the instance.
Hot reload
/dungeon reload (permission dungeons.command + dungeons.reload, dungeon server only) calls BlueprintService.load() again — both folders are fully re-scanned from disk, so blueprint files added, edited, or removed since the last load take effect immediately. It does not affect dungeon instances already running. See Configuration: Reload.