Repository navigation
Feature Track 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:
-
Multiple named NBT templates per kind. Each kind has a
defaultplus 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. -
Per-block randomization sidecars. Each named NBT can have a
<name>.variants.jsonsibling that maps local positions to candidateBlockStatelists; 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 |
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.
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.
/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.
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.
| 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.
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.
-
TrackKind.java— enum of the seven track-side kinds with footprint + subdir helpers -
TrackVariantRegistry.java— per-kind name set, server-start scan, weighted seededpickName(kind, worldSeed, tileIndex) -
TrackVariantWeights.java— per-kindweights.jsonloader with bundled + config tier merge -
TrackVariantStore.java— three-tier NBT store keyed on(kind, name)plusmigrateLegacyPaths() -
TrackVariantBlocks.java— per-(kind, name)*.variants.jsonsidecar; usesVariantState(BlockState + BE NBT) so authored chests/signs/banners round-trip -
TrackSidePlots.java— single source of truth for the track-side editor layout (Xcolumns,Ystack,+Zvariant strides) -
EditorLayout.java— sharedGAP = 5constant for every editor -
TrackEditor.java,PillarEditor.java,TunnelEditor.java— multi-plot editors that iterate the kind's registered names
-
Stairs adjunct has no Y-stack with pillars — adjunct stairs lives at its own
X = 24column rather than under any pillar section. Since v0.52.0 stairs participates as a first-class row in/dt editor tracksand 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 = 0are left as orphan blocks at the prior locations — the new layout doesn't auto-clean them. Use/fillto wipe if they bother you. -
Variants picked independently per pillar section. Themed pillars (matching
top+middle+bottomnames) are out of scope — authors who want a coherent look bias all three kinds' weights toward the same dominant name.