Procedural Generation (Palette / Sub-structure Variants)
Previous: Natural Generation and Pre-loading (Generation Director / Density / Regen) · Next: Realms and Keystones Related: Ruin Template · Structure Files · Blueprints and Objectives
What came before was about "a whole structure landing as one piece." This page covers two procedural capabilities that let ruins be "assembled, different every time":
- Sub-structure variants (variants) —— stick a weighted-random small template onto a slot in the main blueprint, for "same body, different rooms / different spires."
- Tile Palette —— automatically stitch multiple small structures (tiles) by connection rules into a large ruin, the whole thing procedurally generated.
Both are sources of "the same ruin looking a little different each time," but with different roles: variants are the main structure + local random accents, the palette stitches the whole thing from scratch by rules.
🖼️ [Image placeholder] Left: a main tower + a random spire (variant); right: a dungeon automatically stitched from several room tiles (palette) · suggested
assets/procedural-overview.png
1. Sub-structure Variants (variants)
1.1 What It Is
After the main structure generates, QR will, at each variant slot declared in the blueprint, draw one weighted-randomly from a set of candidate templates and stick it on (NONE orientation). Used to do:
- The same tower, whose spire is randomly one of A / B / C;
- The same crypt, whose certain room is randomly a vault / trap / empty room;
- Terrain-adaptive accents like "tower base follows terrain undulation" and "sub-structures staggered in height."
Variants are written in that ruin's blueprint.yml (all coordinates are relative to the structure origin, the structure's minimum corner = 0,0,0); for the full blueprint picture see Blueprints and Objectives.
1.2 Fields
# blueprint.yml fragment
variants:
- id: top # slot identifier (for recognition only)
x: 8
y: 20
z: 8 # slot coordinates relative to the structure origin
surface: false # true = stick to the real surface height of that x/z (ignore y), for "tower base follows terrain undulation"
y-min: 0 # vertical random reposition: add a random [y-min, y-max] blocks to the final Y
y-max: 0 # both 0 = no jitter; e.g. y-min:-2 y-max:3 for "staggered heights"
options: # draw one weighted-randomly by weight and stick it on (NONE orientation)
- { template: spire_a, weight: 30 }
- { template: spire_b, weight: 20 }| Field | Default | Description |
|---|---|---|
id | — | Slot identifier, for recognition only. |
x / y / z | — | Slot coordinates (relative to the structure origin). |
surface | false | true = ignore y and stick the variant to the real surface height of that x/z (for "tower base / pillars follow terrain undulation"). If the surface is too low (close to the world bottom), the slot is skipped. |
y-min / y-max | 0 / 0 | Vertical random reposition: add a random [y-min, y-max] blocks to the final Y. Both 0 = no jitter; e.g. y-min:-2 y-max:3 for staggered heights (matching other structure plugins' VerticalRepositioning). |
options | — | The candidate template list, { template, weight }. Draw one by weighted roulette by weight and stick it on. |
⚙ Drawing and placement: each slot does a roulette draw of one candidate by the sum of
options'weight; whensurface:truethe landing spot takes that x/z's real surface Y, otherwiseorigin + (x,y,z); then a[y-min,y-max]random offset is added; finally it's pasted withNONEorientation. Slots whoseweightsum ≤0 are skipped.
1.3 Requirements for Candidate Templates
- The candidate
templatemust exist (e.g.templates/spire_a/structure.nbt). - It's recommended that the candidate template itself have
generation.enabled: false, so it's used only as a variant and doesn't take part in natural generation on its own. - A variant should land within the main structure's bounds—any part beyond the main structure's bounding box cannot be restored by
/qr remove/ fade snapshot restoration (the snapshot only covers the main structure's volume).
1.4 Recursive Nesting (main keep → corner tower → banner)
A variant sub-template's own blueprint.variants is processed recursively: the main keep sticks on corner towers, the corner towers stick on banners…
ancient_keep (main keep)
└─ variant slot → corner_tower (corner tower) ← level 1
└─ corner_tower's variant slot → banner ← level 2A double safeguard against stack blowup / server lag:
| Limit | Value | Behavior |
|---|---|---|
| Max nesting depth | 3 levels | Variants deeper than 3 levels are no longer expanded. |
| Total variants per generation | 256 | A single ruin generation sticks on at most 256 variants; the excess is truncated + console-warned (a lifesaver when mutual references cause bloat). |
Caution
Two templates referencing each other in their variants get truncated and warned by the depth-3 / total-256 caps—it won't bloat infinitely and freeze the server, but this is a configuration error and circular references should be avoided.
🖼️ [Image placeholder] A recursive-variant render of a main keep + random corner towers at the four corners + random banners atop the corner towers · suggested
assets/variants-nested.png
2. Tile Palette
2.1 What It Is
A palette is a procedural generator that automatically stitches several small structures (tiles) into a large ruin by connector rules. Each tile is an ordinary template (with a .nbt), and tiles dock together via connectors—starting from the start tile, it keeps hanging on tiles whose ports match along the open connectors, until the room count reaches the cap, then plugs the remaining openings with "cap tiles."
Suited for ruins like dungeons / mines / corridor networks, the "rooms + corridors stitched together, with a different layout each time" kind.
Note
A palette lives under plugins/QinhRuins/palettes/, where one .yml = one palette, and the filename (without extension) = the palette ID. The templates referenced by tiles must already exist in templates/.
2.2 Palette Fields
# palettes/dungeon.yml —— the filename dungeon is the palette ID
max-rooms: 12 # at most how many tiles to stitch (≥1)
start: hub_room # the template ID of the start tile (empty = random start from tiles by weight)
tiles:
- template: hub_room # the template ID the tile corresponds to (templates/hub_room/)
weight: 10 # weighted-random weight (default 1)
max-count: 1 # at most how many times this kind of tile is used (default 99)
role: room # role: room (default) / cap (cap tile)
connectors: # connectors: side + port coordinates relative to this tile's origin
- { side: NORTH, x: 3, y: 0, z: 0 }
- { side: SOUTH, x: 3, y: 0, z: 6 }
- { side: EAST, x: 6, y: 0, z: 3 }
- { side: WEST, x: 0, y: 0, z: 3 }
- template: corridor
weight: 20
connectors:
- { side: WEST, x: 0, y: 0, z: 1 }
- { side: EAST, x: 3, y: 0, z: 1 }
- template: dead_end # cap tile: single connector, role=cap
role: cap
connectors:
- { side: WEST, x: 0, y: 0, z: 1 }| Field | Default | Description |
|---|---|---|
max-rooms | 12 | At most how many tiles the whole ruin stitches (≥1). |
start | none | The template ID of the start tile; empty = random start from tiles by weight. |
tiles[].template | — | The template ID the tile corresponds to (must exist, otherwise the tile is skipped + warned). |
tiles[].weight | 1 | Weighted-random weight. |
tiles[].max-count | 99 | At most how many times this kind of tile is used. |
tiles[].role | room | room = ordinary room; cap = cap tile (single connector, used to plug the openings left after stitching ends). |
tiles[].connectors | empty | The connector list: { side, x, y, z }. side ∈ NORTH/SOUTH/EAST/WEST, and x/y/z are the port's coordinates relative to this tile's origin. |
tiles[].repetition-penalty | 0 | Adjacency repetition penalty: when an identical tile is already next to a candidate spot, lower this tile's draw weight by the number of identical neighbors (higher = less clustering). Avoids "a long run of the same room". |
tiles[].no-repeat | false | When true, this tile is forbidden from being adjacent to its own kind (enforced for both connection and capping) — harder than a penalty. |
⚙ Tile dimensions are read automatically: each tile's footprint dimensions are computed by QR reading the corresponding template's
.nbtstructure dimensions directly, no need to hand-fill them in the palette—so a tile template's.nbtmust load properly.
2.3 The Stitching Algorithm (how it grows)
1. Start use the tile specified by start, or random-draw one by weight if unspecified, placed at the origin (0,0,0)
2. Queue openings register all the start tile's connectors as "open openings"
3. Repeatedly dock randomly take one from the open openings → find a tile that "has a reverse connector, hasn't reached max-count, and doesn't overlap placed tiles" to dock on
→ the new tile's remaining connectors keep being queued, until the tile count reaches max-rooms or there are no dockable openings
4. Cap the remaining open openings are plugged one by one with role=cap single-connector tiles (only plugged if the port matches and there's no overlap)- No overlap: during stitching a bounding-box check is done, and a candidate whose docking would overlap a placed tile is skipped directly.
- Automatic backtracking: when it hits a dead end (an opening with no matching tile), the planner steps back and tries another option before continuing — giving better connectivity and fewer dead-ends. Backtracking has a step budget (scales with
max-rooms) so it never loops forever. No configuration needed. - Reproducible: the same palette + the same seed = the same layout (driven by the same random seed).
🔑 A palette stitches "the whole structure body itself." It differs from sub-structure variants: variants are "main structure + local accents," the palette is "stitching the whole from scratch by connection rules."
2.4 Commands: Preview and Generate
| Command | Purpose |
|---|---|
/qr genstruct <palette> | Stitch with a random seed under your feet and actually place it, reporting how many tiles were placed / how many were planned / the seed used. |
/qr genstruct <palette> dry | Compute the plan only, don't place: list each tile's offset coordinates and dimensions (see whether the layout is reasonable, without touching the world). |
/qr genstruct <palette> <seed> | Stitch and place with a specified seed (the same seed reproduces the same layout, handy for debugging / replication). |
⚙ The mode parameter is one of three:
dry= dry-run preview; pure number = treated as a seed for reproduction; anything else / empty = use a system random seed.drymode uses a temporary seed to compute the plan only, without writing blocks.
🖼️ [Image placeholder] The tile-plan list output by
/qr genstruct dungeon dry+ a top-down view of the actually stitched dungeon · suggestedassets/genstruct-dry.png
3. Variants vs Palette: How to Choose
| Sub-structure Variants (variants) | Tile Palette | |
|---|---|---|
| Where configured | the variants section of the ruin's blueprint.yml | a standalone palettes/<id>.yml file |
| Role | main structure + local random accents | stitch the whole from scratch by connection rules |
| Randomness | each slot weighted-draws 1 candidate + vertical jitter | overall layout random (room count / connection paths) |
| Trigger | automatically stuck on when the main structure naturally generates | currently manually generated via /qr genstruct |
| Suits | same body with different rooms / spires / corner towers, terrain-adaptive accents | dungeons / mines / corridor networks and other assembled large ruins |
| Cap protection | nesting depth 3 / total 256 | max-rooms / each tile's max-count |
Tip
The two can be stacked: a tile template stitched by a palette can itself carry blueprint.variants, so "each stitched room gets random accents inside too."
4. After You're Done
- Changed a
blueprint.yml'svariants/palettes/*.yml→/qr reload. - Variants not taking effect: confirm the candidate
templateexists, the sum ofoptions'weight> 0, and the slot coordinates are within the main structure's bounds. - The palette won't stitch: first
/qr genstruct <id> dryto see the plan—a tile count of 0 is most likely connector ports not matching or the tile template's.nbtdimensions not readable; referencing a non-existent template warns at/qr reloadand skips that tile.
Next Steps
- The full picture of
variantsplus spawns / objectives / traps in the blueprint → Blueprints and Objectives - How to make tile / variant candidate templates (structure body / marker blocks / terrain blending) → Structure Files
- Scheduling these ruins "where and how often one appears" → Natural Generation and Pre-loading
- All commands → Command Reference