Astral Realms Documentation Help

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

rooms/

RoomBlueprint YAML + the WorldEdit schematics they reference

com.astralrealms.roomeditor.room.RoomBlueprint (AstralRoomEditor)

the room's own name field

blueprints/

DungeonBlueprint YAML

com.astralrealms.dungeons.model.dungeon.DungeonBlueprint

the blueprint's own id field

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

name

String

Yes

The room's id. This — not the filename — is what every reference to a room uses: rooms.start, rooms.end, rooms.rooms, rooms.caps, and room-mobs keys in a DungeonBlueprint. A duplicate name across files is rejected (see Validation).

gamemode

String

No

AstralRoomEditor authoring metadata (the gamemode the room was built in). Not read by AstralDungeons.

layout

String

No

AstralRoomEditor authoring metadata. Not read by AstralDungeons.

schematic-path

Path

Yes

WorldEdit schematic path (any format ClipboardFormats recognises, e.g. .schem), resolved against the dungeon server's data folder — not against rooms/. Loading skips the room if this file doesn't exist.

components

Map<String, RoomComponent>

Yes

Free-form component id → placed component. See Room components.

name: "boss_room" gamemode: "SURVIVAL" layout: "dungeons" schematic-path: "schematics/boss_room.schem" components: bounding_box: component-type: "cuboid" type: "bounding_box" min: { x: 0, y: 0, z: 0 } max: { x: 15, y: 8, z: 15 } dungeon_door_1: component-type: "oriented-cuboid" type: "dungeon_door" min: { x: 7, y: 1, z: 0 } max: { x: 8, y: 3, z: 0 } direction: "NORTH" boss: component-type: "position" type: "boss" position: { x: 8.5, y: 1.0, z: 8.5 } yaw: 0.0 pitch: 0.0 mob_1: component-type: "position" type: "mob" position: { x: 3.0, y: 1.0, z: 3.0 } yaw: 0.0 pitch: 0.0

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:

type

Matched component-type (s)

Where

Required

Purpose

bounding_box

cuboid or oriented-cuboid (anything implementing CuboidComponent)

every room

Yes

The room's block bounds (RoomUtils.boundingBox), used to derive door facings and the room's footprint. A room with none throws while the dungeon blueprint referencing it loads — not caught as a graceful skip (see Validation).

dungeon_door

oriented-cuboid only

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 _<number> (e.g. dungeon_door_1, dungeon_door_2) — doors are sorted by that trailing number, and a non-numeric suffix throws. The door's facing (Cardinal) is derived, not read from the authored direction field: the door's box must be flush against exactly one of the bounding box's four walls (aligned on X or Z, never both, never neither) or extraction throws.

spawn (matched by map key, not type)

position or typed-position (anything instanceof PositionRoomComponent)

start room

Yes, to let players in

Where DungeonService#registerPlayer teleports each participant on join — looked up by the literal component id spawn. Missing it throws when the first player joins the instance, not at load time.

boss (matched by type)

position or typed-position

end room

Functionally required

Where the blueprint's boss mob spawns once a participant is within mobs.activation-range (Configuration). If no room in the generated instance has one, instance creation fails outright.

mob (matched by type)

position or typed-position

any room

No

Ambient mob spawn markers. MobListener scans for these as players move; which mob blueprint actually spawns at one is picked from the dungeon blueprint's room-mobs map for that room, never from anything on the marker itself.

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

id

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.

generation

Generation

Limits the grid generator uses to turn rooms into a procedural layout. See generation.

rooms

Rooms

The room pool: start, end, normal rooms, dead-end caps. See rooms.

room-mobs

Map<String, Set<String>>

Room name → set of AstralMobs blueprint ids eligible to spawn there. See room-mobs.

boss

String

AstralMobs blueprint id spawned at the end room's boss marker. See boss & mob-experiences.

biomes

Biomes

The two biomes the generated world is painted with. See biomes.

mob-experiences

Map<String, Double>

AstralMobs blueprint id → experience awarded on kill. See boss & mob-experiences.

components-loot-tables

Map<String, DungeonLootTable>

Loot tables keyed by an arbitrary id — currently only pots is consumed (the reward-pot mechanic, not a RoomEditor component type). See Loot & Rewards.

