Feature Carriage Editor - bh679/dungeon-train-mc GitHub Wiki

In-world carriage template editor

Since: v0.13.0 β€” PR #17 v0.25.0 β€” PR #33 β€” three-tier loader (config β†’ bundled jar resource β†’ hardcoded), /editor devmode + /editor promote for shipping authored templates with the mod, four canonical carriage NBTs bundled by default v0.26.0 β€” PR #34 β€” custom named variants via /editor new <name> [source], rename-on-save via /editor save [new_name] (standard/flatbed protected), dynamic plot allocation, variantForIndex rolling cycle spans built-ins + customs v0.31.0 β€” PR #41 β€” variant blocks: mark a position in a plot with N candidate block states and every generated carriage rolls one of them in, seeded deterministically on (world seed, carriage index, local pos). Shift-right-click authoring, particle outlines on flagged blocks, client-side icon HUD on hover. v0.32.0 β€” PR #42 β€” diagnostic logging for empty carriages (INFO per placement with variant + source + block count, WARN with reason tag per empty result) and the pillar-directory fix that stops pillar_*.nbt from being misregistered as custom carriages β€” see Feature-Track-Generation Β§ Editable pillar section templates for the directory move + migration. v0.35.0 β€” PR #45 β€” empty-space placeholder: a command block added as a variant acts as a sentinel meaning "render as air at spawn time," enabling windows/gaps and "sometimes nothing" effects without authoring a second variant. v0.36.0 β€” PR #46 β€” editor categories + /dt alias + status HUD + unified /save /reset: group the carriage / pillar / tunnel / track editors into Carriages / Tracks / Architecture (coming soon), shorten every /dungeontrain command to /dt, show a top-centre Editor: <Category> / <Model> bar (with a yellow [DEV] badge while devmode is on), and add context-aware /dt save [default] / /dt save all [default] / /dt reset [default] that dispatch to whichever plot the player is standing in. v0.40.0 β€” PR #54 β€” bounds cage now rendered in bedrock instead of invisible barrier blocks. Authors can see the plot edges at a glance in any game mode; survival players still can't break out of it. Applies to every editor (carriage / carriage contents / track / pillar / tunnel). v0.41.0 β€” PR #52 β€” carriage parts overlay composition: four reusable part kinds (FLOOR / WALLS / ROOF / DOORS) authored as their own NBT templates and overlaid on top of the monolithic carriage NBT at spawn time. A <id>.parts.json sidecar declares per-kind candidate names; spawn picks one deterministically per-carriage so a single variant can render differently from car to car. Parts get their own 2D plot grid in the Carriages category (kind on Z, name on X), shift-click variant authoring per (kind, name), particle outlines + icon HUD on hover, and a Save / All + New / Remove editor menu split. A new transparent MenuTypingScreen opens during the menu's typing mode so movement and hotbar keys pause until the user submits or cancels. Variants without a parts.json sidecar render exactly as before β€” fully opt-in. v0.42.0 β€” PR #55 β€” Clear button + /dt editor clear: one-click destructive action in the editor menu that wipes every interior block of the plot the player is standing in to air, gated behind a ConfirmScreen. Distinct from Reset (which deletes the on-disk template file) β€” Clear only mutates the world, leaving the saved template untouched, so the author can rebuild from a clean slab without re-registering the model. Works in carriages, contents, and parts plots; the barrier cage survives so the player keeps editing in place, and contents-plot Clear preserves the carriage shell stamped as visual context. v0.43.0 β€” PR #56 β€” New source picker + teleport-on-create: clicking New in the editor menu now drills into a Blank / Current / Standard chooser before naming. Blank registers an empty plot (no NBT stamped), Current copies whatever model the player is standing in, Standard copies the canonical baseline. Applies to carriages, contents, and parts. After naming, the player is automatically teleported into the new plot β€” no manual enter step. New command grammar: /dt editor part new <kind> <source> <name>; /dt editor new <name> blank and /dt editor contents new <name> blank accept the literal blank source token. v0.44.0 β€” PR #57 β€” variants v2 + contents-editor variants: variants sidecar schema bumped to v2 β€” each candidate now carries optional block-entity NBT (sign text, banner patterns, decorated-pot sherds, lectern books) and a directional placement context derived from where the author was looking, so stairs / doors / pistons capture facing automatically. Shift-right-click inside a contents plot now appends to a per-contents <id>.variants.json sidecar β€” the same authoring flow shell and parts already had β€” and runtime spawn overlays the picks on top of the base contents stamp using the existing (seed, carriageIndex, localPos) deterministic mixer. /dt editor variant list and /dt editor variant clear now dispatch by plot type (parts β†’ contents β†’ carriage) and print the new +nbt annotation on entries that carry block-entity data. Existing v1 sidecars round-trip unchanged on read. v0.47.0 β€” PR #61 β€” bundled-parts manifest ship-fix: the floor / walls / doors part NBTs were always packaged in the jar but the registry's bundled-tier loader looks for a manifest.json array of names per kind, and those manifests were never added β€” so on a fresh install Part registry loaded β€” 0 bundled + 0 config parts and the parts grid in the Carriages category was empty until the author saved at least one part to the config dir. Adds the three missing manifests so cracked / wood floors, cracked / standard walls, and irondoor / standard / wooddoor doors register at server start and stamp into the parts grid out-of-the-box. v0.50.0 β€” PR #64 β€” variant-place hotkey moved from sneak to a rebindable Forge KeyMapping (default Z). Sneak no longer triggers any variant code path, so authors can move-while-clicking inside the editor without the modifier hijacking placement. The keymap appears under Esc β†’ Options β†’ Controls β†’ Key Binds β†’ Dungeon Train β†’ "Place Block Variant (hold + right-click)" and is rebindable like any vanilla key. Held-state syncs to the server via a new C2S packet on press / release transitions only (cleaned up on logout); applies uniformly to carriage, parts, contents, and track-side variant authoring. v0.53.0 β€” PR #67 β€” part-position world-space menu + weighted/sided variants: looking at a FLOOR / WALLS / ROOF / DOORS surface inside a carriage variant plot pops up a billboarded panel anchored flat against the inward face of that part region. The panel lists every candidate variant for that kind with a per-entry weight cell (click to bump +1, shift-click βˆ’1) and β€” for walls and doors β€” a 3-state side-mode chip: (2) both sides mirror this variant, (1) only one side renders this (the other picks separately), (1|2) the picker decides per-spawn. Add / Remove / Clear / X (close) toolbar with a sub-screen for searching the registered name pool. Schema bump to weighted entries in <id>.parts.json (back-compat with v1 string lists), runtime pick honours sideMode deterministically per (seed, carriageIndex, kind). Toggle the auto-open menu from the editor menu's Part Variant Menu row or the panel's own X button. v0.55.0 β€” PR #69 β€” bedrock cage now wraps the parts grid too. The v0.40.0 switch from invisible barriers to bedrock missed CarriagePartEditor, so when the carriages-editor scene was stamped the carriage row had visible black cages but the FLOOR / WALLS / DOORS / ROOF rows below looked like floating templates with no boundary β€” the parts plots didn't read as "in editor mode." CarriagePartEditor.OUTLINE_BLOCK is now Blocks.BEDROCK, matching every other editor. v0.59.0 β€” PR #74 β€” bundled-discovery unified across every template registry. A new BundledNbtScanner walks the classpath (file: in dev, jar: in shipped builds, union: in Forge's transformer pipeline) and lists .nbt basenames at a given resource prefix. All four registries β€” CarriagePartRegistry, CarriageVariantRegistry, CarriageContentsRegistry, and TrackVariantRegistry β€” now use it instead of hand-maintained manifest.json / customs.json files. Drop a new .nbt into src/main/resources/data/dungeontrain/... and it auto-registers on the next server start. The legacy v0.47.0 parts/<kind>/manifest.json and the carriage / contents customs.json files are kept transitionally as a drift cross-check (the scanner WARNs if the manifest disagrees with what's actually on the classpath) β€” they'll be deleted in a follow-up commit once a few releases confirm zero drift. Implementation note: Forge's UnionFileSystem doesn't implement getPathMatcher, so the scanner uses Files.newDirectoryStream(dir) (no glob) and filters by .nbt suffix in code β€” required to avoid UnsupportedOperationException on bundled jar resources at server start. Closes the original cause of the empty parts grid (v0.47.0 fix) more generally β€” any new bundled template kind added to the project no longer needs a parallel manifest update. v0.56.0 β€” PR #71 β€” world-space block-variant menu: tap Z while looking at a flagged block (in any editor plot β€” carriage / contents / part / track) to open a billboarded panel listing every candidate's name + weight, with a six-cell toolbar Copy | Add | Lock | Remove | Clear | X. Hold Z + right-click still appends a held block β€” both flows coexist on the same key. Add captures the held block (empty hand β†’ the air-sentinel rendered as nothing). Copy produces a dungeontrain:variant_clipboard ItemStack visually mimicking a vanilla command block; placing it elsewhere creates a new flagged cell with the same list. Lock groups bind cells together: clicking Lock cycles the cell's lock-id 0 β†’ nextFreeLockId() β†’ 0; cells with the same non-zero lock-id share their state list (auto-propagated on edit) AND their random pick at spawn. The only way two cells join a group is via Copy / Paste of the clipboard. Sidecar schema bumped v3 β†’ v4 (cells with lockId>0 now serialise as {lockId, states} objects; lockId=0 cells stay as bare arrays so older sidecars round-trip diff-clean). Protocol 4 β†’ 5. First DeferredRegister<Item> in the project. v0.61.0 β€” PR #77 β€” duplicate-variant guard for the part-position and block-variant menus. Part menu's ADD search list hides names already in the entries (case-insensitive), so the player can't even click through to a no-op; server treats a stale duplicate ADD as a no-op and re-syncs. Block-variant menu's ADD compares the held block's BlockState + optional BlockEntity NBT against the cell, refusing with Variant already in this cell action-bar hint if it already exists β€” entries differing only by per-entry weight count as the same variant. v0.62.0 β€” PR #78 β€” parts rename now frees the old name immediately, even when that name ships bundled in the jar. Previously CarriagePartEditor.saveAs skipped CarriagePartRegistry.unregister(oldName) whenever a bundled fallback existed for the old name, so the in-memory NAMES set kept the entry forever and any later attempt to register a new part under that name failed with "already taken." Drops the stillBundled guard so the unregister always runs β€” matching the unconditional behaviour already used by CarriageEditor.saveAs (shells) and CarriageContentsEditor.saveAs (interiors). The bundled jar resource is untouched: on the next reload() the bundled scan re-adds the default, and any user-created part in the config tier wins over the bundled tier on lookup. v0.63.0 β€” PR #79 β€” first bundled roof/ part NBTs ship: roof/standard and roof/windowed. Two new wall variants (walls/slotwindow, walls/smallwindows) plus a refreshed walls/cracked; a new parts/floor/standard.nbt plus a refreshed parts/floor/wood.nbt. The windowed carriage variant now ships a pre-wired templates/windowed.parts.json sidecar that uses the new walls (sideMode: "either") and the windowed roof (weight: 2) β€” the first built-in to render via the parts overlay out of the box. templates/standard.parts.json adds explicit sideMode: "either" to its walls/doors and switches its single roof candidate from none to standard so fresh installs have a visible roof on the standard carriage too. weights.json seeds standard=10, windowed=10 so both built-ins appear with the same frequency by default. Updates contents/2ndlevel, contents/books, contents/enchant, and contents/vase author-template NBTs. v0.66.2 β€” PR #85 β€” preview a part in-place: clicking a part variant's name in the part-position menu re-stamps that variant's template at the placement under the player's crosshair (just one side for walls/doors; the whole kind for floor/roof). Pure visual swap β€” assignment list, weights, and side-mode chips are untouched. Mirrors the block-variant menu's preview-entry flow.

