bgen/godot/README.md
saarsena@gmail.com e45f121fb9 init
2026-04-16 21:04:50 -04:00

330 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](#installation)
- [Project layout](#project-layout)
- [Class `BrogueGen`](#class-broguegen)
- [Returned dictionary schema](#returned-dictionary-schema)
- [Data structures](#data-structures)
- [Enums](#enums)
- [Usage pattern](#usage-pattern)
- [FOV reference (`BrogueFOV`)](#fov-reference)
- [Determinism guarantees](#determinism-guarantees)
- [Build and rebuild](#build-and-rebuild)
- [Troubleshooting](#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 | 126 | influences liquid type weights |
```gdscript
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](#terrain-enum-terrain-byte). |
| `liquid` | `PackedByteArray` | same layout. Values in [liquid enum](#liquid-enum). Meaningful only when `terrain[i] == T_LIQUID`. |
| `surface` | `PackedByteArray` | semantic marker byte. Values in [marker enum](#marker-enum). `MK_NONE` for un-marked cells. |
| `flags` | `PackedInt32Array` | bitmask of [cell flags](#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](#room-dictionary). |
| `machines` | `Array[Dictionary]` | see [machine dictionary](#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"]`:
```gdscript
{
"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"]`:
```gdscript
{
"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:
```gdscript
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.
```gdscript
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.
```gdscript
# 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:
```gdscript
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:
```bash
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:
```bash
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:
```bash
godot --display-driver x11 --path /path/to/your/project
```