Skip to content

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":

  1. Sub-structure variants (variants) —— stick a weighted-random small template onto a slot in the main blueprint, for "same body, different rooms / different spires."
  2. 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

yaml
# 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 }
FieldDefaultDescription
idSlot identifier, for recognition only.
x / y / zSlot coordinates (relative to the structure origin).
surfacefalsetrue = 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-max0 / 0Vertical 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).
optionsThe 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; when surface:true the landing spot takes that x/z's real surface Y, otherwise origin + (x,y,z); then a [y-min,y-max] random offset is added; finally it's pasted with NONE orientation. Slots whose weight sum ≤0 are skipped.

1.3 Requirements for Candidate Templates

  • The candidate template must 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…

text
ancient_keep (main keep)
 └─ variant slot → corner_tower (corner tower)         ← level 1
      └─ corner_tower's variant slot → banner          ← level 2

A double safeguard against stack blowup / server lag:

LimitValueBehavior
Max nesting depth3 levelsVariants deeper than 3 levels are no longer expanded.
Total variants per generation256A 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

yaml
# 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 }
FieldDefaultDescription
max-rooms12At most how many tiles the whole ruin stitches (≥1).
startnoneThe template ID of the start tile; empty = random start from tiles by weight.
tiles[].templateThe template ID the tile corresponds to (must exist, otherwise the tile is skipped + warned).
tiles[].weight1Weighted-random weight.
tiles[].max-count99At most how many times this kind of tile is used.
tiles[].roleroomroom = ordinary room; cap = cap tile (single connector, used to plug the openings left after stitching ends).
tiles[].connectorsemptyThe connector list: { side, x, y, z }. sideNORTH/SOUTH/EAST/WEST, and x/y/z are the port's coordinates relative to this tile's origin.
tiles[].repetition-penalty0Adjacency 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-repeatfalseWhen 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 .nbt structure dimensions directly, no need to hand-fill them in the palette—so a tile template's .nbt must 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

CommandPurpose
/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> dryCompute 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. dry mode 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 · suggested assets/genstruct-dry.png


3. Variants vs Palette: How to Choose

Sub-structure Variants (variants)Tile Palette
Where configuredthe variants section of the ruin's blueprint.ymla standalone palettes/<id>.yml file
Rolemain structure + local random accentsstitch the whole from scratch by connection rules
Randomnesseach slot weighted-draws 1 candidate + vertical jitteroverall layout random (room count / connection paths)
Triggerautomatically stuck on when the main structure naturally generatescurrently manually generated via /qr genstruct
Suitssame body with different rooms / spires / corner towers, terrain-adaptive accentsdungeons / mines / corridor networks and other assembled large ruins
Cap protectionnesting depth 3 / total 256max-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's variants / palettes/*.yml/qr reload.
  • Variants not taking effect: confirm the candidate template exists, the sum of options' weight > 0, and the slot coordinates are within the main structure's bounds.
  • The palette won't stitch: first /qr genstruct <id> dry to see the plan—a tile count of 0 is most likely connector ports not matching or the tile template's .nbt dimensions not readable; referencing a non-existent template warns at /qr reload and skips that tile.

Next Steps