Skip to content

Feature Track Variants

Brennan Hatton edited this page Apr 30, 2026 · 8 revisions

Track-side template variants

Since: v0.46.0 — PR #60 v0.50.0 — PR #64 — variant-place hotkey moved from sneak to a rebindable Forge KeyMapping (default Z). Track-side per-block variant authoring now uses Hold Z + right-click instead of sneak + right-click; sneak no longer triggers variant placement. v0.51.0 — PR #65 — the editor command menu now exposes a [-] Weight (N) [+] Triple row over track-side plots (track tile, pillar sections, tunnel sections/portals). The underlying TrackVariantWeights storage existed since v0.46.0 — only the menu and a CLI verb were missing. New verb: /dungeontrain editor tracks weight <kind> <name> <inc|dec|0..100>. v0.52.0 — PR #66 — stair adjunct is now a first-class row in the Tracks category. /dt editor tracks stamps the stair plot alongside pillars / tunnels, the HUD recognises the plot, the editor menu exposes Save / Reset / New for stairs, and /dt save all iterates the stair row. v0.54.0 — PR #68 — ships two new in-game-saved templates (tracks/sleepside.nbt, pillars/adjunct_stairs/ladder.nbt) and bundled weights.json defaults for both kinds (tracks/weights.json = {"default": 21}, pillars/adjunct_stairs/weights.json = {"default": 20}). Also fixes a bug where the editor menu's [+]/[-] Weight row over an adjunct plot wrote bogus entries like {"adjunct_stairs": 20} to weights.json — VariantOverlayRenderer.modelNameFor() was missing the AdjunctModel case and fell through to the kind id. See Feature-Command-Menu § v0.54.0 fix. v0.59.0 — PR #74 — bundled track-side variants are now auto-discovered. Before v0.59.0, TrackVariantRegistry only scanned config/dungeontrain/<kind>/*.nbt — the bundled tracks/sleeperless.nbt, tracks/sleepside.nbt, and pillars/adjunct_stairs/ladder.nbt shipped in the jar (since v0.46.0 / v0.54.0) but the registry never read them, so the in-world block-variant menu (Z) over a track or stair plot returned an empty list. Adds BundledNbtScanner (a shared classpath walker that handles file:, jar:, and Forge's union: URL schemes) and wires it into TrackVariantRegistry.reload() for every TrackKind. Server start now logs Track variant registry loaded — N bundled + M custom names across 7 kinds. The same scanner replaces the legacy manifest.json/customs.json discovery in CarriagePartRegistry, CarriageVariantRegistry, and CarriageContentsRegistry — drop a new .nbt into src/main/resources/data/dungeontrain/... and it auto-registers on the next server start. v0.82.0 — PR #116 — track-side variant sidecars now auto-promote into the bundled resource tree on explicit save (DevMode). Before v0.82.0, TrackVariantBlocks.saveToSource existed but was never called, so Hold-Z + right-click edits landed in run/config/dungeontrain/<kind>/<name>.variants.json (gitignored) and the bundled tree never received them. The wiring runs only on /dungeontrain save (not on every variant edit), inside the existing DevMode gate, in its own inner try/catch so a sidecar-write failure logs a warning but does not fail the parallel NBT promote. Empty sidecars delete the source-tree file. Ships the first author-promoted track sidecar — tracks/sleepside.variants.json (8 entries, two lock groups). Wired in TrackEditor.save and both PillarEditor.save overloads (section + adjunct).

Every track-side template — the open-air track tile, the three pillar sections (top / middle / bottom), the stairs adjunct, and the two tunnel kinds (section / portal) — supports two layers of authorable variation that mirror the carriage system:

  1. Multiple named NBT templates per kind. Each kind has a default plus any number of authored named variants (stone, cracked, mossy, …). The generator picks one weighted-random per instance, deterministically seeded on (worldSeed, instanceIndex, kind.id().hashCode()) so re-walking the same chunks always picks the same variants.
  2. Per-block randomization sidecars. Each named NBT can have a <name>.variants.json sibling that maps local positions to candidate BlockState lists; the generator picks one per cell with the same deterministic seed. Hold Z + right-click in the editor authors entries directly (default Z, rebindable via Esc → Options → Controls → Dungeon Train).
Track-side kind NBT path Footprint
tile tracks/<name>.nbt 4 × 2 × dims.width()
pillar_top pillars/top/<name>.nbt 1 × 4 × dims.width()
pillar_middle pillars/middle/<name>.nbt 1 × 1 × dims.width()
pillar_bottom pillars/bottom/<name>.nbt 1 × 3 × dims.width()
adjunct_stairs pillars/adjunct_stairs/<name>.nbt 3 × 8 × 3
tunnel_section tunnels/section/<name>.nbt 10 × 14 × 13
tunnel_portal tunnels/portal/<name>.nbt 10 × 14 × 13

Editor layout

All editor plots sit at Y = 250+ and follow a uniform 5-block gap rule (EditorLayout.GAP = 5). Plots are arranged on a single grid:

Axis Role
+X Categories: track tile column at X=0, tunnels at X=9, stairs at X=24, pillars at X=32.
+Y Within a category, sub-kinds stack vertically. Tunnels: SECTION at Y=250, PORTAL at Y=269. Pillars: BOTTOM at Y=250, MIDDLE at Y=258, TOP at Y=264.
+Z Variants of one kind/sub-kind lay out side-by-side. default sits at the kind's baseline Z; subsequent named variants step by footprint.z + 5. The track-side row baseline is Z = 2 × MAX_WIDTH + 2 × GAP = 74 so it clears the carriage row (Z=0..31) and the contents row (Z=37..68) at any carriage width.

The carriage and contents rows now also use the 5-block gap (step = dims.length() + 5), replacing the previous fixed step of 20.

HUD

The status overlay shows the kind and variant name of whatever plot the player is standing on:

  • Track tile → Tracks / track / default
  • Pillar bottom → Tracks / pillar / bottom / stone
  • Tunnel portal → Tracks / tunnel / portal / default
  • Stairs adjunct → Tracks / stairs / default

Weight line works for every track-side kind (reads TrackVariantWeights.weightFor(kind, name)); Hold Z + right-click adds per-block variants in any track-side plot, same end-rod outline particles + hover candidates as the carriage editor.

Authoring flow

/dungeontrain editor tracks                            # walk the whole grid
/dungeontrain editor tracks new pillar_top stone       # clone the variant you're standing on as "stone"
... build ...
/dungeontrain save                                     # writes through the (kind, name) you're standing on
/dungeontrain editor tracks reset pillar_top           # deletes the variant you're standing on, wipes its plot blocks, teleports back to default (errors on default)

/dungeontrain editor tracks new <kind> <name> clones whatever variant the player is currently on (defaults to default if they're outside any plot) and teleports them to the new plot at the next +Z slot.

Runtime

Each track-side generator picks a registry-weighted name per instance and applies the per-block sidecar after stamping:

Generator Instance index Sidecar pass
TrackGenerator.placeTrackColumn floorDiv(worldX, TILE_LENGTH) Inline — sidecar resolved before setBlockSilent
TrackGenerator.placePillarSlice worldX of the pillar centerline Inline per cell in stampSliceCell
TrackGenerator.placeStairsBesidePillar centerX of the anchoring pillar Post-pass after placeInWorld; LEFT_RIGHT mirror handled
TunnelTemplate.applyTunnelSidecar Tunnel section's origin X Post-pass after placeInWorld; FRONT_BACK mirror handled for exit portals

Block-entity NBT round-trips through the variants v2 format: chests / signs / banners with content survive the variant pick.

Storage

Tier Location Written by
1 — per-install override config/dungeontrain/<kind subdir>/<name>.nbt (+ optional <name>.variants.json sidecar) /dungeontrain save while standing on the plot; sidecar additionally accumulates per Hold-Z + right-click edit
2 — bundled mod default /data/dungeontrain/<kind subdir>/<name>.nbt (+ optional <name>.variants.json sidecar) inside the jar /save while devmode on, or /promote — since v0.82.0 the variant sidecar is promoted alongside the NBT; an empty sidecar deletes the source-tree file
3 — fallback Hardcoded palette per kind (or no-op for adjuncts/tunnels) Implicit — empty registry resolves to default only

Per-kind weights live in config/dungeontrain/<kind subdir>/weights.json (same flat schema as carriage weights). Missing or empty = uniform pick across the kind's registered names.

Migration

On first server start after upgrading to v0.46.0, TrackVariantStore.migrateLegacyPaths() moves any pre-existing single-file overrides into the new <kind>/default.nbt layout:

Legacy path New path
tracks/track.nbt tracks/default.nbt
pillars/pillar_top.nbt pillars/top/default.nbt
pillars/pillar_middle.nbt pillars/middle/default.nbt
pillars/pillar_bottom.nbt pillars/bottom/default.nbt
pillars/adjunct_stairs.nbt pillars/adjunct_stairs/default.nbt
tunnels/section.nbt tunnels/section/default.nbt
tunnels/portal.nbt tunnels/portal/default.nbt

Idempotent — runs every server start, no-ops once migration is complete. The bundled NBTs were renamed to match in the same release.

Files

Known limitations

  • Stairs adjunct has no Y-stack with pillars — adjunct stairs lives at its own X = 24 column rather than under any pillar section. Since v0.52.0 stairs participates as a first-class row in /dt editor tracks and the editor menu (Save / Reset / New / Remove all work when standing in the stair plot); the layout still requires walking from the pillar column to the stairs column to switch kinds.
  • Plot positions shifted from earlier preview builds. If you tested a 0.41.x preview, old plot positions at Z = 0 are left as orphan blocks at the prior locations — the new layout doesn't auto-clean them. Use /fill to wipe if they bother you.
  • Variants picked independently per pillar section. Themed pillars (matching top + middle + bottom names) are out of scope — authors who want a coherent look bias all three kinds' weights toward the same dominant name.

Clone this wiki locally