Build your own carriage variants directly in Minecraft and save them as structure NBT files. Each variant gets its own editor plot in the overworld; the mod stamps your saved template whenever that variant spawns (initial spawn or rolling-window addition). The loader walks a three-tier fallback chain β€” the per-install config-dir override wins, then the NBT bundled inside the mod jar, then the hardcoded legacyPlaceAt geometry β€” so authored templates committed to the repo become every fresh install's defaults.

Beyond the four built-in carriage types, you can create unlimited custom variants with user-chosen names (cargo, glass_car, freight_v2, …). Customs live as plain .nbt files in config/dungeontrain/templates/; their presence on disk is what registers them. Only carriage-sized templates belong in this directory β€” pillar and tunnel templates live in their own config/dungeontrain/pillars/ and config/dungeontrain/tunnels/ directories so the scan can't misread their footprints as carriage variants (v0.32.0 fix, see PR #42). Long trains cycle through every built-in followed by every custom in alphabetical order, so a train of length β‰₯ (4 + N customs) shows one of each.

Usage

/dungeontrain editor enter [variant]           # teleport to plot (default STANDARD)
/dungeontrain editor save [new_name]           # capture plot footprint as NBT; with new_name, also rename (v0.26.0)
/dungeontrain editor exit                      # return to pre-enter position
/dungeontrain editor list                      # show variant kind + config/bundled status, dev-mode, source-tree writability
/dungeontrain editor reset <variant>           # delete the config copy; for customs also unregister the variant
/dungeontrain editor new <name> [source]       # create a new custom variant seeded from source (default: standard) (v0.26.0)
/dungeontrain editor devmode <on|off>          # session flag β€” when on, /editor save also writes to the source tree
/dungeontrain editor promote <variant>         # explicit copy: config-dir β†’ src/main/resources (built-ins only)
/dungeontrain editor promote all               # explicit copy for every built-in with a saved config copy
/dungeontrain editor variant clear             # remove the variant entry at the block you're looking at (v0.31.0)
/dungeontrain editor variant list              # dump every variant entry for the current plot (v0.31.0)
/dungeontrain editor variant overlay <on|off>  # toggle the END_ROD particle outline on flagged blocks (v0.31.0)
  • Requires permission level 2 (same as spawn / speed).
  • variant tab-completes against every registered id β€” the three built-ins (standard, windowed, flatbed) plus any custom variants currently on disk, plus tunnel_section and tunnel_portal (tunnel variants share the same enter / save / exit / list / reset subcommands β€” see Feature-Tunnel-Generation Β§ NBT templates + in-world editor; new / devmode / promote are carriage-only).
  • name for new / save <new_name> must match ^[a-z0-9_]{1,32}$ and cannot collide with a reserved built-in name.
  • enter saves the player's previous dimension + position in RAM; exit restores it. Session is lost on server restart.

Editor categories + /dt alias + status HUD (v0.36.0)

The editor commands are unified under a category abstraction so a player thinks about Carriages, Tracks, or Architecture rather than about "which NBT subdirectory do I need?" Every /dungeontrain subcommand is also reachable under the shorter /dt alias, and a top-centre HUD bar tracks which model the player is currently standing in.

Categories

Category Models it iterates
Carriages Every registered carriage variant (built-ins + customs), in registry order (standard, windowed, flatbed, then customs alphabetical).
Tracks Open-air track tile first, then pillar_bottom β†’ pillar_middle β†’ pillar_top (ground-up), then tunnel_section β†’ tunnel_portal.
Architecture Empty placeholder β€” walls, floor, roof templates are planned. /dt editor architecture returns a "coming soon" message.

/dt editor (no args) defaults to Carriages. /dt editor <category> stamps every plot in that category in one pass so the full list is visible at once, then teleports the player to the first model. Switching categories (and /dt editor exit) clears every plot in every category back to air so stale models don't pile up at y=250.

v0.82.5 β€” PR #117: with growing custom-variant counts the CONTENTS and TRACKS plot rows would bleed into the CARRIAGES plot row when the carriages list got long enough. The plot allocator now offsets CONTENTS and TRACKS past the actual CARRIAGES extent (rather than a fixed-stride origin), so each row gets its own clear strip regardless of how many carriage variants you've authored.

/dt alias

Every command under /dungeontrain is also reachable under /dt via a Brigadier redirect on the root node. One registration, no duplicated subcommand wiring. Existing scripts and videos using /dungeontrain still work β€” nothing was removed.

/dt editor carriages                     # = /dungeontrain editor carriages
/dt save                                 # = /dungeontrain save
/dt spawn 10                             # = /dungeontrain spawn 10

Status HUD

A centred bar at the top of the screen reads Editor: <Category> / <Model> whenever the player is inside any editor plot. When EditorDevMode is enabled, a yellow [DEV] badge is drawn to the right of the bar so saves that will write through to the source tree are visually obvious.

The server detects which plot the player is in via EditorCategory.locate(player, dims) once per tick and sends an EditorStatusPacket only when the (category, model, devmode) triple changes β€” stationary play is zero network traffic. On editor exit or player disconnect the packet clears.

The existing version/carriage-index overlay in the top-left steps aside while the editor HUD is active so the top-of-screen area stays uncluttered.

Unified /save and /reset

Two new top-level subcommands replace the per-editor save plumbing and add a gap that didn't exist before (pillar save was buried under /dungeontrain editor pillar save β€” now /dt save dispatches to it automatically):