id: "crypt_of_ashes" generation: min-rooms: 6 max-rooms: 10 cell-size: 16 max-consecutive-identical-room: 2 max-attempts: 20 branchiness: 0.7 rooms: start: "entrance_hall" end: "boss_room" rooms: - "corridor" - "mob_room" - "treasure_room" caps: - "dead_end_small" weights: corridor: 3 treasure_room: 1 biomes: rooms: "minecraft:deep_dark" boss-room: "minecraft:soul_sand_valley" room-mobs: mob_room: - "skeleton_warrior" - "zombie_brute" corridor: - "skeleton_warrior" boss: "ashen_lich" mob-experiences: skeleton_warrior: 5.0 zombie_brute: 8.0 ashen_lich: 250.0 components-loot-tables: pots: minimum: 1 maximum: 3 entries: { }

generation

Field

Type

Description

min-rooms

int

Lower bound of the random room budget — the number of NORMAL rooms the generator tries to grow. Clamped to at least 1.

max-rooms

int

Upper bound of the room budget. Clamped to at least min-rooms's (post-clamp) value, so a misconfigured max-rooms < min-rooms can't produce an invalid range. DungeonGenerator rolls a budget uniformly at random in [min-rooms, max-rooms] once per creation attempt.

cell-size

int (blocks)

Size of one grid cell. Every referenced room's bounding box and doors are quantised into cell-size-block cells once, when the dungeon blueprint loads. See Footprints.

max-consecutive-identical-room

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. <= 0 disables the limit.

max-attempts

int

How many full attempts (topology pass + world-space collision check) DungeonGenerator makes, each with a fresh seed, before giving up and failing instance creation with LayoutInfeasibleException.

branchiness

double, [0,1]

How strongly growth prefers depth over breadth. Omitting the key yields 0 — the behaviour every pre-existing blueprint keeps. A value outside [0,1] fails the generation run.

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

0 (default)

Strict breadth-first — always the oldest open socket

A uniform disc fanning out around the entrance.

1

Always the newest

Long corridors driven out before anything backtracks.

0.6–0.8

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

start

String

Room id used as the single START room — the generator's anchor point, and where the spawn marker lives.

end

String

Room id used as the single END (boss) room — where the boss marker lives.

rooms

Set<String>

Room ids available as NORMAL rooms the generator can grow the layout with. Must be non-empty in the YAML.

caps

Set<String>

Room ids available as CAP rooms — dead ends used to close any doorway the generator can't otherwise connect. Must be non-empty in the YAML.

weights

Map<String, Integer>

Optional relative likelihood per room name during growth. See weights.

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.

rooms: weights: corridor: 5 # five times as likely to be tried first as an unlisted room treasure_room: 1 grand_hall: 0 # only placed when nothing else closes the doorway

biomes

biomes: rooms: "minecraft:deep_dark" boss-room: "minecraft:soul_sand_valley"

Field

Type

Description

rooms

Adventure Key

Biome painted over the whole dungeon world.

boss-room

Adventure Key

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:

  1. 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).

  2. 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:

  1. Topology. A pure, Bukkit-free grid engine (GridLayout) works entirely in integer cells: it seeds the START room's footprint at the origin, grows NORMAL rooms while the random room budget allows — choosing which open doorway to expand from according to branchiness, and which room to try there according to weights — then resolves: the END room claims the still-open doorway farthest from START by hop distance that it fits, and every remaining open doorway is closed with a CAP room. 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.

  2. 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.

  3. 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.

  4. 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 name duplicates one already loaded from another file.

  • Its schematic-path doesn't resolve to an existing file on disk.

Dungeon blueprints — the duplicate-id check is a true drop:

  • A blueprint whose id duplicates 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.start or rooms.end doesn't resolve to a loaded room, or whose rooms.rooms or rooms.caps is empty in the YAML, still loads with a completely empty room pool. Nothing surfaces at load time — generation itself requires a START room in the pool and throws IllegalStateException("pool has no START room") immediately on the first attempt. That exception is deliberately not caught by the attempt-retry loop (which only retries LayoutInfeasibleException), so /dungeon create against that blueprint fails outright, every time, until the blueprint is fixed and the server reloaded.

  • A blueprint whose rooms.rooms/rooms.caps set is non-empty in the YAML but contains individual ids that don't resolve still gets its START and END room — only the NORMAL/CAP pool 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 retried LayoutInfeasibleException, 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_box component.

  • A dungeon_door component 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.

Last modified: 25 September 2026