Adds the Wizardry/M&M core loop: you walk cell-by-cell with 90° turns and descend stairs between levels that actually line up. C side: - pipeline.c: after per-level 2D generation, a link_stairs() pass replaces the randomly-placed down/up stairs with aligned pairs (room cells preferred). Bottom level loses its down-stair; top level keeps the up-stair as the entry point. - dungeon_to_dict.cpp: expose sizeof(cell3d_t) as "cell_stride" so GDScript can index raw cell bytes without hardcoding layout. Godot side: - scripts/blobber_party.gd: reads cell3d_t bytes directly for wall queries, tweens position/rotation on step/turn, swaps level when stair cell is activated. - scripts/dungeon_builder.gd: now hands the generated Dictionary to a party node via `party_path` and groups mesh instances under a "Meshes" child for clean regeneration. - scenes/demo_blobber.tscn: FlyCamera replaced with a Party node (script-driven) holding a child Camera3D. num_levels=3 by default. Still deferred to later PRs: the full port/retirement of src/gen/, and a standalone plan.c/h module (linkage is currently inlined in pipeline.c with just StairPair-equivalent data). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| godot-cpp@60b5a4196d | ||
| src | ||
| brogue_gen.gdextension | ||
| README.md | ||
| SConstruct | ||
brogue_gen — Godot GDExtension API Reference
Brogue-style dungeon generator for Godot 4.3+. Pure C99 generator under the hood; Godot-native Dictionary interface on top.
Scope. Map generation only. One class, one method. Call BrogueGen.generate(seed, depth) and get back everything you need to spawn a level: terrain grid, liquids, markers, rooms, machines, chokepoints, stairs. Gameplay is yours to write.
Table of contents
- Installation
- Project layout
- Class
BrogueGen - Returned dictionary schema
- Data structures
- Enums
- Usage pattern
- FOV reference (
BrogueFOV) - Determinism guarantees
- Build and rebuild
- Troubleshooting
Installation
Drop the addons/brogue_gen/ directory into any Godot 4.3+ project:
your_project/
└── addons/
└── brogue_gen/
├── brogue_gen.gdextension ← registered by Godot scan
└── bin/
└── libbrogue_gen.linux.template_debug.x86_64.so
Re-open the project once so Godot scans and registers the extension. BrogueGen then appears in Create New Node and class_name auto-complete.
Compatibility: The .gdextension manifest declares compatibility_minimum = "4.3". The extension is built against godot-cpp 4.5 and loads in any Godot 4.3+, including 4.6.
Linux x86_64 only for now. Windows / macOS require building additional targets and adding paths to the [libraries] section of the .gdextension manifest.
Project layout
brogue-genesis/
├── src/ pure C99 generator (no Godot, no SDL)
│ └── gen/ room carving, lakes, machines, chokepoints, ...
├── godot/ GDExtension wrapper
│ ├── godot-cpp/ submodule, branch 4.5
│ ├── src/
│ │ ├── register_types.* module init
│ │ ├── brogue_gen.* BrogueGen class
│ │ └── grid_to_dict.* shared grid → Dictionary serializer
│ ├── SConstruct build script
│ └── brogue_gen.gdextension source-of-truth manifest
└── demo/ reference Godot 4.6 project
├── addons/brogue_gen/ ← build output
│ ├── brogue_gen.gdextension
│ └── bin/libbrogue_gen...so
├── project.godot
├── scenes/
└── scripts/
Build with make godot from the repo root.
Class BrogueGen
extends Node
One-shot blocking dungeon generator. Instantiate, call generate(), free.
Typical generation time on the reference machine: under 20 ms per level including machines. Safe to call synchronously in _ready().
generate(seed: int, depth: int) -> Dictionary
Runs the full pipeline (accretion → loops → lakes → wreaths → bridges → walls → stairs → chokepoints → machines) and returns a Dictionary describing the final dungeon.
Parameters:
| name | type | range | notes |
|---|---|---|---|
seed |
int | any 64-bit | same seed + depth always yields identical output |
depth |
int | 1–26 | influences liquid type weights |
var gen := BrogueGen.new()
var grid: Dictionary = gen.generate(2026, 1)
gen.free()
Returned dictionary schema
| key | type | notes |
|---|---|---|
seed |
int |
echoed back |
depth |
int |
1..26 |
width |
int |
always 79 |
height |
int |
always 29 |
terrain |
PackedByteArray |
length width * height, row-major. Index = y * width + x. Values in terrain enum. |
liquid |
PackedByteArray |
same layout. Values in liquid enum. Meaningful only when terrain[i] == T_LIQUID. |
surface |
PackedByteArray |
semantic marker byte. Values in marker enum. MK_NONE for un-marked cells. |
flags |
PackedInt32Array |
bitmask of cell flags. |
room_id |
PackedByteArray |
1–255 inside a room, 0 elsewhere. |
machine_id |
PackedByteArray |
1–255 inside a machine, 0 elsewhere. |
stairs_up |
Vector2i |
always valid grid coords; (-1, -1) only on degenerate seeds. |
stairs_down |
Vector2i |
farthest reachable floor from stairs_up (BFS). |
rooms |
Array[Dictionary] |
see room dictionary. |
machines |
Array[Dictionary] |
see machine dictionary. |
chokepoints |
Array[Vector2i] |
every articulation point in the map graph. |
gate_sites |
Array[Vector2i] |
the "best" chokepoint per pocket — ideal vestibule candidates. Subset of chokepoints. |
Data structures
Room dictionary
Each entry in grid["rooms"]:
{
"id": int, # matches room_id in the cell arrays
"cells": Array[Vector2i], # every cell that belongs to this room
}
Machine dictionary
Each entry in grid["machines"]:
{
"id": int, # matches machine_id in the cell arrays
"interior_cells": Array[Vector2i],
"gate": Vector2i, # the one F_MACHINE_GATE cell; (-1,-1) if unflagged
"markers": Dictionary, # marker_id (int) -> Array[Vector2i]
}
Example:
var m: Dictionary = grid["machines"][0]
for marker_id in m["markers"]:
var positions: Array = m["markers"][marker_id]
match marker_id:
4: # MK_PEDESTAL
for p in positions:
spawn_pedestal(p)
Enums
Terrain enum (terrain byte)
| id | constant | meaning |
|---|---|---|
| 0 | T_NOTHING |
outside the dungeon — not walkable |
| 1 | T_FLOOR |
room floor |
| 2 | T_WALL |
wall |
| 3 | T_DOOR |
door in a room wall |
| 4 | T_CORRIDOR |
corridor between rooms |
| 5 | T_LIQUID |
water / lava / chasm / brimstone — see liquid byte |
| 6 | T_BRIDGE |
walkable span across water/chasm |
| 7 | T_STAIRS_UP |
up-stair |
| 8 | T_STAIRS_DOWN |
down-stair |
T_FLOOR, T_CORRIDOR, T_DOOR, T_BRIDGE, T_STAIRS_UP, T_STAIRS_DOWN are passable.
Liquid enum (liquid byte)
| id | constant | bridges allowed | |
|---|---|---|---|
| 0 | L_NONE |
— | |
| 1 | L_WATER |
yes | shallow levels |
| 2 | L_LAVA |
no | damages walker |
| 3 | L_CHASM |
yes | gameplay-defined fall behavior |
| 4 | L_BRIMSTONE |
no | lowest depths |
Marker enum (surface byte)
Semantic placement markers. The generator stamps these; gameplay interprets them.
| id | constant | typical use |
|---|---|---|
| 0 | MK_NONE |
no marker |
| 1 | MK_CARPET |
decorative floor tint (paint pass) |
| 2 | MK_ALTAR |
item pedestal on an altar |
| 3 | MK_CAGE |
item behind a lock |
| 4 | MK_PEDESTAL |
item on pedestal |
| 5 | MK_STATUE |
decorative statue |
| 6 | MK_TORCH |
point light source |
| 7 | MK_BRAZIER |
point light source (stronger) |
| 8 | MK_CHEST |
item container |
| 9 | MK_SPAWN_MONSTER |
spawn an enemy here |
| 10 | MK_SPAWN_BOSS |
spawn a boss here |
| 11 | MK_KEY_ITEM |
the key that unlocks this machine's parent |
Cell flags
Bitmask in flags[]. Test with (flags[idx] & F_X) != 0.
| bit | value | constant | meaning |
|---|---|---|---|
| 0 | 0x0001 |
F_IN_ROOM |
cell is room floor |
| 1 | 0x0002 |
F_IN_LOOP |
cell was carved by the loop pass |
| 2 | 0x0004 |
F_CHOKEPT |
reserved |
| 3 | 0x0008 |
F_BRIDGE |
cell is a bridge (redundant with T_BRIDGE) |
| 4 | 0x0010 |
F_WREATH |
shoreline cell (adjacent to liquid) |
| 6 | 0x0040 |
F_IN_MACHINE |
cell belongs to a machine |
| 7 | 0x0080 |
F_MACHINE_INTERIOR |
interior (not boundary) |
| 8 | 0x0100 |
F_MACHINE_GATE |
the vestibule / entry |
| 9 | 0x0200 |
F_MACHINE_IMPREGNABLE |
cell is protected from later modification (reserved) |
| 10 | 0x0400 |
F_MACHINE_KEY |
key goes here (gameplay hint) |
Usage pattern
Generate a level, instantiate everything up front, hand off to gameplay.
func build_level(seed: int, depth: int) -> void:
var gen := BrogueGen.new()
var grid: Dictionary = gen.generate(seed, depth)
gen.free()
# Player at the up-stair.
$Player.position = _grid_to_world(grid["stairs_up"])
# Goal marker at the down-stair.
$GoalMarker.position = _grid_to_world(grid["stairs_down"])
# One reward per machine.
for m in grid["machines"]:
for marker_id in m["markers"]:
for cell in m["markers"][marker_id]:
spawn_for_marker(marker_id, cell)
# Monsters proportional to room size.
for room in grid["rooms"]:
var size: int = (room["cells"] as Array).size()
if size > 40 and randf() < 0.6:
spawn_monster(_random_cell(room["cells"]))
FOV reference
demo/scripts/fov.gd ships a reference BrogueFOV class. It is not part of the extension — it's pure GDScript you can copy into your project and modify.
# demo/scripts/fov.gd
class_name BrogueFOV extends RefCounted
static func compute(grid: Dictionary, origin: Vector2i, radius: int) -> PackedByteArray
Symmetric recursive shadowcasting, tile-accurate, O(visible cells). Opaque cells = walls, nothing, and non-water liquids (lava / chasm / brimstone block sight; water does not).
Example:
var vis := BrogueFOV.compute(grid, player_pos, 8)
for y in grid["height"]:
for x in grid["width"]:
if vis[y * grid["width"] + x] == 1:
mark_visible(x, y)
Determinism guarantees
- Same
(seed, depth)input always produces identical output, across runs, platforms, and future minor versions of the plugin. - The C generator uses a PCG32 RNG seeded from the
seedargument only. No platform calls (time(),rand()) are read.
Build and rebuild
From the repo root:
make godot # builds the extension
make godot-clean # removes build artifacts
This runs scons -j$(nproc) platform=linux target=template_debug in godot/. The resulting .so and manifest land together in demo/addons/brogue_gen/ per Godot's plugin convention.
For release builds:
cd godot && scons -j$(nproc) platform=linux target=template_release
To add a new platform (Windows/macOS), add the SCons target + append a new line to brogue_gen.gdextension:
windows.debug.x86_64 = "res://bin/libbrogue_gen.windows.template_debug.x86_64.dll"
Troubleshooting
"Could not find type BrogueGen" / "Cannot get class 'BrogueGen'". Godot has not registered the extension. Check:
addons/brogue_gen/brogue_gen.gdextensionexists in the project.addons/brogue_gen/bin/libbrogue_gen...soexists and matches the path in the manifest.- The project has been opened in the editor at least once (this triggers the extension scan).
- Delete the
.godot/cache directory and re-open if in doubt.
Godot 4.6 crashes on startup (SIGABRT in WaylandThread).
This is a Godot 4.6.1 Wayland bug, not the extension. Launch with X11 instead:
godot --display-driver x11 --path /path/to/your/project