/dt save                       # save whichever plot you're standing in
/dt save default               # save + promote to source tree (dev env only; tunnels error)
/dt save all                   # save every model in the current category; skip empty plots
/dt save all default           # save + promote every model in the category
/dt reset                      # re-stamp the current plot from disk (revert un-saved edits)
/dt reset default              # re-stamp from the bundled tier only (ignore config-dir override)
/dt editor devmode             # toggle devmode (on/off literals still work explicitly)

Notes:

  • /dt save inspects the player's position via EditorCategory.locate and dispatches to CarriageEditor.save / PillarEditor.save / TunnelEditor.save / TrackEditor.save based on which plot the player is standing in.
  • /dt save all iterates every model in the current category and skips plots with fewer than 5 non-air blocks, so untouched plots never overwrite their templates with empty geometry. Reports "Saved N, skipped M empty".
  • /dt save default calls the store's promote(...) after saving. Only works in a ./gradlew runClient dev environment (the packaged jar has no source tree to write to). Tunnels have no bundled tier so save default in a tunnel errors cleanly instead of silently no-op'ing.
  • /dt reset reloads the template via the normal tier chain (config β†’ bundled β†’ fallback) and re-stamps β€” effectively "revert to last save." /dt reset default forces the bundled tier only, ignoring any config-dir override (for snapping back to the shipped default after experimenting). Both use the new *Editor.stampPlot helpers extracted from enter.
  • The existing /dungeontrain editor save [new_name] rename-on-save path still works through EditorCommand β€” use it when you need the rename; use /dt save for the common case.

Files (v0.36.0)

  • EditorCategory.java β€” CARRIAGES / TRACKS / ARCHITECTURE enum, models(), locate(), clearAllPlots()
  • EditorModel.java β€” sealed interface with CarriageModel | PillarModel | TunnelModel | TrackModel records for exhaustive dispatch
  • SaveCommand.java β€” /dt save [default] + /dt save all [default]
  • ResetCommand.java β€” /dt reset [default]
  • EditorStatusPacket.java β€” S2C (category, model, devmode)
  • EditorStatusHudOverlay.java β€” client-side top-centre bar + [DEV] badge
  • stampPlot(...) / clearPlot(...) helpers added to CarriageEditor, PillarEditor, TunnelEditor, TrackEditor β€” shared between the existing enter paths and the new category-wide stamps
  • getBundled(...) added to CarriageTemplateStore, PillarTemplateStore, TrackTemplateStore for /dt reset default
  • DungeonTrainNet.java β€” PROTOCOL_VERSION bumped to "3" (new EditorStatusPacket); clients on older jars cannot connect to a v0.36.0 server and vice versa.

Editor plots

Plots sit along the overworld +X axis at y=250, spaced 20 blocks apart. Built-ins come first in enum declaration order; custom variants follow in alphabetical order:

Variant Plot origin
standard (0, 250, 0)
windowed (20, 250, 0)
flatbed (40, 250, 0)
first custom (alphabetically) (60, 250, 0)
second custom (80, 250, 0)
… …

Plots are allocated dynamically from CarriageVariantRegistry.allVariants() on every lookup β€” registering a new custom (via /editor new) or removing one (via /editor reset) immediately shifts positions for any customs that sorted after it.

On enter the mod clears the footprint at the plot origin, pre-populates it with the variant's current content (saved template if present, otherwise hardcoded geometry for built-ins, or nothing for customs with no file), wraps it in a bedrock cage (v0.40.0 β€” always visible; unbreakable in survival), and teleports the player into the middle. Anything you place or break inside the footprint becomes part of the saved template β€” the cage itself sits 1 block outside the footprint and is not captured.

Custom named variants (v0.26.0)

/dungeontrain editor new <name> [source] creates a new custom variant populated with a copy of source's geometry. If source is omitted, it defaults to standard.

/dungeontrain editor new cargo                  # new 'cargo' seeded from standard
/dungeontrain editor new freight cargo          # new 'freight' seeded from cargo
/dungeontrain editor new glass_car windowed     # new 'glass_car' seeded from windowed

The mod:

  1. Validates the name (regex ^[a-z0-9_]{1,32}$, not a reserved built-in, not already taken).
  2. Registers the variant in CarriageVariantRegistry β€” allocating it the next plot along +X.
  3. Stamps source's geometry at the new plot (falling through the 3-tier loader so built-in sources work even with no saved NBT).
  4. Captures the stamped geometry as a StructureTemplate and saves it to config/dungeontrain/templates/<name>.nbt.
  5. Wraps the plot in a bedrock cage β€” no enter needed, you can fly straight in and start editing.

variantForIndex cycles through built-ins β†’ customs alphabetically, so a train of length β‰₯ (3 + N customs) shows one of each. Trains already rolling when a new custom is registered continue unchanged for past carriages; future rolling-window additions pick up the new cycle.

Rename-on-save

