bgen/godot
saarsena@gmail.com 5235b5bb22 feat: discrete party controller + aligned stair pairs (PR 2)
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>
2026-04-18 14:00:53 -04:00
..
godot-cpp@60b5a4196d init 2026-04-16 21:04:50 -04:00
src feat: discrete party controller + aligned stair pairs (PR 2) 2026-04-18 14:00:53 -04:00
brogue_gen.gdextension init 2026-04-16 21:04:50 -04:00
README.md init 2026-04-16 21:04:50 -04:00
SConstruct feat: 3D blobber dungeon generator (PR 1) 2026-04-18 13:24:27 -04:00

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

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 126 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 1255 inside a room, 0 elsewhere.
machine_id PackedByteArray 1255 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 seed argument 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:

  1. addons/brogue_gen/brogue_gen.gdextension exists in the project.
  2. addons/brogue_gen/bin/libbrogue_gen...so exists and matches the path in the manifest.
  3. The project has been opened in the editor at least once (this triggers the extension scan).
  4. 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