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. Also the id |
|
| 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 |
|
| AstralMobs blueprint id → experience awarded on kill. See |
|
| Loot tables keyed by an arbitrary id — currently only |
|
| Loot tables keyed by AstralMobs blueprint id, same ids as |
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) |
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 |
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.
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). For each activated location, MobListener picks one id from that room's room-mobs set uniformly at random (equal weight per id) and spawns it there via MobsAPI. 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). Experience accumulates on the killer's DungeonPlayerData and is paid out on the lobby once they leave the instance. See Loot & Rewards: Mob loot & experience.
components-loot-tables & mobs-loot-tables
Both are maps of id → DungeonLootTable. mobs-loot-tables is keyed by AstralMobs blueprint id (checked against the same ids as mob-experiences) and rolled on a qualifying kill. components-loot-tables is a general map, but only the pots key is currently consumed, rolled once per 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.
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, then resolves — theENDroom claims the deepest still-open doorway 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.
Exhausting max-attempts without a non-overlapping, fully-connected layout throws LayoutInfeasibleException, which fails the creation request (logged, and the requesting server replies success=false). See Dungeon Instances: Cross-server creation handshake.
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.