/dungeontrain editor save [new_name] β€” the plain save behaves as always (writes the current variant's .nbt); adding new_name also renames:

Current variant Behaviour
Custom (e.g. cargo) Renames: old .nbt deleted, new .nbt written. Registry entry moves.
Built-in windowed Built-in reverts to hardcoded fallback (config NBT deleted); edited geometry saved as a new custom new_name.
Built-in standard or flatbed Rejected. Both are protected because standard is the default target of /editor enter / /editor new [source] and flatbed is the only variant with no walls β€” losing them breaks command defaults and the rolling variant mix.
/dungeontrain editor enter cargo
... build ...
/dungeontrain editor save freight          # cargo β†’ freight rename

/dungeontrain editor enter windowed
... build ...
/dungeontrain editor save glass_car        # windowed reverts to fallback, glass_car becomes a new custom

/dungeontrain editor enter standard
/dungeontrain editor save anything         # FAILS: 'standard' is a protected built-in

Removing a custom

/dungeontrain editor reset <custom_name> deletes the .nbt file AND unregisters the variant. The plot goes away on the next lookup. There is no "reset a custom to fallback" β€” customs have no fallback tier.

Variant blocks (v0.31.0)

Mark individual positions inside a plot with a list of candidate block states. Every generated carriage rolls one of those states deterministically at spawn time, so position (3, 1, 2) on standard might be stone in carriage 5, oak planks in carriage 6, and spruce planks in carriage 7 β€” but walking back shows the identical blocks, because the pick is seeded on (world seed, carriage index, local pos) and is re-computed, not stored.

Authoring lives in-world: no typing coordinates or block ids on the command line. Point at the block you want to vary, hold the variant-place key (default Z β€” see v0.50.0) and right-click with a different block in your main hand, and that block is appended to the variants list at that position. The first time you append, the list is seeded with [base_block, added_block] β€” the block currently placed in the plot becomes the default candidate β€” so you never lose what you already built.

Authoring with the variant-place hotkey

Inside any editor plot:

  1. Place the base block you want at the position (normal creative placement).
  2. Switch hotbar to a candidate block.
  3. Hold Z + right-click the base block. (Z is the default β€” rebind via Esc β†’ Options β†’ Controls β†’ Dungeon Train.)

The action bar shows + minecraft:<block> β†’ N variants @ x,y,z, an END_ROD particle cage starts outlining the flagged block (visible up to 24 blocks away, ~3 Hz), and the client HUD above the hotbar updates with a row of item icons showing every candidate. Hold Z + right-click again with a third block to append it. Duplicates are allowed and double that state's weight in the random pick β€” [stone, stone, oak_planks] picks stone 2⁄3 of the time.

Pre-v0.50.0 behaviour β€” the modifier was sneak (Shift). Sneak-right-click no longer triggers variant placement; vanilla sneak (movement, item drop, etc.) works normally everywhere in the editor.

No command is needed for the happy path β€” only /editor save to persist the entries beside the NBT template.

Empty-space placeholder (v0.35.0)

A command block added as a variant is a sentinel meaning "leave this position air at spawn time." When the seeded pick lands on the sentinel, CarriageTemplate.applyVariantBlocks writes Blocks.AIR in place of the command block β€” enabling random windows, gaps, and "sometimes nothing" effects without authoring a second template variant.

Authoring is the same hold-Z-and-right-click flow as any other variant: hold a command block, aim at a base block inside the plot, hold Z + right-click. The action bar confirmation switches to aqua and adds an (empty-space) label so the special meaning is visible:

+ minecraft:command_block (empty-space)  β†’  3 variants @ 3,1,2

All three command-block kinds count β€” minecraft:command_block, minecraft:chain_command_block, minecraft:repeating_command_block β€” so whichever is in your hotbar works. Mix with normal blocks freely; e.g. [oak_planks, oak_planks, command_block] picks a plank 2⁄3 of the time and air 1⁄3 of the time. Duplicates still double weight.

The sidecar JSON stores the state verbatim (e.g. "minecraft:command_block[conditional=false,facing=north]"); the translation to air happens only on apply, so existing tooling and the particle overlay stay unaware of the sentinel meaning.

Determinism β€” picks are still seeded on (world seed, carriage index, local pos), so a position that rolls empty for a given carriage rolls empty on every reload. Adjacent carriages along the train can differ independently because the carriage index feeds the seed.

Overlay β€” the overlay renderer draws sentinels as command-block item icons in the HUD row, which reads as a visual cue that a slot is "possibly empty" during authoring. No special UI required.

Re-entry note β€” if the sentinel was picked at a live carriage, the position is air; the standard "target is air" guard rejects variant-add on air. Author variants from the empty editor plot, not on rolling carriages.

Inspection + reset

Command Effect
/dungeontrain editor variant list Chat dump of every flagged position and its candidate list for the current plot.
/dungeontrain editor variant clear Remove the variant entry at the block the player is looking at (within 8-block reach, inside plot footprint).
/dungeontrain editor variant overlay on|off Toggle the particle cage for this player. Default: on whenever in an editor plot.

To reset all entries for a variant, delete the sidecar JSON (below) and re-enter the plot β€” the cache is invalidated on enter.

Icon HUD on hover

When the player's crosshair lands on a flagged block inside an editor plot, the client draws a centered row of ItemStack icons just above the hotbar, one per candidate state, with a translucent dark backdrop. Hover state is server-authoritative and flows over a small S2C packet on the dungeontrain:main channel β€” only transmitted when the crosshair transitions between blocks, so a stationary gaze is zero network traffic. A Hold Z + right-click push-refreshes the HUD in the same tick as the mutation so the new candidate shows up immediately.

Storage β€” sidecar JSON

Variant metadata lives in a sidecar JSON file next to the .nbt template: config/dungeontrain/templates/<id>.variants.json. It follows the same three-tier lookup as the NBT template (config-dir override β†’ bundled resource β†’ empty). Devmode writes to the source tree just like NBT saves. Missing sidecar = no variants β€” full backward compat.

Schema v2 (current β€” v0.44.0):

{
  "schemaVersion": 2,
  "variants": {
    "3,1,2": [
      "minecraft:stone",
      "minecraft:oak_stairs[facing=west,half=bottom]",
      { "state": "minecraft:oak_sign[rotation=8]",
        "nbt": "{front_text:{messages:['\\\"hi\\\"','\\\"\\\"','\\\"\\\"','\\\"\\\"']}}" }
    ]
  }
}
  • Keys are carriage-local "x,y,z" coords (same basis as StructureTemplate block infos).
  • Array elements are mixed: a bare BlockState string OR a {state, nbt?} object. The nbt field, when present, is the SNBT form of the block-entity tag (sign text, banner patterns, decorated-pot sherds, lectern books) β€” written via CompoundTag.toString(), parsed via TagParser.parseTag on load.
  • Values are serialized BlockState strings parsed via BlockStateParser, so block properties (facing, axis, half, etc.) round-trip. Capture writes the state derived from BlockItem.getStateForPlacement(BlockPlaceContext) so directional blocks face the way the player was looking when they Hold Z + right-clicked.
  • Each entry requires β‰₯ 2 candidates. Out-of-bounds keys are dropped with a warn log on load.
  • v1 sidecars round-trip unchanged β€” the v1 reader path is preserved; on next save the file is written as v2 with bare strings (no NBT) wherever no BE data exists. The empty-space command-block sentinel still works.

How the random pick is seeded

Per-position, inside CarriageVariantBlocks.pickIndex:

long posHash = (((long) localPos.getX() * 31L + localPos.getY()) * 31L + localPos.getZ());
long seed = worldSeed
    ^ ((long) carriageIndex * 0x9E3779B97F4A7C15L)  // carriage id in train β€” golden-ratio mix
    ^ (posHash * 0xBF58476D1CE4E5B9L);              // block position inside the carriage
int pick = new Random(seed).nextInt(states.size());

Same world, same carriage index, same local position β†’ same block, always. Walk back 300 blocks and the carriage you saw before has the same blocks. Different carriages at different indices along the track pick independently so the visible variety grows with train length. The seed comes from DungeonTrainWorldData (fixed per-world at creation); the index is the train-relative carriage number from TrainAssembler / TrainWindowManager.

Only applies to NBT-backed variants

Built-ins using the hardcoded legacyPlaceAt fallback (never edited β†’ no config-dir NBT, no bundled NBT) log a warn and skip their sidecar β€” the procedural geometry has no stable local-position basis for authors to target. To use variants on a built-in, edit it once (move a block, /editor save) so a tier-1 NBT exists, then author variants against that.

Carriage parts overlay composition (v0.41.0)

Reusable building-block templates that compose a carriage shell from four kinds β€” FLOOR, WALLS, ROOF, DOORS β€” overlaid on top of the existing monolithic NBT. Each kind has its own catalogue of named templates on disk; a per-variant <id>.parts.json sidecar declares which names to use for each kind. At spawn time, the monolithic template stamps first (the base shell, captured as before), then any declared parts overlay on top of it β€” so standard with a parts.json of floor: [wood] keeps the rest of its monolithic geometry intact and only swaps the floor tier for the wood part NBT.

A carriage variant with no <id>.parts.json sidecar renders exactly as before β€” parts are fully opt-in. No existing world is migrated; no save format changes.

The four part kinds

Geometry, with axes X=length, Y=height, Z=width and footprint sized against the world's CarriageDims:

Kind Footprint Stamps Mirror
FLOOR (length-2) Γ— 1 Γ— (width-2) 1Γ— at (1, 0, 1) none
ROOF (length-2) Γ— 1 Γ— (width-2) 1Γ— at (1, height-1, 1) none
WALLS (length-2) Γ— height Γ— 1 2Γ—: (1, 0, 0) and (1, 0, width-1) second stamp LEFT_RIGHT (flips Z + facings)
DOORS 1 Γ— height Γ— width 2Γ—: (0, 0, 0) and (length-1, 0, 0) second stamp FRONT_BACK (flips X + facings)

The four kinds together cover the carriage's outer shell with no overlaps: doors own the corner columns at x ∈ {0, length-1}, walls own the side strips for the interior x-range, floor and roof own the inner rectangle at y=0 and y=height-1. Interior voxels are untouched so contents (chests, stations, decor) stay independent.

Multi-candidate slots β€” deterministic pick per carriage

Each slot in the sidecar is a list of candidate names, not a single name. At spawn, one entry is picked deterministically from (worldSeed, carriageIndex, kind.ordinal()) so the same slot in the same carriage on the same track always renders the same β€” but adjacent carriages can render different walls / floors / roofs / doors from the same variant. Single-element lists short-circuit (no RNG cost).

The reserved name none in any slot means "stamp nothing for this kind" β€” useful for an open-sided variant whose walls: ["none"] leaves the sides air, or a walls: ["standard", "none"] that's open 50% of the time. Empty slots normalise to ["none"].

// config/dungeontrain/templates/standard.parts.json
{
  "schemaVersion": 1,
  "floor": ["wood", "cracked"],     // 50/50 wood vs cracked stone bricks
  "walls": ["standard"],             // single option
  "roof":  ["none"],                 // monolithic roof keeps showing through
  "doors": ["standard", "wooddoor", "irondoor"]  // three-way split
}

The schema is scalar-or-array tolerant β€” a bare string in a slot is normalised to a single-element list on load, so older single-option sidecars still parse.

/dt editor part subtree

The part editor lives under /dt editor part … (registered as a subtree of the existing editor command). Tab-completion suggests every kind and every registered name on disk.

/dt editor part enter <kind> <name>          # teleport to part plot, stamping starter geometry if empty
/dt editor part save                         # save the part plot the player is standing in
/dt editor part save <new_name>              # save-as / duplicate β€” writes a new .nbt file under <new_name>, leaves the old name
/dt editor part rename <new_name>            # true rename (v0.45.0) β€” write new, delete old, swap registry, relocate plot, teleport
/dt editor part save all                     # save every registered (kind, name) plot
/dt editor part list [kind]                  # list all parts (or just one kind)
/dt editor part reset <kind> <name>          # delete the config copy for a part NBT
/dt editor part promote <kind> <name>        # copy config-dir β†’ src/main/resources (dev only)
/dt editor part promote all                  # promote every authored part
/dt editor part set <variant> <kind> <name>  # set a carriage variant's slot to a single name
/dt editor part add <variant> <kind> <name>  # append a candidate to a slot
/dt editor part remove <variant> <kind> <name>  # remove a candidate from a slot
/dt editor part show <variant>               # dump the current parts.json assignment to chat
/dt editor part clear <variant>              # delete the parts.json sidecar (variant reverts to monolithic)

Parts rename (v0.45.0)

Part plots are addressed by (kind, name) pair, with the X slot derived from the name's position in the registry's LinkedHashSet. A true rename therefore needs to (a) write the new template, (b) delete the old config + bundled-source files, (c) shuffle the rendered plot to the new slot, and (d) follow the player into it. /dt editor part rename <new_name> (CarriagePartEditor.saveAs) does all four atomically:

  1. Resolve the current (kind, oldName) from the player's plot or session (same plot-aware lookup used by runPartSave).
  2. Capture the live geometry at the old plot origin (so unsaved edits aren't lost).
  3. Write the captured template under <new_name> (config-dir, plus bundled-source if devmode is on).
  4. Register <new_name> (appended to the kind's LinkedHashSet).
  5. Erase the old plot's blocks and bedrock cage via clearPlot(level, kind, oldName, dims).
  6. Delete the old .nbt (and best-effort the old bundled-source .nbt if devmode is on β€” non-fatal warning on failure).
  7. Unregister oldName from the in-memory NAMES set unconditionally (v0.62.0 β€” see PR #78) so the freed name can immediately be reused for a new part. The bundled jar .nbt for oldName is untouched and the bundled scan re-adds it on the next reload(); until then the slot is genuinely free.
  8. Stamp the new plot at its new X slot, draw the cage, and teleport the player to its centre.

Distinct from /dt editor part save <new_name>, which is a save-as / duplicate (writes a new file under <new_name> but leaves <oldName> registered with its old file on disk).

Edge cases the server rejects (with a friendly chat message): renaming to the same name, renaming to an already-registered name (including bundled), renaming with an invalid name (anything outside [a-z0-9_]{1,32} or the reserved none sentinel).

Plot grid layout (Carriages category, +Z axis)

Part plots live next to the carriage plots in the same Carriages category but along the +Z axis at y=250. Kind selects the Z slot (FLOOR β†’ ROOF β†’ WALLS β†’ DOORS, with a 5-block gap between kinds), name selects the X slot in registry insertion order. The first part plot starts at z=40, so the carriage variants (z=0) and the part grid don't visually overlap at default dims.

Position Plot
(0, 250, 40) first FLOOR name in registry order
(20, 250, 40) second FLOOR name
(0, 250, 50) first WALLS name
(0, 250, 60) first ROOF name
(0, 250, 70) first DOORS name
(20, 250, 70) second DOORS name
… …

Plot indexes are stable across sessions β€” the in-memory registry uses a LinkedHashSet, not TreeSet, so a newly authored part appended at runtime (via the New button or /dt editor part enter floor mynew) lands at the next free X slot past the last registered name and never shifts existing names' positions. The initial config-dir scan sorts filenames once at server start for determinism across filesystems (APFS by inode, ext4 by hash) β€” so the same set of authored parts grids identically on every machine.

A bedrock cage wraps each part plot 1 block outside its footprint, same convention as carriage plots.

Shift-click variant authoring per part

Variant blocks (the v0.31.0 random-pick mechanic) work inside part plots too, keyed per (kind, name). Sidecar lives at config/dungeontrain/parts/<kind>/<name>.variants.json, scoped to the part footprint not the full carriage footprint. Particle outlines and the client-side icon HUD work identically to carriages β€” when the crosshair lands on a flagged block in a part plot, the candidate row appears above the hotbar.

Mirror semantics propagate correctly: a variant flagged on the unmirrored wall stamp (Z=0) renders mirror-flipped on the second wall stamp (Z=width-1), so a LEFT_RIGHT block (e.g. oak_stairs[facing=north]) faces the carriage interior on both walls without authoring it twice.

Status HUD + editor menu integration

When the player stands inside a part plot, the editor status HUD (the top-centre bar from v0.36.0) reads Editor: Carriages / Parts | <kind>:<name> β€” synthetic two-line model so the worldspace command menu auto-opens to the editor branch the same way it does for carriages. The menu picks up two splits when the active model is a part:

  • Save β€– All β€” left runs /dt editor part save (position-aware, saves the current plot); right runs /dt editor part save all (iterates every registered (kind, name)).
  • New β€– Remove β€” left runs /dt editor part enter <kind> <typed_name> after typing mode (creates a new plot with starter geometry, stays in editor mode by registering before save); right deletes the .nbt, the variants sidecar, and unregisters the name.
  • Rename (v0.45.0) β€” drops into typing mode pre-filled with the current part name; on submit runs /dt editor part rename <new_name> (see Parts rename).

MenuTypingScreen β€” keyboard capture during typing mode

Pressing the New button enters typing mode for the part name. To stop movement / hotbar / inventory keys from firing while typing, the menu opens a transparent MenuTypingScreen β€” a Screen with no widgets and no rendering of its own. Its presence is enough to pause vanilla keybindings (Minecraft suspends KeyMapping.consumeClick() while any Screen is up). keyPressed handles Enter/Backspace, charTyped filters to [a-z0-9_] (matching CarriageVariant.NAME_PATTERN), and removed() cancels typing if the screen closes any other way (ESC, clicking out).

The auto-close-on-screen guard in CommandMenuToggleHandler exempts MenuTypingScreen so opening it doesn't dismiss the menu; the worldspace panel stays visible behind the (transparent) screen for the duration of typing.

Storage β€” per-part NBT + per-variant assignment sidecar

Two parallel storage layers, both following the existing three-tier loader pattern (config-dir override β†’ bundled jar resource β†’ absent):

Path Tier What it holds
config/dungeontrain/parts/<kind>/<name>.nbt 1 β€” per-install Part template NBT (one per (kind, name)).
config/dungeontrain/parts/<kind>/<name>.variants.json 1 β€” per-install Optional shift-click variants for that part (same schema as v0.31.0 carriage variants).
config/dungeontrain/templates/<id>.parts.json 1 β€” per-install Optional per-carriage-variant assignment. Absence = monolithic-only.
/data/dungeontrain/parts/<kind>/manifest.json 2 β€” bundled JSON array listing the bundled NBT names for the kind. Required for bundled parts to appear in the registry β€” without it the loader cannot enumerate which .nbt files ship in the jar (the bundled tier is read via classpath, which has no directory listing). Authors add to this file alongside dropping a new bundled .nbt.
/data/dungeontrain/parts/<kind>/<name>.nbt 2 β€” bundled Bundled part NBT (read via classpath).
/data/dungeontrain/templates/<id>.parts.json 2 β€” bundled Bundled per-variant assignment (none shipped in v0.41.0 β€” every built-in renders monolithic out-of-the-box).

The v0.41.0 jar bundles starter NBTs for floor/walls/doors (wood, cracked, standard, wooddoor, irondoor) β€” no roof NBTs are shipped yet. v0.47.0 added the per-kind manifest.json files that register them at server start so they show up in the editor parts grid on a fresh install. Servers and players opt into parts by authoring a parts.json sidecar via /dt editor part set/add or by hand.

Composition order at spawn

CarriageTemplate.placeAt refactors the per-carriage stamp into four stages so the overlay semantics are explicit:

  1. stampBase β€” the monolithic NBT (or hardcoded fallback) stamps first, painting the entire footprint. This is what existed before parts.
  2. stampPartsOverlay β€” for each kind, if the variant has a non-none candidate in parts.json, CarriagePartTemplate.placeAt overwrites the kind's region with the picked NBT. Walls stamp twice (mirror-aware), doors stamp twice (mirror-aware), floor and roof stamp once each.
  3. applyVariantBlocks β€” both the carriage's variant-block sidecar and each part's variant-block sidecar resolve at this point, so a wall-block flagged via shift-click in the part plot still randomises per (world seed, carriage index, local pos).
  4. applyContents β€” the contents overlay goes on last, gated as before.
  5. finishPlace β€” block-entity NBT restore + lighting refresh.

Empty / blank carriages β€” the historical "monolithic NBT was 93 bytes" failure mode that motivated parts in the first place β€” surface in the diagnostic log if stampBase finishes with a near-empty footprint AND no parts overlay non-NONE entries (the diagnostic from v0.32.0 still applies).

Files (v0.41.0)

  • CarriagePartKind.java β€” FLOOR/WALLS/ROOF/DOORS enum, dims(CarriageDims), placements(CarriageDims), kindAtLocalPos(lx,ly,lz,dims) inverse mapping, Placement(originOffset, mirror) record.
  • CarriagePartAssignment.java β€” per-variant List<String> per kind, deterministic pick(kind, seed, carriageIndex), with / withAppended / withRemoved, scalar-or-array tolerant JSON.
  • CarriagePartTemplate.java β€” placeAt(level, carriageOrigin, kind, name, dims, seed, carriageIndex) stamps the part NBT and applies its variant-block sidecar with mirror-aware transformLocal for walls/doors.
  • CarriagePartTemplateStore.java β€” three-tier NBT store rooted at config/dungeontrain/parts/<kind>/<name>.nbt and /data/dungeontrain/parts/<kind>/<name>.nbt.
  • CarriagePartRegistry.java β€” in-memory per-kind LinkedHashSet<String> populated from the bundled manifest.json + the config-dir scan; reloaded on ServerStartingEvent, cleared on ServerStoppedEvent along with all caches.
  • CarriageVariantPartsStore.java β€” three-tier loader for <id>.parts.json sidecars, cache that short-circuits on "absent in both tiers".
  • CarriagePartVariantBlocks.java β€” sidecar JSON load/save for shift-click variants on parts; bounded by the part footprint Vec3i, keyed per (kind, name), sidecar at parts/<kind>/<name>.variants.json.
  • CarriagePartEditor.java β€” 2D grid plot layout (FIRST_PLOT_Z=40, PLOT_GAP=5), plotOrigin(kind, name, dims), nextFreePlotOrigin, stampAllPlots, clearAllPlots, enter() registers the name immediately so the HUD synthesises status before the first save.
  • MenuTypingScreen.java β€” transparent Screen opened during typing mode to suppress vanilla keybindings.
  • EditorMenuScreen.java β€” parts branch with Save β€– All and New β€– Remove splits; the New TypeArg uses the 3-arg constructor so the typed name lands as the second positional of /dt editor part enter <kind> <typed>.
  • CarriageTemplate.java β€” composition pipeline refactored to stampBase β†’ stampPartsOverlay β†’ applyVariantBlocks β†’ applyContents β†’ finishPlace.
  • EditorCommand.java β€” part subtree (enter / save / save all / save <new_name> / list / reset / promote / promote all / set / add / remove / show / clear), runPartSaveAll, position-aware runPartSave with session fallback.
  • VariantBlockInteractions.java β€” branches on part plot first, then carriage; shared buildUpdatedList helper.
  • VariantOverlayRenderer.java β€” sidecar-agnostic emitOutlineParticles(List<Entry>) and updateHoverPacket(Predicate<BlockPos>, Function<BlockPos, List<BlockState>>) so the renderer drives both carriage and part variants. Synthetic part status packet sends category="Parts", model="<kind>:<name>".
  • src/main/resources/data/dungeontrain/parts/ β€” bundled floor/{standard,wood,cracked}.nbt, walls/{standard,cracked,slotwindow,smallwindows}.nbt, doors/{standard,wooddoor,irondoor}.nbt, and (since v0.63.0) roof/{standard,windowed}.nbt.

Known limitations (parts)

  • No bundled parts.json sidecars. No built-in variant ships pre-wired to use parts in v0.41.0; every fresh install renders the same monolithic NBTs as v0.40.x. To preview the system, author a sidecar via /dt editor part add standard floor wood (which seeds the file) or copy one in by hand.
  • Variant authoring on contents / pillars / tunnels / tracks still falls through to vanilla placement β€” only carriage and part plots support variant blocks via the variant-place hotkey. A Hold Z + right-click in any other plot lands a real block which gets captured on save.
  • Per-kind X-stride math assumes default dims. At max dims (32Γ—24Γ—32) some part-grid plots can visually overlap their neighbours' bedrock cages. The underlying file-backed templates are unaffected β€” it's a UX glitch, not save-state corruption.
  • No roof/ NBTs bundled. Authors must enter roof <name> to seed a fresh one; there's no shipped starter. Resolved in v0.63.0 β€” roof/standard and roof/windowed now ship.

Part-position world-space menu (v0.53.0)

Inside a carriage variant plot, looking at any FLOOR / WALLS / ROOF / DOORS surface auto-opens a billboarded UI panel anchored flat against the inward face of that part region (the floor's top, the ceiling's underside, the wall the player is facing, the door surface in front of them). The panel exposes the variant's <id>.parts.json candidate list directly in 3D β€” no slash-command juggling β€” and adds a weight column plus a side-mode column for walls and doors so authors control both which variants can spawn and how they pair across the two-placement kinds.

The panel anchors to the kind region's inward face (the wall-as-a-whole), not to the individual block the crosshair last hit, so swinging the camera across different bricks of the same wall doesn't churn the menu. For walls and doors the panel anchors to whichever side the crosshair is on β€” facing the player into one wall produces one menu, swinging round to face the other wall produces a fresh one.

Layout

  • Header: the part kind name (Floor, Walls, Roof, Doors).
  • Toolbar: Add | Remove | Clear | X β€” X closes the panel and disables the auto-open feature for the session.
  • Grid of entries, one row per candidate variant:
    • Name (left) β€” the registered part-template id.
    • Weight (right) β€” integer 1..100. Click to bump +1, shift-click to decrement. Stays open so authors can rapid-tap.
    • Side-mode chip (walls/doors only) β€” 3-state: (2) / (1) / (1|2). Click cycles BOTH β†’ ONE β†’ EITHER β†’ BOTH.
    • Remove [X] chip (only when remove-mode is on) β€” single-click confirms removal.
  • The grid wraps to a new column every 10 rows; the panel widens.
  • All actions fire on left-mouse release (so a click that started off-panel and dragged onto a cell doesn't trigger).

Side modes β€” walls and doors

Walls and doors each have two stamp placements (mirrored across the carriage). Without a side-mode the same name was always picked for both; v0.53.0 lets authors mark each variant's intent:

Mode Label Meaning
BOTH (2) When picked, both sides render this variant (mirrored). Default.
ONE (1) When picked, only one side renders this; the other side picks a separate variant from the list (excluding BOTH-only entries). Use for asymmetric variants β€” a window on one side and a solid wall on the other.
EITHER `(1 2)`

Pick determinism is preserved on (world seed, carriage index, kind ordinal) β€” the same triple still produces the same pair of names regardless of side-mode mix, so already-rendered carriages don't change geometry on world reload.

Preview a part in-place (v0.66.2)

Since v0.66.2 (PR #85), left-clicking an entry's name cell in the grid re-stamps that variant's StructureTemplate at the placement under the player's crosshair. Lets authors flip through the candidate list and see each variant rendered in-place instead of guessing what each template looks like from the name alone.

  • No assignment mutation. The on-disk <id>.parts.json β€” names, weights, side-mode chips β€” stays exactly as it was. Preview is a pure visual swap.
  • Targeted side only. For FLOOR / ROOF (one placement) the whole kind re-stamps. For WALLS / DOORS (two placements) only the side under the crosshair re-stamps; the other side's stamp stays intact via the CarriagePartKind.NONE sentinel honoured by CarriagePartTemplate.placeAtPerPlacement.
  • Sidecar overlay applied. The standard CarriagePartVariantBlocks overlay runs at seed=0 / carriageIndex=0 β€” same picks the editor uses on plot entry, so previewed templates land identical to the deterministic editor stamp.
  • Side-mode is ignored on preview. Clicking a name stamps where the player is looking, even if the variant's SideMode says "right side only." Preview is for visualizing the template, not faithful spawn simulation.
  • No reset to before-preview. Once previewed, the previous stamp is overwritten. Same caveat as the block-variant menu's preview-entry feature.

Implementation: Op.PREVIEW_ENTRY in PartAssignmentEditPacket; server handler PartPositionMenuController.previewEntry plus the new chosenPlacementIndex helper that mirrors the existing chosenPlacementOrigin pattern for resolving which side the crosshair is on.

Add β€” search sub-menu

Clicking Add drills into a search panel. The search field is inactive until clicked; that's by design (the Add toolbar click only switches sub-screens, it doesn't grab the keyboard). Click the green Search: field to open a transparent typing capture; type to filter the candidate name pool live, then press Esc / Enter to release the camera and click a result. Names are pulled from CarriagePartRegistry for the current kind. Click < Back (left-side header chip) to return to the root panel without picking.

Duplicates hidden (v0.61.0): names already present in this kind's entries are filtered out of the search list β€” they don't appear as click targets. The filter is applied even when the search buffer is empty. Removing an entry via remove-mode makes its name reappear in the search list. The server is also dedupe-aware: an ADD packet whose name is already in the list is treated as a no-op (still re-syncs the client UI), so the constraint holds even if a stale client somehow sends one.

Toggle β€” turn the auto-open menu on/off

The auto-open behaviour is per-player. Two ways to toggle:

  • Editor menu ([ key) β†’ Part Variant Menu [ON/OFF] row when standing in a carriages plot.
  • Panel X button β€” closes the panel and flips the same flag off.

Re-enable from the editor menu. Default is ON.

Slash command

/dungeontrain editor partmenu <on|off>      # mirrors the toggle above
/dungeontrain editor part add <variant> <kind> <name> [weight]   # weight defaults to 1 (v0.53.0)

Storage β€” schema v2

<variant-id>.parts.json entries are now { "name": "...", "weight": N, "sideMode": "..." }. The sideMode field is omitted when it equals the default BOTH, so floor/roof entries and pre-existing wall/door variants stay clean:

{
  "schemaVersion": 2,
  "floor": [ { "name": "wood", "weight": 1 } ],
  "walls": [
    { "name": "standard", "weight": 1 },
    { "name": "windowed", "weight": 2, "sideMode": "one" }
  ],
  "roof":  [ { "name": "standard", "weight": 1 } ],
  "doors": [ { "name": "wooddoor", "weight": 1, "sideMode": "either" } ]
}

Pre-existing v1 string entries still load ("wood" β†’ { "wood", 1, BOTH }); the file is rewritten in v2 form on the next save.

Files (v0.53.0)

  • client/menu/parts/PartPositionMenu.java, …Renderer.java, …Raycast.java, …InputHandler.java, PartMenuSearchScreen.java β€” client UI.
  • editor/PartPositionMenuController.java β€” server-side hover detection, sync packets, edit handling, per-player enable flag.
  • net/PartAssignmentSyncPacket.java (S2C), PartAssignmentEditPacket.java (C2S; ADD / REMOVE / CLEAR / BUMP_WEIGHT / CYCLE_SIDE_MODE), PartMenuTogglePacket.java (C2S). Network protocol bumped 3 β†’ 4.
  • train/CarriagePartAssignment.java β€” schema v2, WeightedName(name, weight, sideMode), weighted pickPerPlacement.
  • train/CarriagePartTemplate.java β€” placeAtPerPlacement lets walls/doors stamp different names per side.
  • client/menu/EditorMenuScreen.java β€” adds the toggle row.

Clear all blocks (v0.42.0)

A Clear button in the in-world editor menu wipes every interior block of the plot the player is standing in to air, without touching the on-disk template. Built for the case where an author wants to start a model over from scratch β€” manually breaking every block of a 9Γ—7Γ—7 carriage is tedious, and Reset can't be used because it deletes the file rather than just the geometry.

Reset vs Clear

Action World blocks On-disk template Registry entry
Reset Re-stamped from disk (or fallback if no file) Deleted Unregistered for customs; built-ins revert to fallback
Clear Wiped to air Untouched Untouched

After Clear, the player can rebuild and Save normally β€” the saved template captures the new geometry. If the player exits without saving, re-entering re-stamps the unchanged on-disk template, so Clear is effectively a non-persistent "scratch the world only" action.

Confirmation gate

Clear is destructive, so the menu drills into a ConfirmScreen titled Clear all blocks in '<model>'? with Yes, confirm / Cancel β€” same pattern as Remove. A misclick does nothing.

What survives the wipe

  • Barrier cage β€” the bedrock outline (1 block outside the footprint) stays put. The player keeps editing in place inside the cage.
  • Player position β€” no teleport, no return-position update.
  • Saved template file β€” unchanged on disk until the player explicitly hits Save.
  • Contents-plot shell β€” when standing in a contents plot (which stamps a carriage shell as visual context around the editable interior), Clear only wipes the interior decoration. The floor, walls, ceiling stay so the author keeps building inside the same shell. Implemented by calling CarriageContentsTemplate.eraseAt only β€” that helper iterates interiorOrigin / interiorSize, not the full plot.

Scope β€” carriages, contents, parts

The Clear button is shown in the editor menu only for the categories whose models have a single addressable id and where authoring-from-scratch is meaningful: carriages, contents, and parts. Tracks / pillars / tunnels / architecture intentionally don't show the button β€” they have no per-model id from the menu's perspective. Use the existing per-editor reset commands if you need to wipe those.

Category World blocks erased What's preserved
Carriages Whole carriage interior (length Γ— height Γ— width) Bedrock cage
Contents Interior volume only (length-2 Γ— height-2 Γ— width-2) Bedrock cage + carriage shell context
Parts Part footprint per kind (FLOOR / WALLS / ROOF / DOORS dims) Bedrock cage

/dt editor clear command

The menu drills into a single server command that does the actual erase. It can be invoked directly without the menu:

/dt editor clear         # wipes the plot the player is standing in

Detection mirrors /dt save priority: parts plot first (via CarriagePartEditor.plotContaining), then contents (via CarriageContentsEditor.plotContaining), then carriage (via CarriageEditor.plotContaining). Outside any plot, the command fails with editor clear: stand inside a carriage / contents / parts plot first.

Block-variants are also cleared (v0.136.0). After eraseAt, the matching block-variants sidecar is wiped via *VariantBlocks.clearAll() so a plot wipe doesn't leave orphaned variant metadata pointing at now-air cells. Parts and contents persist the empty sidecar immediately (matching /dt editor variant clear for those plot kinds); carriage variants clear in memory only and are written together with the NBT on /dt editor save. The success message reports the cleared count, e.g. Editor: cleared all blocks in 'carriage_a' (and 7 variant entries). Run '/dungeontrain editor save' to persist.

Files (v0.42.0)

  • EditorCommand.java β€” clear literal registered as a sibling of reset / new; new runClear(CommandSourceStack) method dispatches to the matching template's eraseAt based on which plot the player is standing in.
  • EditorMenuScreen.java β€” clearEntryFor(category, model) helper paralleling newEntryFor / removeEntryFor; wired into the parts branch (between Save / All and New / Remove) and the default branch (between Reset and New / Remove).

Reuses the existing public eraseAt helpers on CarriageTemplate, CarriageContentsTemplate, and CarriagePartTemplate β€” no new abstractions added to the editor classes.

New source picker (v0.43.0)

Before v0.43.0, clicking New in the editor menu jumped straight into typing mode and silently duplicated whichever model the player was standing in (parts seeded with a stone-brick stub; carriages and contents copied current-or-fallback). There was no UI to ask for an empty plot or a copy of the canonical baseline β€” authors had to use the underlying command tree directly or edit a duplicate down to nothing.

The button now drills into a NewSourcePickerScreen before the typing prompt, and after the new model is created the player is teleported into its plot ready to edit.

The three sources

Source Carriage / Contents Part
Blank Register the variant + allocate a plot, no NBT stamped Allocate the plot, stamp the existing stone-brick starter so the floor is visible
Current Copy of whatever model the player is standing in (Current is hidden when not in any model) Copy of the part the player is standing in; hidden when the kind doesn't match (e.g. New floor picker hides Current while standing in a walls plot)
Standard Copy of the literal standard carriage / default contents Copy of the part literally named standard for the kind, falling back to the first registered name. (Pre-v0.63.0, roof had no registered fallback on a fresh install; from v0.63.0 onward roof/standard is bundled.)

Command grammar (v0.43.0)

The picker resolves to one of these commands. Each accepts the source token directly so the same flow can be scripted from chat:

/dt editor new <name> blank          # carriage β€” blank plot, no stamp
/dt editor new <name> <variant_id>   # carriage β€” duplicate <variant_id>
/dt editor contents new <name> blank
/dt editor contents new <name> <contents_id>
/dt editor part new <kind> <source> <name>   # source ∈ {blank, current, standard}

For carriages and contents, Current / Standard are passed as the existing model id (<variant_id> / <contents_id>) β€” the picker resolves the literal source to the correct id client-side. For parts the source is passed as the literal token because the part picker also handles kind-aware fallback for Standard.

Teleport on create

After the new model is registered and stamped:

  • Carriages β€” runNew / runNewBlank calls CarriageEditor.duplicate (or createBlank) followed by CarriageEditor.enter(player, target). Same return-position bookkeeping as a plain editor enter.
  • Contents β€” runContentsNew / runContentsNewBlank calls CarriageContentsEditor.duplicate (or createBlank) followed by CarriageContentsEditor.enter(player, target, null). Default shell stamped for context.
  • Parts β€” CarriagePartEditor.createFrom does the registration, plot allocation, seed stamping, save, and teleport in a single call. Mirrors the enter verb's session bookkeeping so the editor mode survives.

Files (v0.43.0)

  • NewSourcePickerScreen.java (new) β€” three-option picker with conditional Current / Standard visibility. Constructor takes (category, kind?, currentModel?); entries() returns Blank + (Current if applicable) + Standard + Back.
  • EditorMenuScreen.java β€” New becomes a DrillIn to the picker for parts / carriages / contents.
  • CarriagePartEditor.java β€” NewSource enum + createFrom(player, kind, source, name) (allocates plot, stamps seed, saves, teleports). Private resolveStandardName(kind) prefers standard, falls back to first registered name.
  • CarriageEditor.java β€” createBlank(player, target) (no NBT stamp, plot ready for build-from-scratch).
  • CarriageContentsEditor.java β€” createBlank(player, target) (default shell only, no contents).
  • EditorCommand.java β€” editor part new <kind> <source> <name> subcommand; runNewBlank / runContentsNewBlank for the blank token; teleport calls added to runNew / runContentsNew after duplicate.

Storage β€” three-tier loader (v0.25.0)

Templates are resolved by walking three tiers in order; the first hit wins:

Tier Location Applies to Who writes here
1 β€” per-install override config/dungeontrain/templates/<id>.nbt Built-ins + customs /dungeontrain editor save (always), /editor new (seeds), /editor reset (deletes)
2 β€” bundled mod default /data/dungeontrain/templates/<id>.nbt (inside the jar) Built-ins only /editor save while devmode on, OR /editor promote <variant|all>
3 β€” hardcoded fallback CarriageTemplate.legacyPlaceAt Built-ins only The mod itself, when both above tiers miss

Per-install override (Tier 1) is a single StructureTemplate NBT file per variant, global across every world on this Minecraft install. Block entities (chests, signs, banners) are preserved through save and load. Server admins can drop their own NBT here to override what the mod ships. Custom variants only ever exist at this tier β€” deleting the file removes the variant entirely on the next server start / CarriageVariantRegistry.reload().

Bundled mod default (Tier 2) is shipped inside dungeontrain-<version>.jar at /data/dungeontrain/templates/. v0.25.0 ships authored NBT for all four built-in variants β€” every fresh install now uses these instead of the hardcoded box. Add or replace these files via the editor commands described under Authoring templates that ship with the mod below; they only land on disk when the editor is run from a ./gradlew runClient checkout. Custom variants can't be bundled through /editor promote β€” they stay in the per-install config directory.

Loader caching. Once a tier-1/tier-2 hit is resolved for a variant, the result is cached on CarriageTemplateStore. The cache is invalidated on ServerStoppedEvent so a switch between two single-player worlds with different CarriageDims does not return a stale template.

Authoring templates that ship with the mod (v0.25.0)

Two ways to push an in-game build into src/main/resources/data/dungeontrain/templates/ so it lands in the next mod jar:

Live dev mode β€” auto-write on every save:

/dungeontrain editor devmode on
/dungeontrain editor enter standard
... build ...
/dungeontrain editor save        # writes BOTH config-dir AND source tree

Each save logs a green Editor: also wrote bundled copy to source tree (will ship with next build). line. Toggle off when you're done iterating with /editor devmode off. The flag resets to off on every server restart β€” by design, since it only makes sense in a checkout.

Explicit promote β€” copy a previously-saved template from config-dir to source tree:

/dungeontrain editor save         # config-dir only
/dungeontrain editor promote standard           # one variant
/dungeontrain editor promote all                # every variant that has a saved config copy

Both paths only succeed in a ./gradlew runClient checkout. They resolve <project>/src/main/resources/data/dungeontrain/templates/ by walking up from FMLPaths.GAMEDIR (= <project>/run in dev). On a packaged jar install the source tree is not on disk; both ops fail gracefully with a message pointing back to the dev workflow. /editor list reports Source tree writable: yes/no so you can verify before trying.

After the writes land, commit the new NBTs:

git add src/main/resources/data/dungeontrain/templates/*.nbt
git commit -m "feat(carriages): authored standard + windowed templates"

How spawning uses templates

On every CarriageTemplate.placeAt call β€” both the initial spawn and every rolling-window addition β€” the mod looks up the variant's StructureTemplate via CarriageTemplateStore.get(level, type, dims). If present and its bounds match the world's CarriageDims, the template is stamped via StructureTemplate.placeInWorld. Otherwise the loader falls through to the next tier; if all tiers miss, the legacy stateAt() hardcoded generator paints the carriage.

Templates with non-matching bounds are rejected with a warning per tier β€” the loader refuses to silently rescale, but the next tier gets a turn. So a config-dir 9Γ—7Γ—7 NBT in a world running 5Γ—5Γ—5 dims will be ignored, and the bundled default (also 9Γ—7Γ—7) will likewise be ignored, falling through to the hardcoded generator that always honours the active dims.

/dungeontrain debug scan

Diagnostic command that walks every active carriage of every loaded Dungeon Train and enumerates any non-air block inside a padded scan region but outside the canonical footprint. Prints a count to chat (green if clean, yellow if strays found) and logs each stray's offset to the server console. Introduced alongside the editor to catch sliver bugs β€” see Compatibility Β§ Round-to-nearest, not floor, when computing shipyard origin.

Files

Known limitations

  • Footprint is per-world. Templates must match the world's CarriageDims (default 9Γ—7Γ—7). The loader rejects mismatched bounds rather than stretching or cropping; a per-world dimension switch invalidates any template saved against the old dims.
  • Session state in RAM only. /dungeontrain editor exit requires the same session the enter was issued in; a server restart orphans the bedrock cage and the player must manually fly out. /editor devmode likewise resets to off on every restart.
  • Custom variants live in config/ only. They cannot be promoted into the bundled jar resource tier β€” /editor promote only accepts built-ins. To ship a custom with the mod, either rename it onto a built-in slot or commit its .nbt to the repo manually under src/main/resources/data/dungeontrain/templates/ (the registry only scans the config dir, so a bundled-only custom will not be picked up β€” this is a followup).
  • Hot-add of a custom mid-train. Registering a new custom while a train is mid-flight changes the variantForIndex cycle length β€” future rolling-window additions use the new cycle, but existing placed carriages stay as-is (they're already part of the VS ship).
  • Connected-block side effects. Saving a block with neighbour-update behaviour (water, redstone, pistons) can cause placeInWorld to propagate changes beyond the carriage footprint when the template is stamped. Stick to inert blocks for reliable results.
  • Source-tree writes only succeed in a checkout. /editor devmode on and /editor promote need <project>/src/main/resources/ on disk and writable. On a packaged jar install both fail gracefully but cannot be used. Run from ./gradlew runClient.
  • Two open sliver-adjacent issues (tracked as future work): a "door frame" phantom occasionally appears at the front end of the train on initial assembly, and client-side ghost blocks (server collision, no client render) appear occasionally on rolling-window additions. The Math.round shipyard-origin fix in v0.13.0 eliminated the primary sliver but these secondary artifacts remain.

Vehicles in templates (v0.826.0)

Minecarts you park inside any plot β€” carriage shell, part, track, tunnel, pillar or dimensional carriage β€” are saved with the template and put back on every stamp, chest contents and all. Every minecart type but the command-block cart is carried (TemplateDecor.VEHICLE_TYPES; a shared carriage is stamped into other players' worlds, so a command block on wheels stays out). Inside a build a minecart item places onto any block β€” no rail needed β€” and placing one on a rail warns that carts don't run on rails aboard a moving carriage (the rails are sub-level blocks) but ride along fine off them. Boats are refused on anything that rides the train: Sable carries a boat as loose cargo and it never sits right, so a boat still places but the editor says in red that it won't be saved, and the save leaves it out. A dimensional carriage never moves, so there boats are kept too (TemplateDecor.Rule.ROOM) and nothing is said. See VehiclePlacementRules.java and TemplateDecor.java. The same release fixed a long-standing gap where entities saved in a carriage template never reached the running train β€” see Compatibility.

Signing prop books as any author (v0.836.0)

A book & quill signed while standing inside any editor plot can be credited to any author β€” an in-fiction name, a character, a pen name β€” instead of the builder's Minecraft username, so the lore books you stock into a carriage chest read as part of the fiction (PR #1357). On the sign screen the vanilla "by you" line is replaced with an editable author line that starts blank every time and is required: once a title is typed the Sign and Close button becomes Next: Author and clicking it (or pressing Enter) moves the cursor to the author line; with both title and author present the button reads Sign and Close again and signs. Tab or a click switches between the two lines. Vanilla's edit-book packet carries no author, so the name travels ahead of it in EditorBookAuthorPacket and is applied by the server's sign intercept on its editor-plot branch only, sanitized and capped at 32 characters β€” a book signed out on the live train (community book, letter, Death/Love Note) is untouched and keeps your real name. The signed book still carries the editor-authored tag, so it stays inert until a player takes it from a chest on the live train. Localized in all 21 languages. See BookEditScreenAuthorMixin.java and EditorBookAuthorPending.java.

⚠️ **GitHub.com Fallback** ⚠️