Skip to content

Features

Brennan Hatton edited this page Oct 16, 2026 · 296 revisions

Features

Every feature Dungeon Train ships with. Click through for full details — commands, implementation notes, and known limitations.

Since: v0.3.0 · latest: v0.95.0 (PR #132)

The core feature — /dungeontrain spawn builds a train moving at 2 m/s along +X, with three built-in carriage types (standard, windowed, flatbed) plus open and (since v0.95.0) pen customs in the random pool, laid end-to-end so the interior is one continuous corridor. As of v0.83.0 each carriage is its own Sable sub-level (kinematically driven, ride-along-safe), and groups sharing a trainId are extended ahead of the player by an append-only spawner (TrainCarriageAppender). v0.88.0 retired solid_roof and the four legacy customs (oneside, otherside, opensides, sunroof) so authors could re-author from scratch on top of the new walls/roof part library; the LOOPING cycle is now STANDARD → WINDOWED → FLATBED. v0.89.0 replaces the fixed-tick spawn throttle with a per-carriage placement state machine — each spawn runs a 1×3×5 collision check every tick, nudges itself ±0.5 X out of any overlap, and only goes "placed" after 60 clean ticks; the next group spawn waits on the previous carriage's placement-success flag, and auto-spawn extends backward off the tail when a player walks behind the train. v0.94.0 splits the spawn pipeline into two independent direction lanes — forward and backward each have their own placement-success gate, so a still-settling carriage at one end never blocks the other end's growth, and both ends can spawn in the same tick when both need extension. v0.95.0 ships the first author-content pack — pen carriage + cows/pigs/sheep contents + dirt floor part, paired through bundled <id>.contents-allow.json sidecars so farm interiors only spawn inside pen and the workshop carriages stay loot-themed.

→ Read more

Since: v0.83.0 (PR #120) · latest: v0.181.0 (PR #227)

The first frame the player sees on a fresh world is already at the cached spawn placement next to the train — no vanilla (0, ~64, 0) flash, no visible jump. A PlayerFudgeSpawnMixin overrides ServerPlayer.adjustSpawnLocation to short-circuit to a SpawnPlacement cached at server-start. Multi-stage fallback: buffered-X scan with 40-block clear window → random retry → perpendicular walk capped at 60 blocks → X-slide along the track ±300 blocks → on-train-roof last resort. Never lands on the rails, never in water, never inside a leaf canopy. v0.181.0 adds a per-candidate raycast LOS check (Level.clip to trainCenter.y + 8) so every accepted spawn has an unobstructed sight line to the train; when no candidate at the original anchor passes LOS, the X-slide phase steps the anchor along the track (skipping tunnel-buried columns) until one does.

→ Read more

Since: v0.293.0 (PR #365)

On first entry to a world the player now spawns standing on a flatbed of the moving train, and a locked fly-up intro camera reveals the train before handing over control (Space to skip; no HUD during the shot). The login-spawn ground pose is repurposed as the camera's start (now allowed over water), and the welcome-book lightning is held until the cinematic ends. Plays once per player per world; toggle via introCinematicEnabled. The whole join sits behind one continuous themed loading screen, which at 100% reads "All Aboard!" and slowly fades to a dim over the live world with the camera already parked at the shot's opening pose (PR #1205). The screen now covers the very start of the join too: pressing New World / Load World fades the main menu into the loading screen over about a third of a second — the menu dissolves into its own background, which cross-fades into the themed panel, and the load itself runs behind the finished screen so there is no flash of vanilla Minecraft chrome (PR #1223).

→ Read more

End exit portal returns you to the train

Since: v0.924.0 (PR #1473)

Beat the Ender Dragon and drop into the exit portal and you land back on the train — front flatbed of the world's starting-dimension train, facing along the track, inventory intact, with the same deck hold the login placement uses — instead of vanilla's bed / world spawn in an empty Overworld. Handled in RespawnDimensionEvents, which hooks both ways vanilla 1.21.1 leaves the End: the first exit (credits → PlayerRespawnEvent.isEndConquered()) and every later one (a plain dimension change → PlayerChangedDimensionEvent, gated on a real portal transit so DT's own End→Overworld teleports — login placement, the rolled respawn, /dtp — don't bounce the player back). In an End-start world this returns you to the End train.

Nether portals travel along the ride

A lit Nether portal on the overworld ride is a portal along the track, not a door to the real Nether dimension. Step in and, after the normal few seconds and the usual Nether-portal loading screen, you step out of another portal in a band's core: from the first overworld stretch you reach the first Nether band; from any later non-Nether band you go back to the previous Nether; from inside a Nether band you go back to the band just before it. You land the same proportion of the way through the destination as you were through the band you left, and new exit portals are built 48 blocks off the side of the track so they never sit on the rails or in a tunnel. Each trip pairs its two frames (NetherPortalLinks), so re-entering either end returns you to the other exactly, like a vanilla pair — the only way a portal standing in a transition is ever reached. Everything is vanilla's own machinery: a mixin at the head of NetherPortalBlock#getPortalDestination hands vanilla a same-level transition built by its own exit-portal search (NetherPortalBandJump, target math in NetherPortalJump); timing, cooldown, sound and chunk ticket are untouched, the train stays where it is and nothing is said in chat. Portals in other dimensions, and anything that is not a player, stay vanilla. Toggle with netherPortalBandJump (common config, default on). (v0.1200.0, #1897) Since v0.1203.0 (#1904) a trip into a Nether band counts as the real thing for advancements — We Need to Go Deeper and the Nether tab open just as they would through a real portal.

Successor-train chain — retired

Active: v0.40.0 – v0.82.0. The Sable migration (per-carriage sub-levels, PR #119) eliminated the 128-chunk shipyard wall this feature worked around, so the entire mechanism — TrainChainManager, spawnSuccessor, forwardLimit/backwardLimit — was removed. Page kept as historical context only.

→ Historical context

Since: v0.11.0

Carriage count, target speed, and spawn Y are editable from an in-game settings screen (Esc → Mods → Dungeon Train → Config) and persisted per-world in dungeontrain-server.toml. The /dungeontrain speed <value> OP command offers the same tweak from a command source, and live trains accelerate/decelerate immediately.

→ Read more

Since: v0.15.0 · latest: v0.83.0

The mod starts itself — no /dungeontrain spawn needed. On server start, if no Dungeon Train is present, a 10-carriage train is auto-assembled at (0, trainY, 0) before any player joins — so the world's first frame already contains the train. Spawn altitude is configurable via trainY ([-64, 320], default 78). The login teleport itself is its own feature now — see Login spawn placement for the cached-placement / mixin / fallback pipeline.

→ Read more

Since: v0.17.0

Promotes the starter-train toggle and trainY from server-wide TOML to per-world choices made on the Create World screen. A "Dungeon Train Options…" button on the World tab opens a sub-screen with a "starts with train" checkbox and a Train Y editor; choices are baked into dungeontrain_world.dat and isolated per world.

→ Read more

Since: v0.16.0 · latest: post-v0.74.0 (PR #105)

A custom world preset whose overworld floor sits at y=32 instead of vanilla y=-64, removing the unused deep-basement layers (~24% fewer noise samples per chunk-gen). An 11-option Floor Y dropdown on the World tab lets the player pick between −64 and 96 at world creation, and the preset is default-selected when the Create World screen opens. PR #105 adds two more World Type entries — Dungeon Train (Nether) and Dungeon Train (End) — that start the player and the auto-spawned train in the chosen dimension on first login (worldgen for those dimensions stays vanilla; only the starting dimension changes).

→ Read more

Since: v0.864.0 (PR #1390)

Editor blocks that stand in for "whatever this stage is built from" — ten most-used solid slots, stairs/slabs/button/plate, a 13-block wood set and a 28-block stone set — and are swapped for the stage's real blocks when the carriage spawns, so one build serves every stage. Each stage's answers are baked into stages.json; the Stage Palette panel (families × block types) shows them per stage and lets you override any slot or lock the wood/stone family from a held block. In the editor the blocks render as the selected stage's target with their own tile ghosted over. Since v0.1046.0 the stage fence and stage stone walls carry the vanilla wooden_fences / walls tags, so they join each other (and vanilla fences/walls) in the editor and on the train.

→ Read more

Since: v0.13.0 · latest: v0.50.0

Build your own carriage variants directly in Minecraft and save them as structure NBT. /dungeontrain editor enter <variant> teleports you to a dedicated plot wrapped in a bedrock cage; whatever you build inside is captured on save and stamped whenever that variant spawns. v0.25.0 adds a three-tier loader (config override → bundled jar resource → hardcoded), plus /editor devmode and /editor promote to ship authored templates with the mod. v0.26.0 adds unlimited custom named variants via /editor new <name> [source] (seeded from an existing variant) and rename-on-save via /editor save [new_name] (standard/flatbed protected), with all variants joining the rolling-cycle so long trains show every registered one. v0.31.0 adds variant blocks — hold the variant-place hotkey (default Z, see v0.50.0) and right-click a block in the plot with another block in hand to mark that position as random across the held candidates; every spawned carriage rolls a different one deterministically. Particle outlines on flagged blocks, client-side item-icon HUD on hover. v0.35.0 adds an empty-space placeholder — a sentinel entry meaning "render as air at spawn time," so positions can randomly be gaps without authoring a second template (originally a vanilla command block; v0.903.0 / PR #1443 replaces it with the translucent variant_placeholder ghost cube, which has no right-click screen so blocks place against it without shift, and normalises legacy command-block entries on load). v0.36.0 unifies the carriage / pillar / tunnel / track editors under Carriages / Tracks / Architecture categories with /dt editor <category>, adds a top-centre status HUD (with a yellow [DEV] badge while devmode is on), a short /dt alias for every /dungeontrain subcommand, and 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 swaps the invisible barrier-block cage for bedrock so bounds are visible in every game mode. v0.41.0 adds carriage parts — four reusable kinds (FLOOR / WALLS / ROOF / DOORS) authored as their own NBT templates and overlaid on top of the monolithic carriage NBT at spawn time; a per-variant <id>.parts.json sidecar declares per-kind candidate names with deterministic per-carriage random pick, so a single variant can render different floors / walls / roofs / doors from car to car. Parts get their own 2D plot grid in the Carriages category (kind on Z, name on X), per-(kind, name) variant authoring (Hold Z + right-click), and a Save / All + New / Remove editor menu split. Variants without a parts.json sidecar render exactly as before — fully opt-in. v0.42.0 adds a Clear button to the editor menu (carriages / contents / parts) that wipes every interior block of the current plot to air, gated behind a ConfirmScreen — distinct from Reset (which deletes the on-disk template), Clear only mutates the world so the author can rebuild from a clean slab without re-registering. Barrier cage survives, contents-plot Clear preserves the carriage shell stamped as visual context. v0.43.0 adds a New source picker — clicking New drills into a Blank / Current / Standard chooser before naming, so authors can start an empty plot, copy the model they're standing in, or copy the canonical baseline. Player is automatically teleported into the new plot after creation. Applies to carriages, contents, and parts. v0.50.0 moves the variant-place modifier from sneak to a rebindable Forge KeyMapping (default Z) under the Dungeon Train Controls category — sneak no longer hijacks placement, so authors can move-while-clicking inside the editor. v0.836.0 (PR #1357) lets a book & quill signed inside a plot be credited to any author name — the sign screen's "by" line becomes a required, editable field (Sign and Close turns into Next: Author until one is typed), so stocked lore books read as fiction rather than carrying the builder's username; live-train signing is unchanged.

→ Read more

Since: v0.36.0 · latest: v0.300.0 (PR #377)

A second catalogue of NBT templates — this time inside the carriage shell. /dungeontrain editor contents enter <name> [shell] teleports you to a plot at y=250 z=80 where the chosen shell is stamped as non-editable context and the contents fill the interior. Every spawned carriage rolls a contents variant deterministically from (world seed, carriage index) — same math as the shell pick, independent catalogue — so walking back over the same stretch always shows the same mix. The default built-in fallback places a single stone_pressure_plate at the floor centre, so every pre-existing world gets a minimal default without authoring. Flatbed carriages are skipped (no walls → any interior would float visibly). Decorative entities (armor stands, paintings, item frames) save and load in the editor plot; ship-side render is blocked by a VS limitation documented in Compatibility. v0.92.0 adds a per-carriage allow-list — a Contents drilldown in the X-menu (carriage editor only) with one red/green Toggle per registered content, persisted as a <id>.contents-allow.json sidecar; OFF excludes that content from this carriage's spawn pool. Author-side knob for shaping which contents pair with which shells. v0.97.0 ships three more bundled contents (library, seating, shop), wires up dev-mode write-through for the allow-list sidecar so X-menu Contents toggles ship in the next build alongside parts and weights edits, and adds a window2 wall to the windowed pool. v0.300.0 turns shop into a group of themed merchant carriages — a toolsmith plus a new leatherworker stall (group weight 3) whose chest rolls leather, the full leather-armour set, leather horse armour, a complete dye set, emeralds, a cauldron and a potion via the bundled lethershopchest loot table.

→ Read more

Since: v0.191.0 (PR #237)

Naturally-spawned weapons, tools, shields, and armor roll a procedurally-composed display name at chest/pot generation time — "Doombringer of Dreams, Wraith of Iron Blade", "Pope of Cherry Grove Hoofs", "Supreme World Overlord of TechnoBlade". Ports the original Unity DungeonTrain SharedLines weighted-text system: ~1,400 entries across 33 themed pools (Title Prefix, Title Post-Prefix, Food, Places, Animals, Names, Music, Discord People, Colors, Feelings, Materials/Elements, plus 11 MC-themed pools — Dimensions, Biomes, Hostile Mobs, Lore, TechnoBlade epithets, Minigames, Enchantments, etc.). 30% naming gate for plain items, 50% for enchanted (enchanted items roll the longer 4-segment chain). Material slot reads the item's actual material (Iron, Diamond, Netherite, ...) 50% of the time, or rolls a fantasy element (Mithril, The Night, Crystal) the other 50%. Item-type synonym suffix (Blade / Tunic / Hoofs / Wood-Wrecker / Stocking-Things) attaches with weighted placement — 65% none, 30% end, 4% start, 1% "X of …". Deterministic per chest position — same world seed always rolls the same names. Crafted items stay vanilla-named; the hook is wired to ContainerContentsRoller.rollItemStack only.

→ Read more

Echo drops credit their player

Since: v0.1081.0 (PR #1733)

When an echo lets go of a non-stackable item — gear it had equipped when it died, a piece it dropped while swapping to better gear, or a gift — the item's lore gains a line naming the player the echo came from, in AIN's "Crafted by" colour. Wording follows how the item left: a player kill draws Grim phrases ("Pried from the hands of …"), any other death Wistful ones ("Still warm from …"), a gift Gift ones ("A gift from …"), with Plain and Heroic phrases always possible; the name reads as the echo of X, X's echo, X, the ghost of X or something that looked like X. Lore never grows past four lines — a new credit replaces the oldest earlier echo credit, else the last line — and each player's echoes credit an item at most once (CUSTOM_DATA.dungeontrain_echo_credits). Stackables, backpack loot spilled on death and plain PlayerMobs are untouched. Hooked at Entity.spawnAtLocation (echo/EchoDropCredit); off-switch echoDropCredit in the common config.

Since: v0.193.0 (PR #241)

Naturally-spawned armor now rolls two independent cosmetic effects alongside the existing naming gate. Leather pieces have a 30 % chance to be dyed in one of the 16 vanilla DyeColors (white, orange, magenta, … black) via the standard DYED_COLOR data component. Any ArmorItem (leather + chainmail + iron + gold + diamond + netherite + turtle) has an independent 15 % chance to receive a random armor trim — pattern picked uniformly across the registered patterns, material weighted toward common metals (iron / copper / redstone / quartz at weight 3, gold / lapis / amethyst at 2, diamond / emerald / netherite at 1). A leather chestplate can roll both a dye AND a trim on the same spawn. Rolls share the existing (world seed, carriage index, slot) determinism mix and use five independent salts (SALT_DYE_*, SALT_TRIM_*), so adding the effect leaves prior durability/enchantment/name outcomes untouched on old worlds. Crafted armor is unaffected — the hook lives in ContainerContentsRoller.rollItemStack, the same single entry point used by procedural item naming.

→ Read more

Repairing armor keeps its trim (Keep Trim)

Since: v0.839.0 (PR #1359)

Combining two damaged trimmed armor pieces in a crafting grid (2x2 inventory or crafting table) no longer strips the trim — vanilla rebuilds the repaired item from scratch and keeps only durability and curses. The repaired piece keeps the trim of the left-most input (top-most if both sit in the same column). The rule is strict: an untrimmed piece on the left yields an untrimmed result even when the right piece is trimmed, so put the trim you want to keep on the left. The anvil already keeps the left item's trim in vanilla and is untouched. Provided by the sibling mod Keep Trim (one mixin on RepairItemRecipe.assemble), built into the Dungeon Train jar for Modrinth and manual installs since v0.909.0 (PR #1452), and installed as its own required mod by the CurseForge app (it was a separate download everywhere between v0.863.0 / PR #1388 and v0.908.x). See Compatibility → Keep Trim.

Since: v0.37.0

Per-variant integer weights (0..100) bias the seeded random carriage pick in RANDOM + RANDOM_GROUPED modes — set standard=5, windowed=0 to make one variant dominate and another never appear, without changing LOOPING or the fixed-flatbed separator rhythm. /dungeontrain editor weight <variant> <n> edits live from in-game (writes config/dungeontrain/weights.json, reloads immediately); the editor status HUD grows a second centred line weight = N for carriage plots, orange when zero. Default weight is 1, so worlds with no weights.json produce the pre-feature uniform distribution byte-for-byte.

→ Read more

Carriage sizes — Room, Half and Group

Since: v0.1103.0 — PR #1755

A carriage template is one of three lengths: a Room (one carriage), a Half (half a group, placed two in a row) or a Group (the whole run of three). Each group of the train is filled one of three ways — three Rooms, two Halves or one Group carriage — drawn by the layout weights on the X menu's Layout tab (/dt editor layout rooms|halves|group <n>). Inside each layout the templates are picked by their own weights as before.

In the editor each size has its own row of plots, tab and floating menu, alongside rows for Flatbeds and Portals. A template's data sheet has a Carriage: Room · Half · Group switch that resizes it and moves it to the matching row, and a Carriage blocks win switch that makes its contents fill only the carriage's empty space instead of replacing its blocks. Flatbed variants are picked by weight per group, and both ends of a group always get the same one.

Since: v0.20.0 · latest: v0.46.0

A 5-wide stone-brick bridge deck with two east-west rails auto-generates in world space below every Dungeon Train, with stone-brick pillars descending to solid terrain wherever the bed floats over air, water, or forest canopy. Tall pillars get wider spacing and thickness so deep terrain reads as a viaduct instead of toothpicks. v0.30.0 splits pillars into three editable StructureTemplate sections — top cap, repeating middle, ground base — each designable across the full track width via /dungeontrain editor pillar enter <section>. v0.33.0 makes the bed + rail rows editable too — a 4×2×W tile authored via /dungeontrain editor track enter that tiles every 4 blocks along the train axis, plus a clearBlocksAhead floor clamp so the authored rail layer can't be destroyed by VS-AABB float precision. v0.38.0 adds stair adjuncts — a fourth editable template (PillarAdjunct.STAIRS, 3×8×3) that spawns alongside roughly every 40 blocks of track, alternating +Z / −Z sides via Mirror.LEFT_RIGHT; authored under the same /dungeontrain editor pillar subtree (enter stairs). v0.46.0 brings every track-side template — track tile, all 3 pillar sections, stairs, and both tunnel kinds — full variant parity with carriages: multiple named NBTs per kind, weighted random pick per instance, per-block randomization sidecars, and a unified 5-block-gap editor grid. Placed blocks are plain world state, so the bridge stays behind the train forever.

→ Read more

Since: v0.46.0

Every track-side template (track tile, pillar TOP/MID/BOT, stairs adjunct, tunnel SECTION/PORTAL) supports the same two layers of authorable variation that carriages do: multiple named NBTs per kind with weighted random pick per instance, and per-block randomization sidecars (*.variants.json) for shift-right-click cell-level randomization. Editor plots laid out on a unified 5-block-gap grid — track-side row at Z=74, variants along +Z per kind, pillar parts and tunnel kinds stacked on +Y. New /dungeontrain editor tracks new <kind> <name> clones the variant the player is currently on; /tracks reset <kind> deletes it. Legacy single-file paths (pillars/pillar_*.nbt, tunnels/section.nbt, etc.) auto-migrate to <kind>/default.nbt on first server start.

→ Read more

Since: v0.56.0 · latest: v0.1317.0 (PR #2062 — conditional entries: an option only places next to chosen blocks, or when a lock group rolled them)

A billboarded worldspace UI for managing per-cell block variants in any editor plot (carriage / contents / part / track). Tap Z while looking at a flagged block to open a panel anchored to the targeted face 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 — the legacy flow from v0.31.0 is preserved; tap-vs-hold detection runs client-side (≤ 8-tick press without an interaction = tap). Add captures the player's main hand; empty hand → the air-sentinel (rendered as nothing). Copy produces a dungeontrain:variant_clipboard ItemStack visually mimicking the placeholder ghost cube (its model parents dungeontrain:block/variant_placeholder; a command block before v0.903.0); placing it elsewhere creates a new flagged cell with the same list. Lock groups: clicking Lock cycles the cell's lock-id 0 → nextFreeLockId() → 0 (template-local positive integer). Cells with the same non-zero lock-id share their state list (auto-propagated on edit) AND their random pick at spawn — CarriageVariantBlocks.pickIndexFromLockGroup hashes the lock-id where the unlocked path hashes the local position, so every cell in the group lands on the same index. The only way two cells join a group is via Copy/Paste of the clipboard. Schema bumped v3 → v4 (cells with lockId>0 now serialise as { "lockId": K, "states": [...] }; lockId=0 cells stay as bare arrays so v3 sidecars round-trip diff-clean). Protocol bumped 4 → 5 with three new packets. First DeferredRegister<Item> in the project. v0.174.0 adds mob entries (schema v6 → v7): hold a vanilla SpawnEggItem instead of a block on either authoring path (Z + right-click or C-menu Add) and the candidate becomes a mob spawn — when it rolls at carriage spawn, the cell becomes air and the mob spawns at that position via the existing 48-block deferred entity pass. The VariantState record gains an optional entityId field; mob entries auto-stamp the COMMAND_BLOCK sentinel as their state so every existing applier branch routes through the AIR-on-empty path. The C-menu and HUD overlay render the spawn-egg item icon and a light-purple (mob) label. Track/tunnel sidecars accept mob entries but currently log+air at runtime (carriage-side queueing infra only) — non-carriage mob spawning is a follow-up. v0.239.1 adds a T/R/B half-mode pill (Top / Random / Bottom) to rows whose block has SLAB_TYPE or HALF — slabs, stairs, and trapdoors. Cycle on tap; T and B lock the half explicitly, R rolls TOP/BOTTOM at spawn via the existing flip seed. Stairs and trapdoors keep their L/R/O facing pill and gain the new T/R/B pill alongside (independent control over facing and half). The "half" JSON field is additive on v7 entries — no schema bump; legacy entries derive their value at read time (LOCK rotation → halfMode from captured SLAB_TYPE/HALF, RANDOM → halfMode=RANDOM), so existing prefabs spawn bit-for-bit identically. v0.282.0 (PR #348) makes slimes and magma cubes spawned from mob entries roll a random size (1–4) per spawn — the variant spawn path skips vanilla's finalizeSpawn size roll, so previously every variant slime appeared at the smallest size; a spawn egg tagged with an explicit Size keeps it. v0.931.0 (PR #1485) adds an On / R / Off pill to rows whose block latches a toggled state without a standing signal — trapdoors, doors and fence gates (open), levers (powered) and copper bulbs (lit). On spawns it thrown/open/lit, Off (the default, matching the old placement-state capture) spawns it closed, R rolls it per carriage with its own seed salt so a corridor can mix open and closed trapdoors; the editor preview shows the chosen state and blinks under R. Signal-following blocks (redstone lamps and torches, pistons, dispensers, hoppers, buttons, pressure plates, rails, note blocks) deliberately get no pill — a stamped on would revert on the first neighbour update. The "active" JSON field is additive (no schema bump); entries without it derive the mode from the stored state, so a hand-authored open=true reads as On and everything else spawns exactly as before. Protocol 76 → 77 (activeMode byte on the sync packet, SET_ACTIVE_MODE op). v0.953.0 (PR #1534) makes cells work with two-block blocks — doors, beds, tall grass/flowers, pitcher plants: a rolled one spawns with both halves (a bed's head follows its rolled facing), and aiming at either half opens the same menu, anchored on the half you looked at. Cells that also hold one-block entries get a Span toolbar button whose popup sets, per cell, How many spaces a one-block pick fills (1 / 2 / R), its Position (1 / 2 / R, for 1 or R) and Repeat (Same / Random re-roll, for 2 or R); unset follows the first entry (door first → 2 / Same, one-block first → 1 / Pos 1). Stored as an additive per-cell "span" beside lockId; protocol 88 → 89. See Block variants. v0.1046.0 (PR #1684) adds a Def / Auto / Lock connect pill to fence, pane, iron-bars and wall rows (stage walls included). Def places the captured arms as before; Auto re-derives the arms from the real neighbours once the carriage is placed (and in the editor preview); Lock shows N / E / S / W toggles and keeps exactly those arms for the carriage's life — through the Sable lift and later neighbour changes — until the block itself is replaced (ForcedConnectCells + ForcedConnectShapeMixin). Shown only on carriage, whole, contents, part and builder plots (tracks and portal rooms don't apply it). Additive "connect" JSON field; protocol 101 → 102. v0.1092.0 (PR #1742) adds a Grow pill to rows of column-forming blocks — vines, cave vines (glow berries), weeping/twisting vines, kelp, ladders, chains, scaffolding, bamboo, sugar cane, cactus, big dripleaf, pointed dripstone and chorus. Its popup sets Grow (Off / On, default Off), Min / Max length (1–32, rolled per spawn from the variant seed), Direction (Up / Down, only for blocks that grow either way) and Tip (On / Off, only for blocks with a distinct end piece). At spawn the block grows into a column that stops at the first block in its way, the edge of its own template piece, or where the next block can't survive, and never overwrites anything; a cell whose own block can't survive there doesn't grow. Works in every template type — carriages, contents, parts, whole carriages, tunnels, track tiles (bed→rail only), pillars and arches, stairs, portal rooms with their floor/roof planes, and chunk frames. A green outline in the editor shows the longest reach; editor plots are never grown into, so nothing is captured back into a template. Additive "growth" JSON field ("down 2-5", "up 3-3 notip"); protocol 107 → 108. Mirror and rotate now also keep each entry's connect mode. v0.1317.0 (PR #2062) adds conditional entries: every row gets an If pill whose popup gates that option on its surroundings. Touching mode picks sides (N S E W U D) with the band picker's clicks — click toggles one, shift-click toggles all the others, none = all — and the option is eligible when any selected neighbour ends up as one of the listed blocks (added from the hand with + Held, OR'd, matched on block type so rotation/half/open rolls don't matter; empty hand = air, which also matches empty cells). Group mode targets a lock group anywhere in the template and lists that group's own blocks as on/off toggles. A failed condition drops the option and the cell rolls the rest by weight; all options gated out leaves the cell empty. A variant neighbour is judged on its own deterministic roll (re-derived, so placement order never matters), a plain neighbour on the stamped template; cells gating on each other are cut deterministically. Works in carriages, contents (difficulty band included), parts, whole rooms, portal rooms, tunnels and track tiles; mirror/rotate turn the sides. Additive "if" JSON token ("down+north=minecraft:oak_planks", "group3=…"), also carried by the Variant Clipboard; protocol 141 → 142.

→ Read more

Since: v0.74.0

A second world-space menu mirroring the block-variant menu — this one for authoring what items spawn inside a container. Tap C while looking at any block with an inventory BlockEntity (chest, barrel, hopper, dispenser, dropper, shulker, furnace, modded containers — anything implementing Container) and a panel anchored against the targeted face appears with a 5-cell toolbar Add | min | max | Clear | X and per-row <item-name> | xN (count max) | wN (weight) | ×. Add captures the held item; min / max cycle the slot-count range that gets filled at spawn; per-entry count is a maximum (actual stack size rolls uniformly in [1, count] per slot). At carriage spawn, three independent deterministic rolls keyed on (worldSeed, carriageIndex, localPos, …) produce the inventory: K = uniform(min, effectiveMax), Fisher–Yates picks K slots, each slot weight-rolls an entry and rolls a count. Add minecraft:air with high weight for sparse fills. Pool sidecar at config/dungeontrain/containers/<plotKey>.contents.json with bundled-resource fallback at /data/dungeontrain/containers/. Hooked into 3 carriage placement callsites (shell, contents, parts); track / tunnel placements are follow-up. Protocol bumped 6 → 9 with four new packets.

v0.826.0 (PR #1342) adds three random-potion placeholder entries (random_potion / random_good_potion / random_bad_potion) with per-row Scale (tier follows distance, or flat) and Form (Any / Potion / Splash / Lingering) toggles, and makes authored potions spawn exactly as placed — see Potion entries.

v0.1138.0 (PR #1803) lets the menu open on suspicious sand and gravel. An authored pool rolls the single item they dig up, and blocks with no pool keep vanilla archaeology loot. See Suspicious sand and gravel.

→ Read more

Since: v0.1055.0 (PR #1696, relay PR #618)

What items trade for with villagers is tuned by the community at brennan.games/dungeontrain/items. Discord members suggest a value (or the villager trade they want), vote Support / Reject on each other's suggestions with a reason, and approvers settle them on the same page. Every item shows its icon, value and what a villager actually pays. Approved values ship in the next build through ./gradlew pullTradeValues.

→ Read more

Lucky loot — Luck potions add bonus items to train chests

v0.850.0 (PR #1372). DT's train loot is not vanilla loot-table machinery — ContainerContentsRoller bakes a chest's Items NBT at carriage placement, before any player exists — so vanilla Luck (quality weighting) never touched it. Now every chest and barrel also pre-rolls a set of bonus candidates from its own pool at placement (LuckyBonusRoller, stored as NeoForgeData.dt_lucky_bonus on the block entity so it survives Sable moves, carriage snapshots and save/load; book placeholders excluded). The first open spends them (LuckyLootEvents, RightClickBlock in plot space): an opener with Luck gets several extra items dropped into empty slots only — stronger Luck, more items — with an action-bar "Lucky!" line; an opener without Luck gets nothing. Either way the bonus is consumed, so an already-looted chest can't be revisited with a potion. Base loot is never replaced and the base roll stays deterministic per seed — only whether the bonus is claimed depends on who opens the chest. Bad Luck does nothing; shulkers, dispensers and furnaces carry no bonus.

Vases (v0.867.0, PR #1393): decorated pots bake a single bonus candidate from their own pool at placement (LuckyBonusRoller.VASE_BONUS, same NeoForgeData.dt_lucky_bonus slot, baked even when the pot's base roll came up empty) and spend it on break rather than open — LuckyLootEvents.onBlockDrops (BlockDropsEvent, which covers mining and projectile shattering alike and hands over the pre-removal block entity). A breaker with any Luck gets the item 50% of the time (vaseBonusFor, a coin flip — a vase only ever holds one item, so there is no tiered range), dropped into the same drop list and level as the pot's own contents, so on a carriage it spawns in plot space like the pot's own drop. The same PR fixed the chest bonus action-bar line, which had been rendering its raw lang key.

Live Eyes and TVs in train loot

v0.1213.0 (PR #1915). The Loot, Iron Loot, Starter, Vase and Writing pools carry a Live Eye slot (dungeontrain:random_live_headpiece) and a TV (vista:television). Loot is baked with no player present, so the slot bakes a pending Live Eye that resolves for the first player who sees it — opening the container, breaking it, looking at it with Jade (compat/jade/LiveEyeJadePlugin, a server data provider that runs ahead of Jade's storage tooltip), or the stack reaching their inventory (LiveEyeLootResolver). It stays a Live Eye unless the player already holds one (inventory, worn or ender chest), saw one in a container in the last 15 minutes (live_eye_seen_at attachment, game ticks), is a Kid with Livestreaming off, or someone is live on the Live Feed and the 10% keep roll fails — then it becomes a TV. TVs are never limited. "Someone is live" is the relay's public /live/status, polled by the server every 30 s while players are online (player/ServerLiveStatus), or a streamer on the same server. Drifting shared carriages resolve only on pickup, so a resolution never syncs to other worlds. Expected rates (Overworld, levels 1–30): ~1.8 Live Eyes and ~0.5 TVs per 100 carriages with no one streaming; ~0.18 and ~2.1 while someone is. Since v0.1221.0 (PR #1925) the TV is also listed in the Dungeon Train — Narrative creative tab, after the Live Eye.

Since: post-v0.76.0 (PR #108)

Two named prefab libraries authors curate by hand and browse via two new left-side tabs in the creative inventory. Block-variant prefabs are saved per-cell List<VariantState> snippets (the same shape the block-variant menu's Copy button captures); loot prefabs are saved per-container ContainerContentsPools captured from the container-contents menu. Save buttons added to both world-space menus open a modal name screen; on submit the server writes a JSON sidecar and broadcasts a registry sync. Tab entries are vanilla BlockItem stacks (oak_planks, chest, barrel, etc.) with discriminator NBT, so the icon matches the source block exactly and tooltips are decorated with the prefab id. Right-clicking a block-variant prefab pastes the snippet to a target cell (cancels vanilla placement); right-clicking a loot prefab places the actual container block normally and a post-place handler rolls the pool to fill the new BlockEntity. Loot prefabs work anywhere; block-variant prefabs require an editor plot. Protocol bumped 9 → 11. v0.967.0 (PR #1558) adds deleting: Cmd-click (Ctrl elsewhere) a prefab you saved to be asked whether to delete it. In dev mode, the dev can also delete bundled prefabs.

→ Read more

Since: v0.115.0 (PR #156)

Three new in-world UI families that float above editor templates plus a landing-on-top default teleport. Per-plot label panels (name + ×N weight + [-]/[+] arrows + Save/Reset/Clear + Contents + Enter buttons) hover above each cage with a green frame for the player's current plot. Template-type menus at the start of every variant row list every variant — name click teleports, weight cell click bumps +1 (shift -1), header click jumps to the first variant; the active row is tinted green. When the player is in a plot, a companion duplicate of the type menu floats next to the per-plot panel sharing its billboard frame. Every editor enter() (slash commands + every panel/menu name click) now lands the player on top of the cage instead of inside; the per-plot panel's new [Enter] button explicitly teleports to the floor. Both panel families scale via the Options menu's worldspace slider. Three new packets, PROTOCOL_VERSION 16 → 19. v0.135.0 replaces the per-row type menu with a full template navigation menu — see that page for the layout and dispatch changes.

→ Read more

Since: v0.135.0 (PR #176)

Replaces every per-row type panel with one wider nav panel at every row start: a category bar across the top (Carriages | Contents | Tracks, click to run /dt editor <id>), a type-tab strip with the row's own type expanded leftmost and every other type collapsed to a clickable header that teleports to its first variant, the variant list inside the expanded column unchanged, and inline sub-variants drawn as horizontal cells next to every variant that has children — not just the active one. Server populates the per-variant subVariants list from CarriageContentsGroupStore for CONTENTS variants. Panel grows up to 2.25× its base width to fit wide sub-variant rows; past that, the row wraps once below the variant (hard cap at 2 lines, overflow truncated). The standalone sub-variants companion next to the per-plot panel — including the sub-variant weight editor from v0.134.0 / PR #175 — is untouched. Wire format: Menu gains activeCategoryId/categoryBar/typeStrip; Variant gains a recursive subVariants list. ARCHITECTURE is filtered out until it has stamped plots.

→ Read more

Since: v0.146.0 (PR #187)

A worldspace onboarding panel that appears beside the first template navigation menu every time the editor is active. Teaches the three keybinds (X Editor Menu, C Container Menu, Z Block Variant Menu) and frames user-content authoring. A clickable "Read the Wiki to learn more" button opens the Editor wiki page via vanilla ConfirmLinkScreen — same UX as the title-screen Discord button. The panel has its own cylindrical-billboard anchor (offset 5 blocks perpendicular to the row direction) so it stays at a stable world position and rotates independently to face the camera, instead of orbiting the nav menu's anchor. Pure client-side: one renderer, one raycast, one input handler, eight new lang keys — no new packets, no server-side touches.

→ Read more

v0.935.0 (PR #1495). The editor screen's data sheet gains a Lights row (a small icon + count per light-block kind) and a Loot row: the build's total loot value, then a small icon + count per loot-bearing block kind, most valuable first, variant-only blocks corner-marked, hover for source / chance / value / best items as icons. Clicking the row opens Loot pages at the end of the panel's pager — every item the build can give, as a value-sorted icon grid paged so all show, hover for value and holding blocks. Reads containers-store pools and prefab links, container variant candidates, saved Items/LootTable NBT, suspicious sand/gravel, and block_loot_defaults.json; value reuses ItemPowerScore. Client UI only — no packets.

→ Read more

v0.1054.0 (PR #1694). The X editor screen's detail pane gains a Blocks page (opened from the data sheet's Blocks count): every block the build uses — structure blocks and block-variant candidates — as an icon grid with counts. Standing in the build, clicking a cell replaces that cell's blocks with the held block, keeping orientation, as one undo step. A re-skinned cell stays separate from any other cell of the same block until the template is saved, then cells merge one per block (TemplateBlockGroups, server-side per player; dropped on editor or builder save). Off-plot the page is a read-only tally of the saved file. Undo/redo hotkeys now also work while the X screen is open. Protocol 105 → 106 (template_block_groups_edit / template_block_groups_sync).

→ Read more

Since: v0.39.0 · latest: v0.116.0 (PR #157)

A worldspace point-and-click menu toggled by the X key that mirrors a curated slice of /dungeontrain commands. A flat, billboarded panel floats 2.5 blocks in front of the player; crosshair-raycast hover, click to select, world keeps ticking. Automatically switches between a main menu (Editor / Train ▸ / Options ▸ / Debug ▸) and an editor menu (DevMode toggle, Enter category picker, Save ‖ All, Reset, Clear, New ‖ Remove with confirmation screens) based on whether the player is standing in an editor plot. Also promotes Contents to a first-class editor category so /dt editor contents loads every content plot at once and /dt save / /dt reset dispatch correctly inside them. v0.42.0 adds the Clear button — wipes every interior block of the current plot to air without touching the on-disk template; barrier cage survives and contents-plot Clear preserves the carriage shell stamped as visual context. v0.43.0 swaps New's straight-to-typing flow for a Blank / Current / Standard source picker (plus auto-teleport into the new plot after naming), and changes menu activation to fire on mouse release rather than press — slide off a button before releasing to cancel a click. v0.45.0 pairs Reset | Clear on a single 50:50 row, adds a [-] Weight (N) [+] nudge row for carriage variants, introduces reusable Stay and Triple menu primitives, adds a Rename button (parts / carriages / contents) with the typing field pre-filled to the current id and hidden for protected built-ins, and moves the typing field inline at the clicked button row instead of at the top of the panel. v0.57.0 replaces the editor menu's bottom < Back row with an Exit button that runs /dt editor exit — unwinds the editor session and teleports the player back to where they entered, instead of merely popping to the main menu while leaving them stranded in the plot. v0.90.0 promotes the main menu's flat Debug Scan row to a Debug ▸ drill-in containing a new Wireframes ▸ sub-menu — five individually toggleable debug overlays (gap cubes, gap line, next-spawn preview, post-spawn collision box, HUD Δx line) plus an All On / All Off Split master at the top. The post-spawn collision wireframe — always-on between v0.89.0 and v0.90.0 — now defaults off like the others. v0.107.0 adds an Options ▸ submenu with three independent display-scale steppers — All Displays / Worldspace / HUD, range [0.2, 2.0] snapped to multiples of 0.1. Worldspace scales the X menu and every editor menu uniformly (renderer scales PoseStack; raycast divides hit point by the same factor); HUD scales the version overlay and editor status bar via a new HudText helper. Defaults to a more compact UI (Worldspace 0.7, HUD 0.4); persists to a new client-scoped config at <minecraft>/config/dungeontrain-client.toml. v0.112.0 gates cross-category clicks in Enter ▸ behind an unsaved-changes confirmation screen — comparing live block state to a post-stamp snapshot baseline (kept stable against parts overlay drift and the variant-cell preview ticker), surfacing per-template name/Save/View rows with a Continue button that flips between Don't save - continue and Continue based on outstanding edits. Carriages, contents, track tile, pillar sections, adjuncts, and tunnels are all detected. Same-category clicks keep the v0.109.0 templates picker. New packets EditorUnsavedRequest/List and EditorChangesRequest/List carry the lists; protocol bumped to 16. v0.116.0 adds a + New footer row to every floating template-type menu (the v0.115.0 worldspace panels at the start of each variant row), one-click authoring a new variant of the type the player is looking at — clicking opens the keyboard X menu rooted directly at the source picker for that menu's category via a new CommandMenuState.openAt(MenuScreen) (single-screen stack, so < Back closes the menu cleanly instead of falling through to MainMenu / EditorMenu). Carriages / contents / parts route to the existing Blank / Current / Standard picker; tracks collapse to a single name TypeArg via a new NewSourcePickerScreen.Category.TRACKS, mirroring EditorMenuScreen.newEntryFor for tracks. Pure additive client-side wiring on top of existing slash commands. v0.787.0 keeps the editor menu the same width on every tab (the Current tab no longer widens the panel) and adds a Save icon in the header that pulses green while the plot has unsaved edits and sits blue once saved — see Header Save icon.

→ Read more

Since: v0.177.0 (PR #223) · latest: v0.184.0 (PR #230)

The first custom block in the codebase and the first light-emitting block — a vanilla-lectern look-alike that loads ~50 KB of long-form narrative from the original itch.io game as signed-book + lectern primitives. Stories parse from data/dungeontrain/narratives/stories/, loaded through vanilla's datapack ResourceManager channel (a translation datapack can override any story to localize it — PR #735). As of v0.184.0, the first right-click on a narrative_lectern resolves the world's next-unread letter via BookFactory.buildOrRandomForLectern, places it on the vanilla LecternBlockEntity (reused via BlockEntityTypeAddBlocksEvent — no custom BE), and locks that book to the lectern. Subsequent clicks see the BE's non-empty book and skip resolution — the locked content opens unchanged, surviving save/load via vanilla BE serialization. Progression is world-scoped (one cursor per save, shared by all players); each new world starts fresh. A narrative content cell ships in black / cracked / pen / windowed carriage templates so the lecterns drop into worlds without authoring. Commands under /dungeontrain narrative …: list, book, lectern, spawnlectern, progress, reset, reload. v0.180.0 added .lightLevel(s -> 12) so the lectern always glows — between glowstone (15) and a soul torch (10) — making it visible from a distance in dark carriages and reading as interactive at a glance.

→ Read more

Since: v0.182.0 (PR #228) · world-scoped: v0.184.0 (PR #230)

A parallel content track to the narrative lectern: standalone flavour books that scatter through carriage loot as vanilla written books. Each data/dungeontrain/narratives/random_books/<id>.json declares a book with title, author, generation, weight, and a variants[] array of body strings. A new editor-only placeholder item dungeontrain:random_book is the only thing authors place — at carriage-spawn time, ContainerContentsRoller.rollItemStack substitutes it with a stamped vanilla WRITTEN_BOOK rolled deterministically per (worldSeed, carriageIndex, localPos, slot). Every rolled book carries a RandomBookTag identity component (basename, variantIndex); a LivingEquipmentChangeEvent handler marks (book, variant) tuples as seen the first time any player holds them, and on re-hold of an already-world-seen tuple swaps the stack's WRITTEN_BOOK_CONTENT to an unseen pick via RandomBookFactory.pickUnseenForWorld (cycle resets silently once everything's been seen world-wide). Pre-mutation happens on equip rather than on right-click because WrittenBookItem.use opens the book screen client-side from the local stack before the server's response arrives. New Dungeon Train — Narrative creative tab groups the random book + narrative lectern; dungeontrain:random_book is also seeded into 16 prefab loot tables (pot=9, mining/villager/horse/loot_irongold=2, others=1). Ships with one bundled book: musings_of_faulthurst (16 short musings). Commands under /dungeontrain narrative randombook …: list, give, progress, reset. v0.184.0 moves the seen-tracking from per-player to world-scoped (matching the narrative lectern); legacy per-player data in dungeontrain_narratives.dat is silently dropped on first load. v0.265.1 (PR #328) adds a second, organic source — breaking a vanilla bookshelf has a ~1/100 chance (config narrative.randomBookFromBookshelfOneIn) to drop a Random Book in place of a plain book (Silk Touch + chiseled bookshelves excluded; new item entities inherit the drop's velocity so they ride a moving Sable carriage), and rebalances the brick/fish/mining loot pools to make random books rarer.

→ Read more

Since: v0.669.0 (PR #1113) · toned follow-ups: v0.886.0 (PR #1417)

A one-page note signed by Faulthurst that names one real number from the reader's current run — opener (always by name), stat line, closing remark. The coin-flip half of a stats_book loot slot, and one per subject in a Stat Room. v0.886.0 grows it to 15 openers and 61 tails, each tail carrying a tone: positive lines never follow a grim reading, negative lines only do. Kill counts are read against carriages travelled (KillBudget) — within budget is plain, over is grim, and zero kills gets its own kind line once the run is far enough in.

→ Read more

Since: v0.485.0 (PR #807)

Books written and signed by real players, moderated and shared through the mod's relay, then handed back out as vanilla written books in carriage chests. Distinct from Random Books (bundled JSON flavour books): community books are authored by the playerbase and their selection is personal to each player, not world-scoped. The loot slot is an editor-only placeholder dungeontrain:random_playerbook baked pending at container-load and resolved per player when it first reaches their inventory (a per-second inventory sweep, so it resolves out of chests, chiseled bookshelves, hoppers, or shift-click). SharedBookSelector runs a priority chain over the relay pool snapshot: eligibility (no repeat within a life, unless still-unread and two carriage groups behind — the far-behind escape) → language bucket (your language + translations drain before other languages; an absent family falls back to English) → unread-first (soft, and persists across lives and worlds) → weight tier (translated books docked one weight) → seeded random. On pool exhaustion the per-life de-dup relaxes and repeats a community book rather than degrading to a built-in; if the relay is unreachable or empty the pending placeholder is already a real built-in book, so the slot is never empty. Read history is global and client-side (dungeontrain-client.toml, mirrored to the server via SharedBookReadSyncPacket → SharedBookReadMirror) — because DT death spawns a fresh-seed world, this is what makes a book read in one run stay read in the next; the relay also personalises the pool per account (/books/pool?uuid=). Server master switch discoverSharedBooksEnabled (no per-player consent needed to read loot). Since v0.608.0 (PR #1033, relay PR #430) book text is sanitized on both sides of the wire — the relay cleans what it serves (on intake and on serve, so books stored before the guard existed are cleaned as they're handed out), and the mod cleans again at BookFactory.buildPlainBook rather than trusting the relay. Control characters, lone surrogates, ZWSP/BOM and Zalgo combining stacks go; § is stripped from titles and author names (which reach chat and tooltips) but kept in page bodies, where styling your own book is a feature. Bidi marks and ZWNJ/ZWJ are deliberately preserved — stripping them would mangle right-to-left writing and break emoji sequences. Every length cap now truncates without splitting a surrogate pair, closing the one purely-structural crash a player could trigger (an emoji landing on the title or page cap). Since v0.1260.0 (PR #1969, relay PR #759) books from a fallback language fade in as a player's own language runs out: with fewer than 10 unread in-language books by other writers left, a growing share of picks comes from English (or, for English players, the biggest other language per the relay's /books/languages), reaching every pick at none left; while it is active the player's own books are capped at 5% of picks (SharedBookSelector.fallbackShare, FallbackBookPool, BookLanguages). It depends only on what is left to read, not on how much the player has written. Since v0.1265.0 (PR #1975, issue #1970) players can read in more than one language: Options → Dungeon Train → General → Book Languages… lists the game language plus any added ones (searchable picker of Minecraft's languages, up to 8), each with a weight 1–5 (default 1) and a live share of books. The settings sync to the server (ReadingLanguagesSyncPacket → ReadingLanguagesMirror, per player), each extra language gets its own relay window (FallbackBookPool, its own session), and SharedBookSelector.byWeightedFamily rolls each pick's language by weight among the languages that still have unread books; a used-up language's next page is fetched and waited for so bursts keep the ratio. The fallback fade counts unread books across all reading languages and never falls back to one of them. When signing a book and quill, a player with extra languages gets a Language dropdown beside the book and a grey Language: … line under the byline; the pick (BookLanguagePacket) files the book — and lectern letters — under that language, and the server only honours one of the player's own reading languages. No extra languages → no dropdown and exactly the old picks. No relay change; protocol 126.

Since v0.1231.0 (PR #1934) the book & quill screen is quieter and tells writers the rules up front. The bundled Scribble mod's save-to-file / load-from-file and insert-page buttons are hidden by default; Book File Buttons and Insert Page Button under Options → Dungeon Train → General bring them back (client config [scribble] fileButtonsVisible / insertPageButtonVisible). Undo/redo, delete page and the B/I/U/S/k column are untouched. A small ⓘ above Bold shows, on hover, what the train won't take — slurs or hate, sexual content, real names / contact info / socials, self promo, threats or gore, bullying, self-harm, illegal stuff, swearing, spam or gibberish — in a tooltip pinned beside the book (right of the button when it fits, otherwise left of the book) and hidden while signing.

→ Read more

Since: v0.489.0 (PR #816)

Every player-written community book ends with a virtual vote page from the train itself — a dimmed page (visibly not the author's parchment) reading "The train asks," plus one of 10 questions (stable per book), with thumbs-up/down sprites labelled (Y)es/(N)o. Zero mixins: the page is appended via the public BookViewScreen.setBookAccess on Init.Post, so the vanilla forward arrow and page indicator discover it on their own. Clicking a thumb or pressing Y/N from any page commits instantly — the vote is sent (BookVotePacket, PROTOCOL 46 → 47), the book closes, and the train answers with one of 30 gray chat lines (10 yes / 10 no / 10 general). The server re-validates the held stack's identity, stamps dt_book_vote (±1) into CUSTOM_DATA — so the vote works fully offline — and consent-gates a POST to the relay's /books/vote (durable outbox, last-write-wins on clientTsMs, same-vote retries deduped server-side). The stamped vote tints the burn: green (👍) or red (👎) flame dust mixed 50:50 with the vanilla orange flame; unvoted books stay orange and Death Notes keep their soul burn (soul takes precedence). Relay-side (dp-relay ≥ 0.52.0), votes become a bounded factor clamp(1 + 0.5·(up−down)/(up+down+K), 0.5, 1.5) applied as a weighted sample within the operator's curation weight tier in /books/pool selection — tiers stay absolute, worst within-tier skew 3:1, and a downvoted book gets rare but never impossible. Dev-authored content (random/starting/narrative books, letters, deathnotes) is not votable — enforced client-side and in the packet handler. Kill switch BOOK_VOTES_ENABLED relay-side; no votes → selection byte-identical to before. Since v0.494.0 (PR #821) the entire vote flow — prompts, (Y)/(N) labels, and all 30 train replies — is localized into all 19 non-English languages (hotkeys stay Y/N everywhere).

→ Read more

Status: in testing — off by default (sharedCarriagesEnabled)

Certain carriage slots become community-authored spaces that live on the mod's relay and travel between players' worlds. A shared slot either leases an existing player-built carriage from the pool — its blocks placed back verbatim, chests exactly as left — or drops a fresh blank shared template that only enters the pool once someone actually builds in it. The build's furnishings travel with it — armor stands, item frames, paintings and the animals or villagers standing in it are saved with the blocks, placed back in the receiving world, and ride the moving train there (players, echoes and bosses are never carried). A leased carriage is locked to one world until returned or ~1 h of silence, so a build is edited by one player at a time and then carries those changes on to the next world (submit → lease → save → return, a straight NBT round-trip; the relay never parses the blob). Moderation is serve-now: an automatic text scan of the carriage's sign/book/named-item text on every upload flags offensive builds out of the pool, backed by an in-game /reportcarriage command and a relay admin Carriages review page (flag, reports, lease state, scraped text, moderation reason, and a full edit history with Approve / Flag / Reject). The carriage analog of Community Books — books are handed out as loot; shared carriages hand out an editable space that carries your changes back. Sealed (Bedrock) dimensional carriages drift the same way: one locked pair in fifteen becomes a drifting room that uploads when built in and can arrive as another traveller's copy of exactly that room, with a per-room Drift setting to opt out.

→ Read more

Status: editor live; spawning off by default (whole and wholegroup weights 0)

The editor's new Whole section, first in the row, holds templates saved as a single build: a Room is one complete carriage, shell and interior together; a Group is a whole run of three. Rooms roll into the train the way shared carriages do — the whole entry in the carriage weights sets how often a slot draws from the room pool, and the pool's own weights, gates and Stage links pick which — while groups work the same way one size up: the Whole Group template in the carriage editor's Groups row sets how often a Group carriage becomes a whole group (portals win). Both are stamped verbatim and then get their Z-menu variant pools and C-menu container prefabs rolled on top. Other creators' carriage or contents builds can be loaded straight into the room pool from Find a Builder, and Firefly_on_ytb's basiccave1 and jungletemple ship as the first two whole rooms.

Test the Carriage works on both (v0.996.0): the saved room or group is stood up in the basement test band, rolled the way the train rolls it, with Reseed and Back.

→ Read more

The Workbench — stage relay builds, then commit them as any kind

Since: v0.1291.0 — PR #2009, relay dp-relay #792

The editor's Workbench is a staging category that holds templates of every kind at once. Builds are loaded into it from the relay (/dungeontrain editor workbench, or the Workbench tab in the templates menu), laid out in free space and never re-laid out, so you can walk through them, edit them and only then decide what each becomes: Commit (the Move icon on a staged build) installs it as a chosen kind, sub-kind and parent, and clears it off the bench. Nothing uploads from the Workbench.

  • Workbench tab: every builder's builds awaiting review (submitted or resubmitted), filtered to the category you stand in by default and searchable by player. Autoload pulls them all onto the bench in one press, entering the Workbench first if needed. The tab reads the live relay while open; the relay answers with the review queue only (review=pending), since the whole pool is far too large to download.
  • Whole Half: the Whole section gains a Half row — a corridor-length whole carriage beside rooms and groups. Editor-only for now; the train does not draw from it yet.
  • Pool moves: a template can move between the Carriage, Whole and Contents pools at its own size (room, half or group) from the data sheet's Pool cells, the Move icon on shells and whole tiles, or /dungeontrain editor pool-move <carriage|whole|contents> <id> <to> [name]. A move never reshapes: carriage ↔ whole is the same box; a full box becomes contents by taking its interior; contents are baked onto that size's default shell. Variants, container links, weight, gate, Stage link, label and credit travel; a shell's parts and contents list stay behind and chat says so. Shipped templates, flatbeds and dimensional carriages are refused — save your own copy first.
  • Each build's own tools (v0.1293.0, PR #2011): a staged build gets what its kind gets in its own editor mode, decided by the kind the relay gave it.
    • Doors: a dimensional carriage room shows its amber ghost doors at its own door offsets. A carriage or contents build shows its named entry/exit doorways; contents doors are drawn on the shell around the interior.
    • Block variants: the variant menu, the Variant Clipboard, Mirror X/Y/Z/V, lock ids, mob ghosts, the hover icons and the chest (C) menu all work on every staged build.
    • Rooms: a staged room gets the full Dimensions toolset: size steppers and typed size, Walls, Copies (with the cell editor), Contents, Exits, Sky (the plot is lit by it), Fog, Drift, Books, Lock, and the door-in-hand pointer. A staged tunnel shows the red train-envelope wash.
    • Saved with the build: what you set is written into the staged build's own sidecar document and carried into Commit. Room settings already arrive from the relay in that document's weights entry.
    • Resizing: a staged room's resize carries the live room and is written to the shelf at once. Shrinking crops the far rows for good, and a room that grows into the next build moves along the row.
    • Not yet: a staged room's Contents allow-list drill-in.
  • Test the Carriage (v0.1294.0, PR #2012): works on every staged build, as the kind the relay gave it — a carriage, contents, carriage group, track piece or dimensional carriage stands up in its usual test scene, with Reseed, Shift-Reseed and Back, and Save-and-test for unsaved plot edits. Nothing is committed: the build is installed under a hidden wbtest_… id in a scratch folder (dungeontrain/workbench-test/) for as long as a test names it, weighs 0 so no train can draw it, and is swept away within a second of Back (the folder is also wiped at server start/stop). Parts and buildings have no test scene and are refused in chat, as is a carriage or contents of a size this world cannot stand up. Loot prefabs the build links that this install lacks are not installed for the test — those chests stay empty and chat says how many.
  • Dev checkouts lead with the Workbench in the title-screen Train Editor picker, the Nav tab, the editor menu and the category list; releases never list it.

Buildings — edit and add the buildings the world places

Since: v0.1113.0 — PR #1769, relay dp-relay #639

The Train Editor has a Buildings tab, last in the nav. It holds DT's own Lost City / WWOO buildings (office tower, civic hall, hotel and the rest) and any you add. Editing a shipped building replaces it on your install; a new one joins the placement roster with a weight of 1–10 and starts appearing in newly generated Lost City chunks. Buildings can be up to 64 × 159 × 64, the size of the largest Lost City building, so their plots stand lower than the other categories (y = 150). Resize is /dungeontrain editor buildings size <x|y|z> <inc|dec|n>; /dungeontrain editor buildings lists the rest of the commands.

  • Lost City is a second type in the same tab: Big Lost City's own buildings, read from that mod at runtime, to look at only. They cannot be edited, saved, copied, uploaded or installed, and breaking or placing blocks in their plots is refused — Big Lost City is All Rights Reserved.
  • Sharing: a building save uploads to My Builds like any other template, can be submitted for review, and can be downloaded and installed by another player. Lost City pieces never go up.
  • Free Play: custom buildings count as custom content, except a building whose blocks match one an operator has accepted on the relay (POST /<cap>/carriages/accepted). Changing it ends the exemption until the new version is accepted, and offline nothing is exempt.

Both nav menus (title-screen picker and in-editor nav) were redesigned at the same time: Whole and Dimensional Carriages keep their large tiles, the rest are shorter.

Status: in testing — off by default (builderProfileEnabled)

Saving in the Train Builder also uploads the build to your own profile on the mod's relay, so what you make travels with you between worlds instead of living on one machine. Every kind the builder authors goes up — carriages, carriage rooms, shell parts, track tiles, tunnels, portal rooms — and a My Builds screen off the builder's pause menu lists them as 3D tiles, each labelled saved or on the train. Submit to the Train publishes a carriage of yours into the shared carriage pool so it can be leased into anyone's run; Take Off the Train pulls it back. The trash button beside them deletes a build from your profile for good after a confirm step — the local template stays, and saving it again uploads it fresh; a build another world is riding right now is refused with a Delete anyway option. The title shows how many builds you have. A build stays private to you until you submit it — saving is not publishing — and the relay enforces that line itself: a lease only ever serves a build that is both published and a carriage. Building from the platform saves the whole parked run as one carriage group, because which carriage follows which is part of what you made; groups reopen in the builder and are placed on a train as whole groups. Since v0.1097.0 (PR #1748) a group keeps its Z-menu block variants and C-menu container loot through save, reopen, upload and download — each parked carriage has its own working copy in the builder world, stitched into the group's one whole_group:<id> document on save. Groups saved before that carry blocks only and need a re-save. Uploading requires both the server switch and the player's own network consent.

→ Read more

Save-as for built-in templates

Since: v0.946.0 — PR #1520

Saving a template that ships with the mod, in the Train Editor or the Train Builder with dev mode off, no longer writes over it. A Save-as screen asks instead:

  • Save as new <name>: your edits become a new template of your own on its own plot, carrying the source's sidecars (variant pools, parts, contents allow-list, container links, weights). It goes to My Builds and can be submitted. The built-in is put back exactly as it was last saved.
  • Keep as local edit: saves over the built-in on this install only. It is never uploaded, and chat says why.
  • Cancel: nothing is written.

The server decides when to ask, so every Editor save path prompts: the X menu, the header Save icon, the editor screen, the plot panel, the unsaved-changes list and a typed /dt save. The typed answers are /dt save as <name> and /dt save local. Every template kind is covered: carriages, contents (a sub-variant's copy joins its parent's group), whole rooms and groups, parts, track tiles, pillars, adjuncts, tunnels and dimensional carriages.

"Built-in" means the mod jar ships a template under that name, so shipped customs such as black or cracked, shipped contents, parts and named tracks and rooms all count. The same check keeps them off My Builds. Dev checkouts (dev mode on) save built-ins in place as before, which is how the mod's own templates are authored.

Note: in carriage templates the inner floor, wall, roof and door cells are drawn by parts, so an edit there is saved but covered by the part. Edit the part itself to change those cells.

/dtp — teleport by coordinate or band (OP)

/dtp <x> teleports you to world-X x, spawns a fresh train there and drops you on its flatbed (difficulty matched to the destination). /dtp <band> [distance] does the same for the next occurrence of a band ahead of you — overworld/ow, nether, void, end, upside_down/ud, chuncks, spheres, stacks — landing distance blocks past its entry (default 32; negative lands before it). If you're already inside that band it jumps to the following one. A band disabled in config reports "No band ahead of you". Band entry is found by BandLocator scanning TrainPhase.phaseAt. (v0.948.0, #1522)

Dimensional carriages tell you who built them

Since: v0.845.0 — PR #1368

Walk into the room of a dimensional carriage whose template carries a builder credit (the Built by set in the editor, or the download credit of a build pulled from another player's profile) and a quiet gray chat line names them — one of eight phrasings, e.g. "The train speaks of AntonioMacarely here." — once per visit, in the same voice as the drifting carriage credit line. Enter one you built yourself and it reads "You built this one." Uncredited rooms stay silent. Read from local data, so it needs no network consent, and it is translated into all 21 languages.

Your own library turns up when you've written something

Some dimensional carriages open onto a library stocked from one author, and sometimes that author is you. Since v0.1102.0 (PR #1757) how often that happens depends on what you have written lately. Ordinarily a library of your own books is about half as likely as the room weights alone would make it. Sign a book and it becomes three times as heavy in the draw, with every other dimension giving up a little in proportion, until you walk into one; then it drops back until you write another. Carriages already rolled keep their rooms. In your own library the lectern's note gains a second page with a short tip saying as much.

Server owners can tune both numbers in dungeontrain-server.toml: ownLibraryChanceScale (default 0.5) and ownLibraryChanceWrittenScale (default 3.0). Setting both to 1.0 restores the draw exactly as the room weights state it. Recognising a library as your own needs network access granted, as the library itself does.

Since v0.1206.0 (PR #1909) any player head in a library wears its author's face, and drops as "'s Head". The skin comes from the relay's skin cache (dp-relay 0.244.12+); heads a template already gives a face keep it. In your own library your current skin is used until the relay has one.

Since: v0.841.0 — PR #1363

The title-screen Credits page thanks the people whose work rides every train, in three lists under Made by: Translations (from the build-time credits plus anyone the relay has approved since — those above 1% of a language first), Writers (players with 20 or more community books the train loved — a book counts when its up-votes beat its down-votes better than ten to one), and Builders (whoever originally built each shipped carriage, dimensional carriage or contents template). Each list opens on its top names with See more, then pages of ten with Previous / Next and See less; the Writers list ends with how to get on it. Writers and Builders are refreshed from the community relay every time the page opens and cached for offline launches, so a credit set after a release shows before the next one. Two Tallyman leaderboards go with them: Most Templates Built and Most Praised Writers. Since v0.849.0 your own row on any card carries an Edit button: rename yourself (every card follows), be listed as Anonymous, or restore your name. Since v0.872.0 two more cards follow: Funders (everyone who has funded the game, by all-time contribution, with the amount beside the name — hideable from the Edit button) and Community (the Discord's Value Adders by MEE6 level; link your Discord with /dtlink <code> from the card's footer to get the Edit button on your line). Since v0.1182.0 the team is credited too: Brennan Hatton on Builders (uncredited shipped templates), Writers (built-in books) and Funders (what he has spent that support hasn't covered), and Wilson Taylor on Writers for the original game's stories. The Edit form also gains Show my skin on the website.

→ Read more

Discord reactions and emoji in chat

Since v0.1296.0 (PR #2018), reactions added in Discord show on the in-game chat line they belong to, as small chips after the text: [🔥 2]. Hover a chip to see who reacted. This works for:

  • a player's own chat line, which Discord Presence posts to their thread
  • Discord replies in that thread
  • Developer messages
  • Live stream chat lines

If a reaction lands on your own line after it has faded from the closed chat, a short grey note says who reacted and with what.

Emoji draw in colour from a bundled Twemoji font (CC-BY 4.0). That covers ZWJ families, skin tones, flags and keycaps.

  • Text symbols: ©, ❤, → and similar stay plain unless the text carries the emoji selector (U+FE0F). This matches Discord.
  • Custom server emoji (<:name:id>) are downloaded once from Discord's CDN (only with network consent) and drawn inline. A grey :name: stands in until the image arrives.
  • Shortcodes: :fire:, :+1: and the like in your outgoing chat become the emoji, in-game and on Discord.
  • Menu chat: the title-screen menu chat shows colour emoji too.

How it works:

  • Relay: dp-relay #797 (0.244.94) forwards reaction snapshots over the game gateway as {"type":"reactions"} frames, and adds a reactions list to /live/chat.
  • Discord Presence 0.69.0 (#72) provides the seams: DiscordChatHooks (the Discord id of each posted chat line, and delivery of replies) and DiscordReactionHooks.
  • Server side: compat/DiscordChatSeams and discord/chat/DiscordChatLines remember which Discord message each line is. They send three S2C packets: line bind, Discord line, and reactions.
  • Client side: client/chat/ChatReactionTracker re-draws the line in place through ChatLineEditor (ChatComponentAccessor). client/chat/emoji/EmojiText does the emoji conversion. Custom emoji use the dungeontrain:discord_emoji font provider, a NeoForge enum extension of GlyphProviderType.
  • Regenerating the font: scripts/emoji/build-emoji-font.py.
  • Limits:
    • Reaction counts only include reactions the relay has seen since it last restarted.
    • Animated custom emoji show their first frame.
    • With a Discord Presence older than 0.69.0, chat still works but carries no reactions.

Discord linking and pings — /discord and the Account tab

Since v0.1193.0 (PR #1881) a player can link their Discord account without leaving the game. /discord (a client command, so it never reaches the server or affects run integrity) gives a one-time code with Copy code and Open Discord. In the Dungeon Train Discord they type /dtlink, pick it from the list and paste the code — Discord won't run a pasted /dtlink CODE, so only the code is copied. Chat confirms once the link lands. The Credits page's Link your Discord button uses the same code flow.

Once linked, the relay @-mentions them on their own posts: the death report (public manifest), Tributing their own photo, someone Tributing a photo they took, a PlayerMob photographing them, and milestone advancement posts. Options → Dungeon Train… → Account shows the Minecraft and Discord accounts side by side and has All Discord pings (same as /discord pings on|off) plus one toggle per kind. Pings default on, including for players who linked earlier through Credits.

How it works: each post's hook URL carries a hint (ping=<uuid>&ping_type=<kind>, or ping_photo=<id>&photo_cap=<cap> for a found photo). The relay (dp-relay #716, #717, #718) strips the hint, resolves it to linked Discord ids it keeps to itself, and adds the mention — Discord ids never reach the mod. Pings are capped at 30 per member per hour. Code: client/credits/DiscordLinkCommand, client/credits/DiscordAccountState, discord/DiscordPings, discord/PingType.

Since: v0.185.0 (PR #231)

A third narrative-pool track, sibling to Random Books and the Narrative Lectern. On first login and every respawn after death, a cosmetic colored lightning strike lands 2 blocks in front of the player — vanilla LightningBolt with setVisualOnly(true) for the flash + thunder, overlaid by a 25-block vertical column + ground burst of tinted DustParticleOptions particles (HSV random hue, full saturation/value — distinct vibrant color every strike). A vanilla WRITTEN_BOOK ItemEntity lands at the strike point with a 1 s pickup delay, carrying one of the 12 bundled welcome messages (12 books, 19 total variants — Brennan Intro, The Game, Blank Pages, A Paper Algorithm, etc., generated from the original itch.io game's unused/Welcome Messages/ drafts). The strike defers 10 ticks after PlayerLoggedInEvent so Login spawn placement's placement-retry loop finishes teleporting the player first. Closing the book screen sends a client ScreenEvent.Closing → server StartingBookClosedPacket (PROTOCOL 20 → 21); the server drops the book forward via Player.drop(stack, false, false) and registers it for the burn lifecycle. A single EntityJoinLevelEvent handler catches every starting-book ItemEntity (close-flow drop, Q-throw, death-drop), tags it for 60 ticks of FLAME + SMOKE particles, sets a vanilla fire block on the supporting surface, then extinguishes + discards on burn-out with a FIRE_EXTINGUISH poof. The lightning-spawned welcome book itself is exempt from burn via a per-entity dt_starting_spawn_book persistent-data flag set before addFreshEntity. Pagination uses a stricter rule than the other narrative loaders — every \n\n in the source is exactly one page break (no greedy collapse), so authors can place blank pages between sections via doubled-up breaks; locked down by 11 unit tests in StartingBookPaginationTest. Per-player first-login receipt persists in NarrativeProgressData (starting_book_received root-level UUID list) alongside the world-scoped story + random-book maps. Commands under /dungeontrain narrative startingbook …: list, give, reload, reset.

→ Read more

Since: v0.23.0 · latest: v0.85.0

Wherever the train's corridor runs through thick enough rock or dirt, a stone-brick tunnel auto-generates: walls, arched roof, floor extension, and sea-lantern wall lamps every 10 blocks (baked into the section NBT). v0.29.0 turns the tunnel geometry into editable NBT templates — /dungeontrain editor enter tunnel_section|tunnel_portal lets you redesign the section + portal models in-world the same way you already customise carriages; the v0.23.0 procedural paint remains as the fallback. v0.46.0 brings tunnels into the unified track-side variant system — both section and portal support multiple named NBTs with weighted random pick per tunnel. v0.85.0 moves placement entirely to chunk-gen time (no more runtime drain) and adds an extended qualifier that tolerates soft spots inside an active tunnel run; portals + 20-block approach trench are pulled out for now and will return when worldgen-time portal stamping lands.

v0.1049.0 (PR #1690) adds tunnel template groups:

  • Groups: tunnel sections and entrances can belong to any number of named groups. A tunnel rolls one group (weighted, among groups with a section and an entrance allowed there) and builds every stamp from entrance to exit from it, each member at its own weight inside that group. Templates in no group form the Ungrouped pool.
  • Keeping a tunnel one group: the choice is saved per chunk X (dungeontrain_tunnel_groups SavedData), so a tunnel generated a chunk at a time stays one group. Since v0.1058.0 (PR #1700) a group only changes after at least 5 chunks with no tunnel: tunnels closer than that share one group, and a new chunk looks up to 5 chunks each way (walking past tunnel-free chunks) for the nearest tunnel's group.
  • Nether fade: inside the Overworld→Nether crossfade every tunnel is the stone group dithering into blackstone, whatever group it rolled; a tunnel that starts in the fade rolls stone, so it keeps that look on both sides.
  • Bundled groups: stone, blackstone (Nether only), copper, mud and mossy. New copper and mud templates, and overgrowth, brassish and fancytunnel by Pumpkin_P_Patch.
  • Editing: the X editor's Groups tab (Tracks only) lists every group with its block icon, weight and counts. It has pages to add or remove entrances and sections and set their in-group weights, plus Rename, Delete and a Test that builds a tunnel from the group alone. The editor world lays tunnel plots out by group, so each group's entrances stand over its sections.
  • Commands: /dt editor tracks tunnelgroups list|new|weight|ungrouped|rename|delete|toggle|member and /dt editor test tracks tunnelgroup <id>.
  • Data: memberships are stored as groups / groupWeights in the tunnel weights.json, and group weights in tunnels/groups.json.

v0.1051.0 (PR #1692) gives groups spawn filters:

  • Filters: each group has the same filters as a template — a Stage link, or its own min/max Diff-Level and bands.
  • Rolling: a tunnel only rolls a group where the group's filters allow the spot. Its members are still filtered by their own gates.
  • Editing: the Groups tab's Group page shows the same Stage picker, level steppers and band letters a template's sheet has. Gated groups are marked ◆ in the list, and member icons show their weight inside the group.
  • Commands: tunnelgroups minlevel|maxlevel|phase|stage.
  • Data: each group in groups.json is written in the template weight format, a bare weight until it has a gate or Stage.

→ Read more

Since: v0.101.0 (PR #139)

The Minecraft main menu shows the Dungeon Train wordmark instead of the vanilla "MINECRAFT" logo, with the "Java Edition" subtitle removed. Rendered at 343×41 px (≈ 1.25× vanilla LOGO_WIDTH), centered, 5 px below vanilla's logo origin. A LogoRenderer.renderLogo mixin (@Inject(HEAD, cancellable=true)) bypasses vanilla's two-stacked-tile blit so the single-row 1440×173 source artwork renders cleanly at its natural aspect ratio. Panorama fade-in alpha is honored via RenderSystem.setShaderColor. Side effect: the Mojang vertical-flash intro (which reuses LogoRenderer) also shows the new wordmark.

→ Read more

Since: v0.144.0 (PR #185)

Two new buttons on the vanilla title screen — Discord opens the project's community invite (discord.gg/jdKAwb6rbW) via vanilla ConfirmLinkScreen; Train Editor opens a four-tile picker asking what you want to build — Train Outside, Inside Carriage, Tracks & Tunnels, Train Dimensions — and only then launches a fresh creative-mode world on the Dungeon Train default preset, automatically running /dungeontrain editor <category> for the tile you picked once the player has spawned in (40-tick settle delay + visible chat heads-up). The Mods row is split 50/50 between the two new buttons; the existing Options + Quit Game row absorbs the displaced NeoForge Mods button at 33/33/33.

→ Read more

Since: v0.795.0 (PR #1298)

A Shaders page on the title screen (between Mods and Options) and in the Esc menu (beside Mods), listing the nine Iris shader packs measured to render Dungeon Train's bands and dimensional carriages correctly. Each is shown as a screenshot of the same carriage from the same camera at the same hour — with an unshaded control frame to compare against — and one click downloads it from the author's own Modrinth listing (host-allowlisted, size-capped, SHA-512 verified, staged as a .part file) and turns it on. The running pack carries a green edge, a downloaded one a fainter blue; the list sorts by our pick, performance cost, closeness to vanilla, or mood; and once a pack is on, the button opens Iris' own options for it. Nothing is mirrored or bundled — most of these packs are all-rights-reserved.

→ Read more

Since: v0.842.0 (PR #1362)

A play-button icon in the title screen's icon column (between Credits and Discord) — green-dotted while a streamer is live (v0.878.0) — opens Dungeon Train Community Videos — the relay's curated list of every video the community has made about the game on YouTube, Bilibili, Twitch and Instagram. One toolbar row: platform icon toggles, a Twitch streamers toggle, a ★ dev-picks toggle, an uploader search box with a suggestion panel, and a Default / Views / Recent / Dev picks sort. Rows show a YouTube thumbnail or platform tile, uploader, views and how long ago (v0.878.0), and open in the browser; the Default order alternates the newest video with the top dev pick (v0.878.0); Twitch streamers are one purple row each, with a pulsing green dot when the relay has verified them live with the game right now. Players can submit a video link (it goes to a review queue) or, while live with "Dungeon Train" in their stream title, their own Twitch channel (verified on Twitch and listed at once) and flag a video as not about the game or not safe for kids — enough flags remove it from every list or from the kid-mode list, and one adult's "not safe for kids" pulls it from the kid list at once. Brennan's YouTube, Bilibili, Instagram and Discord sit bottom-right; Creator Tools and this page link to each other. Chinese-language clients get the Bilibili mark on the icon instead of a separate title-screen button. While OBS or Streamlabs is running, the icon slides out a "Streaming? Share link!" tag and the Submit button pulses red (v0.924.0). Click a platform icon to see only that platform, Shift-click to toggle just one (v0.1160.0). The same list is on the website at brennan.games/dungeontrain/videos/.

→ Read more

Since: v0.333.0 (PR #492)

The singleplayer pause menu's vanilla "Save and Quit to Title" slot becomes a single red "Abandon This Run" button — one click kills the player and opens the narrative death screen, ending the run like any in-world death (via a serverbound AbandonRunPacket → player.kill()). Holding Shift swaps the red button in-place for the normal exits: Exit to Title (grey) and Quit Game (dark grey), both of which save the world and reuse the death screen's Sable-safe teardown (no #679 "Saving world…" hang). Since PR #1741 (v0.1091.0) the Shift pair works on a server too: the vanilla Disconnect button keeps its slot and is swapped for Exit to Title | Quit Game while Shift is held (no Abandon button in multiplayer — there is no run to kill from the client).

→ Read more

Since: v0.178.0 (PR #223) · world-scoped + lock-on-first-read: v0.184.0 (PR #230)

Loads ~50 KB of long-form narrative content from the original itch.io game into Minecraft as signed-book + lectern primitives. Stories (~120 Letters) ship as JSON in data/dungeontrain/narratives/stories/, loaded through vanilla's ResourceManager datapack channel (PR #735 — a translation datapack can override any story/random-book/starting-book/death-lore file, replacing the earlier raw classpath scan which bypassed datapacks entirely). A narrative_lectern block always renders with a book on top; its first right-click resolves the world's next-unread Letter via BookFactory.buildOrRandomForLectern and locks that book to the BE for the lectern's lifetime. Subsequent clicks bypass resolution. Atomic resolve + read-mark on the first click ensures a one-and-done viewer advances the world cursor. World-scoped progression persists per-overworld via SavedData (<world>/data/dungeontrain_narratives.dat) — one cursor for all players, fresh per save. Read detection fires on both lectern right-clicks (PlayerInteractEvent.RightClickBlock) and inventory book opens (PlayerInteractEvent.RightClickItem), with each book stamped with its (story, letter) identity in DataComponents.CUSTOM_DATA. Books are paginated to vanilla limits (≤ 256 chars/page, ≤ 100 pages) on the paragraph → sentence → word boundary chain. New narrative content cell + narrative.group.json ship two themed sub-variants (dungeonnarrative, seatingnarrative) whitelisted in black/cracked/pen/windowed carriage templates. A one-shot Python converter at scripts/narrative/txt_to_json.py parses the original Unity-era .txt format (Letter X- chapters, Alt/Alt 2- variant blocks, [bracketed editor notes] stripped + captured with (variant, offset, text) records). First registered block in the codebase — uses BlockEntityTypeAddBlocksEvent to reuse vanilla LecternBlockEntity instead of registering a custom BE type. Commands: /dungeontrain narrative {list,book,lectern,spawnlectern,progress,reset,reload}.

→ Read more

Since: v0.315.0 (PR #427) · current layout v0.323.0

Replaces the vanilla DeathScreen on singleplayer Dungeon Train worlds with NarrativeDeathScreen — a slow, single-button retrospective the train tells about you, one page at a time over a cinematic cross-fade: the fall (this-life headline + cause of death) → the deeds (combat grid + an animated portrait of the passenger you befriended or killed) → the cargo (most-used weapon, worn armour, loot, books) → all your lives (cross-world lifetime totals) → the creator's ledger (one feedback-survey question per page) → the platform (Board anew / Leave the line). Pages sit over ride-photo backgrounds captured during the run. Death ends the run — there is no in-world respawn. Stats are tracked server-side per life by RunStatsEvents / PlayerRunState, rolled into a DeathStatsPacket (with the server-chosen DeathNarrative lore) and pushed to the client on death. Board anew launches a fresh random-seed save inheriting the dying world's settings + DT options; the transition mirrors vanilla "Save and Quit to Title" and pre-drains Sable train sub-levels before disconnect to avoid the Sable issue #679 chunkMap.hasWork() shutdown spin. Supersedes the pre-#427 two-panel run-recap layout. Since PR #824 reboarding also discards the old world by default — the run is one world per ride, so the dying world's chunk save is skipped (ServerLevel.noSave) and its folder deleted after teardown, keeping the world list and disk clean. A grey trash chip left of the top reboard chip toggles this (strike-through = world kept; persisted client-side as world.deleteOnReboard in dungeontrain-client.toml); since PR #1364 the chip is hidden unless Shift is held, and stays visible while the toggle is off so an opted-out player can find their way back. Deletion is double-gated: only auto-generated <prefix><epoch-millis> saves (Dungeon Train , Dev World , World ) ever delete — renamed/hand-made worlds and train editor N worlds are always kept. Since PR #1944 (v0.1241.0) a dead run's world also goes when the player leaves the death screen for the title or desktop, and the first title screen of every launch sweeps leftover auto-named worlds whose saved player is dead (level.dat Data.Player.Health <= 0) — covering a window closed on the death screen, a crash after death, a failed delete, and worlds left behind by older builds (client/RunWorldCleanup). Same toggle and name gate; runs quit alive are kept. While discarding, the teardown screen swaps vanilla's "Saving world" for a randomised "Leaving the world behind…" farewell (11 lines, all 21 languages; zh_cn/zh_tw draw from a 21-line pool that adds 10 classical Chinese sayings).

Since PR #1030 (v0.606.0) the vanilla Immediate Respawn game rule is honoured in spirit: with the rule on, dying skips the death screen entirely and performs the same reboard as Board anew — the world is discarded and a fresh run starts on the spot (InstantRespawnReboard, hooked via a ClientPacketListener mixin). Holding Shift while dying keeps the current game mode, matching the reboard chip. Singleplayer/host only — on a remote server the rule keeps its vanilla respawn-in-place behaviour.

Since PR #1735 (v0.1083.0) a submitted bug report gets an answer: once a real report (anything but No) is sent, a "train upgrades" page joins the flow right after the bug question. It reads the bundled changelog ledger — entries can declare addresses: lag | train_vanished (append-entry.py --addresses) — and says whether a newer release already fixes the reported issue (listing those releases), explains the known multiplayer issue, offers settings-aware lag tips, or simply thanks the player, with an "N releases behind" note when out of date. Buttons: See changes (the Versions page) and, while a CurseForge build is still in review, Get vX on Modrinth. The update itself lives on the platform page, where an Update to vX button rides beside Board anew whenever this launcher is behind. Editing the report drops the page until it is re-sent. Code: client/bugresponse/ (BugIssueClassifier, BugResponse, BugResponseCard, LagTips, ReleasesBehind).

Since PR #1737 (v0.1086.0) a bug reported with /bug or /feedback (Discord Presence's survey) gets the same answer in chat, prefixed [Dungeon Train]: same decision (BugResponseCard.decideNow), same strings. It posts once the survey has closed and the version listings have arrived (10 s cap, so an offline report is still answered). The card's buttons become clickable links: Update to vX and Get vX on Modrinth open the modpack page; See changes and each lag tip's button run /dt-bug-response, a client-only command that never reaches the server or run integrity. An answer longer than six messages is sent in two parts, the headline first and the details four seconds later. The "Other" comment is read off the survey screen, so derail / two trains still give the known multiplayer issue on a server. The death screen never fires the survey hook, so it never doubles up. Also since #1737: a lag report with no applicable tip (common on a server) gets a plain "Thanks, your report was sent." on the card and in chat. Code: BugResponseChat (lines), BugResponseChatNotifier (hook, timing, command).

→ Read more

Crash recovery — salvage a crashed run into the Ender Chest

Since: PR #1364 (v0.842.0)

A run only ends properly through the death screen; a client crash (OOM, driver fault, force-quit) leaves the save on disk with no way back to your gear. Now the next launch detects it and, at the title screen, offers to put you back into that world for salvage only: reach an Ender Chest and stash what you can — the Ender Chest (EnderChestPersistence) is the one thing that carries to the next run. The card says plainly that a world saved mid-crash will be buggy and should be abandoned for a fresh run once you've stashed. Buttons: Salvage / Not now (asks again next launch) / Discard (forgets the run; the save is left alone).

Detection is a dead-man's-switch file, not crash-log parsing: <gameDir>/dungeontrain/session/run-state.json is written on joining a singleplayer Dungeon Train play world (CrashRunTracker) and deleted on every clean exit — quit to title / fresh world / Abandon (LoggingOut), closing the window (GameShuttingDownEvent), and the narrative death screen opening. Surviving a restart means the last session crashed. The offer is skipped when the save is gone, and when the run was already Free Play (SaveFreePlayProbe reads allowCommands, the run_cheated attachment and the game mode from both playerdata/ and the owner copy embedded in level.dat). Salvage pre-answers vanilla's "experimental settings" card (ExperimentalWarningSeal sets the same level.dat flag as I know what I'm doing!) so the reopen goes straight in. Inside a salvage session a top-centre banner keeps the reminder up and the pause menu gains Start a Fresh Run, which rolls a new world via launchWorld rather than the kill-the-player Abandon path. Train Editor / builder worlds and multiplayer are never tracked. Client-only; no packets, mixins or save-format changes.

Since: v0.102.0 (PR #140)

Replaces the vanilla rotating cubemap behind the main menu with six in-world captures from a Dungeon Train world. Pure resource override at assets/minecraft/textures/gui/title/background/panorama_<0..5>.png — Forge resource precedence beats vanilla at the same path, so the existing PanoramaRenderer keeps spinning and blurring the new images with no mixin or code path. Pairs with the main menu logo replacement shipped in v0.101.0.

→ Read more

Since: v0.7.0 · latest: v0.73.0

Displays Dungeon Train v<version> (<branch>) in the top-left corner of both the in-game HUD and the main-menu title screen. Hidden on release builds (jars built on main) — the overlay only shows on feature branches and dev builds (PR #99). Values are baked at build time from mod_version and the current git branch, so spot-checking which build is installed never requires loading a world. v0.34.0 extends the in-game line with a live — Carriage: +N suffix showing the player's closest train carriage index (0 = origin, positive forward, negative back) — pushed by the server on change, cleared when the player drifts > 128 blocks from any train.

→ Read more

Since: v0.858.0 (PR #1381)

Clicking the top-left version label now opens a Versions page: the installed build (You are on v0.800.0 — 54 versions behind (25 releases)), the newest modpack release on your launcher, the newest on the other launcher when it is ahead — with the words CurseForge is delaying Dungeon Train releases when CurseForge's review queue has left it behind Modrinth — and a Companion mods check of AIN/AIS/PMOB/ECP/TE against their newest builds. The selected row's release notes sit beneath (cumulative for a newer release; fullscreen with a tab per version), and the page opens by itself at launch when you're behind. Modrinth is the yardstick; CurseForge is read from the keyless cfwidget mirror. v0.861.0 (PR #1384) adds tag filtering: every release-notes entry carries tags (New Feature, Bug Fix, Editor, Train, World, Loot, Books, Mobs, Community, … 17 in all), a row of chips above the notes shows each with its count most-to-least, and picking one or several keeps only those changes — in fullscreen the chips describe just the selected version. Release announcements open with the same tag counts.

→ Read more

New-version chat notice

Since: v0.984.0 (PR #1605)

When a new Dungeon Train release drops while you're in a world, a chat line tells you: [Dungeon Train] Version 0.984.0 is out! [What's new] - it dropped 7 minutes ago. The gold line names the version, the aqua [What's new] link opens the GitHub release, and the grey "dropped … ago" is worked out on your client at the moment the line appears from the release's GitHub published_at (seconds → minutes → hours → days, localized in all 21 languages; left off if GitHub can't be reached). Client-only: each client checks the repo's update.json (promos.1.21.1-latest) every 10 minutes while in a world — no server code or packets, so it behaves the same in singleplayer and on servers. Live only — the first check of a session records what's already out, and only a newer release announces, once; joining on an old version relies on the Versions page instead. Real releases only — major/minor bumps, never the ~22 auto-release cascade patches. Toggle: Options → Dungeon Train → New Version Chat (on by default; updates.updateNoticeChat in dungeontrain-client.toml). Dev seams: ./gradlew runClient -PupdateFeedUrl=<url> -PupdatePollSeconds=<n> -PreleaseApiUrl=<base> (also enables the notice on dev builds, which are otherwise silent).

Outdated-version chat notice (once per game)

Since: v0.1165.0 (PR #1841) + dp-relay PR #670

Joining a game on an outdated build prints one line, once per game: [Dungeon Train] You're 4 releases behind (v0.1137.0 → v0.1161.0). [⬆ Update to v0.1161.0]. The prefix is gold, the count yellow, and the aqua link opens the update page with ?v=<installed>&from=<launcher>. The count is the same as the title-screen Versions card's (NewestRelease): real MAJOR.MINOR releases across both launchers, never cascade patches. The line waits until the player is in the world with no screen open, so it never fires under the "Press Space to Start" loading screen. A game is the save folder plus seed in singleplayer, or the server address in multiplayer, kept in outdated-notice-seen.json under the player-data root (newest 200). It reuses already-translated strings, so it renders in every shipped locale. It uses the same toggle as the new-version notice (New Version Chat). Dev builds are silent unless run with -PmockInstalledVersion=<old>. Code: client/version/OutdatedVersionNotice, OutdatedNoticeSeen.

Older builds (below 0.1165.0) have no such code, so the relay reaches them. On their /telemetry/world-info it sends the same message once per (uuid, worldSeed, trainSeed) in plain text, translated from the vendored DT strings. Language comes from world-info: English if blank, Chinese if blank on 0.451.0/0.458.0. Two copies go out:

  • one to the title-screen inbox;
  • one as an in-game <Dungeon Train> … chat line, through Discord Presence on the player's thread. It isn't attributed to the dev, so there's no @Dev consent prompt.

On old builds the in-game line lands about 1 s after join, under the loading screen, so players see it if they open chat. Relay env: OUTDATED_NOTICE_CLIENT_SINCE (first DT build that prints it itself) and OUTDATED_NOTICE_ENABLED=false (kill switch). /health → outdatedNotice, releaseMinors.

Since: v0.227.0 (PR #279) · latest: v0.290.0 (PR #358)

The train gets harder the further you ride. Every hostile mob spawned in a carriage rolls armor / weapon / enchantments / potion effects scaled to a difficulty tier = abs(travelledCarriageIndex) / carriagesPerTier (default 20 carriages per tier), derived from the furthest-progressed boarded player this life — reset on death, lowered by walking the train backward. v0.230.0 replaced the original 5-rung tiers.json ladder with a smooth procedural curve (ProceduralTiers): overlapping leather→chainmail→iron→diamond→netherite material bands, ramping slot/weapon/enchant chances, and potion effects that layer in (Speed @ tier 15, Strength @25, … Slow Fall @90) with +1 amplifier every 25 tiers. Rolled gear is AIN-named + AIS-statted, hostile-only, and PlayerMobs scale on the same tier (no effects). v0.290.0 caps carriage villager trade level to the dominant mob weapon stage — none→1, wood→2, stone→3, iron→4, diamond→5 — so merchant strength tracks the mobs you're fighting. v0.416.0 keeps gear scaling past the netherite / level-50 material cap — a per-tier attack/armor stat bonus so late-game mobs stay dangerous instead of plateauing (difficultyScaleHostileGearPastCap, default on). Both default-on; difficultyEnabled / carriagesPerTier live in dungeontrain-server.toml.

→ Read more

Since: v0.504.0 (PR #828)

Every villager gains a Trade Anything slot at the top of its trade list — insert any item and it exchanges for goods the villager buys, priced by real material worth (recipe + smithing derivation, durability, enchantments), with an emerald → emerald-block payout ladder, purchase-time emerald-block change-making, and 10%-margin stock buy-back. Powered by the new companion mod Trade Everything (bundled via jarJar). On top, villager trades now scale with the villager's own carriage: levels cap by local mob gear stage, and from carriage 30 sold gear keeps improving past vanilla Master — re-enchanted at powers past the 30-level table, AIS-statted slightly ahead of the local tier, and priced by enchant value. ⚠️ Balancing under active review.

→ Read more

Pigman villagers (Pigman Villagers)

Since: v0.973.0 (unreleased) (PR #1579)

About 1 in 100 villagers is a pigman: an ordinary villager drawn with the player model wearing a pig-headed skin ("EmbeddedPigman"), with pig sounds instead of villager sounds (ambient, trade and "yes" → pig oink; "no" and hurt → pig hurt; death → pig death; job-site work sounds unchanged). It trades, works, breeds and gossips exactly like any villager — it is a flag on the vanilla villager, not a new mob. The roll happens once per villager (on its first tick) and is saved, so a pigman stays a pigman across reloads and when an unloaded carriage comes back. Villagers stamped from a template — carriage contents, part decor, dimensional carriages, shared carriages, and vanilla structure templates — each roll their own chance instead of copying the villager the template was captured from. Chance is set in config/pigmanvillagers.properties (chance=0.01); force one with /summon minecraft:villager ~ ~ ~ {PigmanVillager:1b}. Provided by the sibling mod Pigman Villagers, built into the Dungeon Train jar. See Compatibility → Pigman Villagers.

The Killer Bunny returns

v0.1033.0 (PR #1671). About 1 in 250 freshly spawned rabbits — in a carriage or anywhere in the world — is The Killer Bunny: vanilla's hostile rabbit variant (white with red eyes), which hunts players and wolves and hits hard (8 attack, 8 armour). Vanilla still ships the variant but stopped spawning it naturally; KillerBunnyEvents brings it back by calling Rabbit#setVariant(EVIL) on EntityJoinLevelEvent. Each rabbit rolls once (scoreboard tag dungeontrain_killer_bunny_rolled), on its fresh spawn only, so reloads never re-roll or stack its goals; a saved killer bunny stays one. Never on Peaceful, and never during the opening no-hostiles stretch. Chance is SERVER config difficulty.killerBunnyChance (default 0.004, 0 disables).

Monty Python names — v0.1089.0 (PR #1740). A fresh Killer Bunny then rolls SERVER config difficulty.killerBunnyNameChance (default 0.1, twice AIN's 5% passive-mob naming rate): a win names it from the 30-name pool in KillerBunnyNames — Rabbit of Caerbannog, Tim's Pet, Run Away, The Holy Hand Grenade of Antioch and friends — replacing any Adventure Item Names fantasy name. A miss leaves the rabbit as it was: AIN's name if it had one, otherwise the plain "The Killer Bunny" that vanilla's setVariant(EVIL) applies. The pool is deliberately not localised (proper nouns and quotes). 0 never names it, 1 always does.

Since: v0.236.0 (PR #292) · latest: v0.310.0 (PR #413)

Two custom advancement tabs — Dungeon Train (player milestones: riding, exploring, chest streaks, narrative reading) and Editor (content-authoring milestones). Both persist across worlds via a per-UUID JSON sidecar at <minecraft>/config/dungeontrain-achievements/, so earning a milestone in one save unlocks it on every subsequent save — and since v0.310.0 (PR #413) this extends to every advancement, vanilla Minecraft and other mods included (the hidden recipes/* tree stays per-world). The carts-per-life tier (Dungeon Train Explorer / Long-Haul Passenger / Nearing Infinity) is driven by PlayerRunState counters reset on death; time-on-train milestones (Settling In / Enjoying the Ride / I Live Here Now) accumulate cross-world via GlobalPlayerStats. v0.245.0 redefines the carts counter as absolute (forward + backward both count) and adds The Long Way Back — a side-branch goal off Dungeon Train Explorer that requires ≥100 carriages forward AND ≥100 carriages back in a single life, gated by a new dungeontrain:carts_both_directions trigger. v0.268.0 adds four dimension advancements chained into one lineage — Inter-Dimensional Passenger / Void Passenger (board the train in the Nether / End) and Nether Return Again / End of the Line (read every Nether / End starting book) — and promotes Inter-Reality Passenger to the grand slam over every book including those dimensions. Book-set membership is now folder-driven (StartingBookContext.achievementSetId()), so new sets need no engine code. v0.271.0 adds Come Along With Me — the first multiplayer advancement, granted on login when you join a multiplayer world or someone joins yours (every online player is awarded, via the new dungeontrain:multiplayer_join trigger). v0.301.0 adds a first-run keybind hint — earn a Dungeon Train advancement before you have ever opened the advancements screen and a grey chat line reminds you of your (rebindable) advancements key; it picks from twelve train-flavoured lines and stops for good once you open the tab. v0.502.0 adds Cover me in ...leather? (PR #830) — a challenge under Dungeon Train Explorer for wearing a full leather set whose AIS-rolled armor total beats a full vanilla diamond set (>20); realistically earnable around the first Nether band, where loot rolls scale past it.

Per-difficulty profiles — v0.1292.0 (PR #2010). Like the Ender Chest and carried loadout, the cross-world advancement profile is now kept per vanilla difficulty (AdvancementProfiles). Normal keeps the original <gameDir>/dungeontrain/achievements/<uuid>.json, so everything earned before the split lives on Normal; Peaceful/Easy/Hard use <uuid>.<difficulty>.json and start empty. The Editor tab is shared across all difficulties (<uuid>.shared.json). Changing difficulty mid-world banks the current tree and swaps in the new difficulty's silently (no toasts or announcements); tab roots and tab-gateway copies stay put. A per-player advancement_partition attachment records which profile a world's tree mirrors, so pre-split worlds and difficulty changes made while offline are reconciled on login. Gated by the same difficultyIsolatedStash server config as the Ender Chest split. On the first start of this version, AdvancementSplitBackup backs up any pre-split profiles first (PR #2014): a Dungeon Backup restore point (before-advancement-split, listed by /dtrestore) plus a never-pruned copy in achievements/pre-split/; a .difficulty-split marker makes it once-per-install.

One-time promote from the relay: added in v0.1306.0 (PR #2031, relay dp-relay #804). This is for a player who earned most of their pre-split advancements on another difficulty. The developer sends them a promote from the explorer's player page (🏆 Advancement promote), choosing a target difficulty and either Player chooses each (copy) or Remove from Normal (move). At the player's next title screen, AdvancementPromotePopupHandler opens Promote Advancements. It lists the pre-split backup (or the Normal profile when there is none), minus the shared Editor tab and anything the target already holds. The player ticks which ones count there and, for a copy promote, picks Copy or Move per row. AdvancementPromotion writes the files at the title screen, where no world is running, and never touches the backup. The promote is single-use. Promote or Decline records the id in achievements/.promotions-used and acks the relay through RelayOutbox (POST /advancements/promote/ack), while Not Now offers it again next launch. The game fetches it with GET /advancements/promote?uuid=, and only with network consent. Dedicated servers aren't covered, because the profile lives on the server.

Changing difficulty mid-run — v0.1311.0 (PR #2051, relay dp-relay #810). A run is a world, so changing difficulty in a singleplayer run (vanilla Options button, /difficulty, or the new Options › Dungeon Train › General › Difficulty row) opens a prompt with Save for next run (the current world keeps its difficulty; the next one starts on the new one) or Abandon run & restart now (ends the run the way pause-menu Abandon does and reboards straight into a fresh world on it, with no death screen — InstantRespawnReboard.expectRestartRun). Remote servers and the Train Editor offer Change difficulty in place instead; a dedicated server with difficultyIsolatedStash off changes instantly. The chosen difficulty is remembered client-side (dungeontrain-client.toml → run.newRunDifficulty) and read by the death-screen/immediate-respawn reboard, the title screen's New World and vanilla Create New World (CreateWorldScreenDifficultyMixin); empty means the first reboard keeps the dying world's difficulty. Hardcore stays on Hard. A keepInventory carry that lands on another difficulty is banked into the locker of the difficulty it was captured on. The relay now keeps advancement stats per difficulty: every clean earn's first-claim carries difficulty, tallied in adv_by_difficulty, and the explorer's Advancements page shows Peaceful/Easy/Normal/Hard columns (dev builds never record).

→ Read more

AIS data integrity — session Free Play on changed settings

Since: v0.501.0 (PR #829)

Dungeon Train's balance is tuned against Adventure Item Stats' standard settings. At every server start, AisDataIntegrity compares config/adventureitemstats.properties against the installed AIS build's own defaults (read live from AIS's AisConfig.defaults() — always consistent with whatever AIS version the release ships, with a guarded built-in fallback). When the settings deviate, the whole session runs in Free Play through the existing RunIntegrity.isCheated seam: advancements/stats don't persist to the cross-world profile, the Ender Chest swaps to the Free Play slot, and the Free Play effect shows. Session-only — restoring the standard settings restores normal play on the next boot; nothing is written to the world or players. (Cheating during a tainted session still records the permanent RUN_CHEATED flag, quietly.) The join notice explains the cause, lists the exact deviating keys, and ends with a clickable line running /fixaisconfig — open to everyone (it can only write the known-good defaults, and it's allowlisted so clicking never taints), it backs up the old file to a timestamped .bak-* sibling before restoring and reports the backup name in chat.

DT config integrity — Free Play on changed balance settings

Since: mid-run check + bundled siblings in v0.983.0 (PR #1604)

DtConfigIntegrity holds DT's balance settings to their shipped defaults, the DT-side twin of the AIS check. Performance, visual, privacy and content-restriction settings are deliberately not watched: turning off telemetry or restricting content never costs a run.

What's watched Where Takes effect
Train speed, generation mode, group size; every [difficulty] key; Player Mob spawn rates; break-blocks-on-contact dungeontrain-server.toml, dungeontrain-common.toml Live
Edible Backpacks resetOnDeath, maxSlots ediblebackpacks-server.toml Live
PlayerMob combat and scavenging (TNT/End crystal/fire combat, arrows, engage ranges, container/armor-stand/floor looting, hunting, dig-through, natural spawns, extra pickups, modded ranged weapons) playermob.properties, via PlayerMobConfigCheck comparing PlayerMob's live getters against its DEFAULT_* constants Next game launch

Two ways it trips:

  • At world start, deviations make the whole session Free Play (session-only): restore the defaults and the next boot is clean. The join notice lists the changed keys and links /fixconfig, and the title-screen prompt offers the same reset. It moves every governed file aside to .bak-<stamp> (DT's two, AIS, Edible Backpacks, PlayerMob).
  • Mid-run, a deviation the session did not start with permanently marks every online player's run Free Play (RunIntegrity.markCheated), the same as the /dungeontrain tuning commands. This closes the "change it, play, change it back before quitting" hole that in-game config editors such as Configured, or DT's own settings screen, would otherwise open. It's detected by DT's ModConfigEvent.Reloading plus a 5 s sweep that only re-parses a file whose modification time changed; the sweep also catches Edible Backpacks, whose reload event fires on its own mod bus. No confirmation prompt: the change is already live when DT sees it. Choices made on the Create World screen are rebaselined in WorldLifecycleEvents so they never taint the creator.

Deliberately unchecked siblings: Ender Chest Persistence store-location (a storage choice; off only removes cross-world persistence), Pigman Villagers chance (cosmetic), DiscordPresence, Dungeon Backup and Edible Backpacks' client UI settings.

Mod whitelist — session Free Play for unsupported mods

Since: v0.980.0 (PR #1594) · relay bh679/dp-relay#606

Free Play's mod check works from an approved list (whitelist) instead of a list of banned mods. At every server start UnapprovedModIntegrity scans the installed mods against ApprovedModList — the baked list (generated at build time from Dungeon Train's own dependencies, its siblings and the modpack roster, plus the hand-curated src/main/whitelist/approved_mods.curated.json), plus approvals and revocations served by the relay (GET /<cap>/approved-mods). Any mod that is neither approved nor a known cheat mod makes the session Free Play (cause unapproved_mod, session-only like the AIS check). Known cheat mods keep their own "known cheat mod" notice (CheatModIntegrity).

Enforcement ships ON in the jar; the relay can switch it off with an explicit enforce:false (the kill switch, cached for offline boots). A payload that doesn't mention enforce keeps the current value. ⚠ The prod relay's enforce override must be true when a release that includes this ships, or every online player's first fetch turns enforcement off.

Unsupported Mods screen (client/modcheck/). Once per launch, on the title screen (it waits up to 5 s for the whitelist fetch and never covers another screen), players with unsupported mods see them in a two-column list, the Free Play effect icon and what Free Play means, "To play a normal / live run, quit and disable these mods.", and a link to the public list. Continue (red) carries on in Free Play; Quit and Disable renames each unsupported mod's jar in mods/ to .jar.disabled — the CurseForge/Modrinth convention, so the launcher shows it disabled — then quits. Only jars whose every mod is unsupported are renamed, and only in-process — nothing is spawned or scheduled to run after exit (CurseForge moderation rejects that). On Windows the running game holds its jars open, so the rename usually fails there; whenever nothing can be disabled from in-game, the screen says so and the button becomes a blue Quit Game pointing players to their launcher.

In-game votes. Each mod has a + button: pick what it is — Whitelist (doesn't impact gameplay unfairly), Modpack (should be in the modpack) or Cheat (gives unfair advantages) — and say why (required). It goes to the relay (POST /<cap>/mods/suggest) under the player's own name; a Microsoft-signed-in client also proves itself through the owner-proof handshake, which marks the vote verified. Whitelist starts or joins a suggestion, Modpack is a modpack vote, Cheat is a Rejected vote with reason helps cheat.

Public list and votes — brennan.games/dungeontrain/mods: Discord members vote any mod Whitelist / Modpack / Rejected (with a comment; Rejected picks helps cheat / unrelated / other). Undecided mods with votes sit on the Voting tab; the operator's Approve / Reject / Modpack suggestion in the explorer's Mod requests queue moves them to the list, Rejected or Modpack suggestions.

Free Play Ender Chest — Expand to 114 slots

Since: v0.883.0 (PR #1412)

In Free Play the Ender Chest — already locked onto Ender Chest Persistence's separate Free Play slot so cheated items never reach a legit stash — gains an Expand button. Pressing it grows the chest from 27 to 114 slots, (5 + 9 + 5) × (3 + 3): the vanilla 9×3 stays where it is at the bottom-centre, three rows are added above it inside the (now double-chest-sized) window, and a five-column wing spanning all six rows hangs on each side, drawn the way Edible Backpacks draws its panels. Existing items don't move: slot indices 0–26 are the vanilla 27 in both sizes. A Shrink button replaces Expand once expanded and is enabled only while every extra slot is empty (the server re-checks), so nothing can ever be orphaned. The expansion follows the player to every Free Play world; legit runs keep the vanilla chest and never see a button.

How: the Free Play chest opens through DT's own menu type dungeontrain:ender_chest (FreePlayEnderChestMenu / FreePlayEnderChestScreen, slot geometry in the pure, unit-tested EnderChestLayout). Capacity is changed by swapping the player's live PlayerEnderChestContainer for one built at the wanted size — the class hard-codes super(27), so PlayerEnderChestContainerSizeMixin swaps that constant for a thread-local that EnderChestExpansion raises only around its own construction, and PlayerEnderChestAccessor installs the result. A per-player flag lives at <instance>/dungeontrain/enderchest/<uuid>.dat; see the Compatibility note for how it is restored at login relative to ECP.

Since: v0.309.0 (PR #411) — via Discord Presence 0.19.0

A Give Feedback button on the death screen opens a short survey that walks the player through one question at a time — a 0–10 rating + optional comment each — posting every answer to the community Discord. DT ships one question ("How would you rate the difficulty progression?") after Discord Presence's built-in NPS question; an answered question is never re-asked, and the button hides once everything's answered. The survey system lives in Discord Presence and loads questions from data/<ns>/dp_surveys/*.json data files, so any mod or datapack can add more with no code.

→ Read more

Since: v0.544.0 (PR #889)

If you run mods beyond Dungeon Train and its modpack, the death screen gains a page asking which of them you'd recommend to the creator — a scrollable grid of exactly those mods, ordered by how many other Dungeon Train players run each (from the relay's /mods/popular aggregate; the counts themselves are never shown). Pick a tile, say why in a line, send; the tile sinks to the bottom and you can go again as often as you like. A pinned tile takes the name of a mod you don't have but wish the train supported, and each request leaves its own card. A comment is required — a bare tile click isn't a recommendation — and the page never appears at all on a clean modpack install. Comments reach the private Discord survey channel only; the relay stores the modId and nothing you typed.

→ Read more

Since PR #1787 (v0.1123.0) the death screen has a page just before the final one that shows off the life's photos: the player's own disposable-camera shots first, then the run's ride photos, in one horizontally scrolling strip (mouse wheel, or the ‹ › end arrows — each click centres the next photo). Clicking a photo opens it fullscreen with a save icon top-right that writes a PNG to screenshots/; ‹ › or the arrow keys step through, Esc / right-click / clicking off the photo closes. The page is absent when the life has no photos. Camera frames are recorded server-side per life (PlayerRunState, cap 24, in memory) and ride to the client in DeathStatsPacket; the client pulls each image through Exposure and caches it as its own texture. The top-bar photos chip on the final page still opens the older ride-photo grid. Code: client/deathphotos/.

Since: v0.347.0 (PR #533)

As the train travels, the world periodically breaks apart into a repeating cycle — Overworld → Void → End islands → Void → (repeat forever) — anchored to a fixed block distance from spawn. The void stretches show the floating track suspended in empty space with the sky and fog turned to the End; the island stretches are real End world-gen (islands generated from the actual End noise router and trilinearly interpolated like the End's own NoiseChunk, with vanilla chorus plants clustered in end_highlands patches). Only endermen spawn in the broken bands, and the train rides level through the whole thing on its preserved bed + rails. Phase lengths are config-driven (default 500 blocks each).

→ Read more

Since: v0.577.0 (PR #997)

The Nether crossing has looked like the Nether since v0.354.0, but generated no structures — you rode through empty country. It now grows all four vanilla Nether structures: fortresses (with their blaze spawners, wither skeleton halls and loot), bastion remnants (piglin brutes, treasure rooms, Pigstep, the netherite upgrade template), nether fossils in soul sand valleys, and ruined portals. These are the real vanilla structures re-sited in place, not copies, so /locate structure minecraft:fortress finds them and their loot and spawns are vanilla's. Fortresses stand on their own bridge supports over the lava; where one lands across the line, the train tunnels straight through it. Deliberately rarer than the real Nether — a straight corridor samples structures far more densely than wandering does. Toggle with netherStructures.

→ Read more

Netherrack face on the Nether band's walls

Since: v0.951.0 (PR #1531)

The real-Nether core carves its caverns right up against the transition mountain, whose body is overworld stone. From inside the Nether that used to show as grey stone cliffs. Now the one-block face of rock next to open core space (air or lava), plus the single block past any open top or bottom row of the core's Y band, is repainted netherrack. The mountain behind the face, and the train tunnel through it, stay stone right up to the edge. Openness is read from the same NetherCoreGeometry density the core is stamped with, so the face is exact across chunk borders. Code: NetherRockCover, NetherTransitionFeature.coverCoreFacingFace / coverCoreCaps. Only newly generated chunks are affected.

Caverns, deepslate and an ancient city in the Nether transition

Since: v0.1029.0 (PR #1665)

The transition mountains used to be solid rock under the ridge (the density raise filled every natural cave) and were forced to highland biomes above sea level, so no cave biome could appear there. Now big open caverns run through the mountain interior — CavernNoise, a 3D field carved out of the finalDensity wrapper in the window [seaLevel+4, min(top−24, 250)], evaluated at noise-cell corners so the per-block wrapper stays cheap. Where a cavern crosses the corridor the stone-brick tunnel stops and the track runs through on pillars. The caverns keep a jittered ~10-block solid margin before the real-Nether core and pinch shut over the next 8 blocks (WorldGenCycle.netherCoreGap). The interior under that window is labelled a cave biome (BandBiomeDecision.caveWindowTop, carried on the memoised biome column): lush caves / dripstone caves on the way in, and after the core mostly deep dark (three quarters of 512-block regions) with lush / dripstone blending in the rest. The aquifer's floodedness noise is forced to −1 in raised columns (NetherBandFloodednessDensityFunction), so caverns and the rails through them never flood.

Over the core-ward half of the approach, stone and stone ores become their deepslate forms behind a wavy ±48-block front (DeepslateFront). The netherrack crossfade skin gets one block of obsidian under every netherrack cell that sits on rock, and the core wall now reads stone → exactly one obsidian → netherrack on open and solid faces alike. DT's Nether mob spawner never places a mob in a deep_dark quart (wardens still come from shriekers). One ancient city per Nether pass — dungeontrain:ancient_city, vanilla's city_center pool anchored 20 blocks above the track bed — stands in a fall-side deep-dark region chosen by AncientCitySite: the first pass has a 50 % chance, every later pass always has one, and the chosen region is forced to deep dark. Gated by netherStructures. Always on otherwise; only newly generated chunks are affected. /dungeontrain debug nether-passes gives each pass's core X range; the rise-side mountains are the ~1000 blocks before it.

Clocks and compasses spin in the otherworldly stretches

Since: v0.1008.0 (PR #1636)

The bands are overworld, so clocks used to keep showing the time of day and compasses kept pointing to spawn. Now both spin at random, as they do in the real End and Nether:

  • inside the End and Nether bands, from about where the band's music takes over (intensity ≥ 0.5);
  • from the Far Lands (halfway into the Beta → Far Lands crossfade) to the end of that run. Every run restarts at the overworld, so they work again there.

Lodestone and recovery compasses keep their targets. The check reads the holder's position, so a clock in an item frame inside a band spins whoever is looking at it. The change is client-side only and off on non-train worlds. Code: OtherworldBand, ClockBandSpinMixin, CompassBandSpinMixin, WorldGenCycle#isAtOrPastFarLands.

BetterNether in alternating Nether bands

Since: v0.936.0 (PR #1501)

Every second Nether band (the 2nd, 4th, 6th… crossing, i.e. odd cycle passes) generates its core from BetterNether: New Dawn biomes; the 1st, 3rd, 5th crossings stay vanilla. The core is cut into 300-block cells along X, each taking the next biome from a world-seeded shuffle of BetterNether's standalone biomes (edge/terrace/cleared sub-biomes excluded), with cell borders waved along Z — a 5000-block core visits every biome, never the same one twice in a row. BetterNether's own placed features (trees, vines, plants, ores) run through the normal core decoration pass; floors get a per-biome skin mirroring BetterNether's WorldWeaver surface rules (BetterNetherSurfaceSkins); ground mobs come from the biome's own MONSTER spawn list (naga, skulls, jungle skeletons…); fog uses each biome's colour. Vanilla passes sample a private vanilla-only Nether source built from MultiNoiseBiomeSourceParameterList.knownPresets() — WorldWeaver injects BetterNether into both the live Nether source and the registered Nether preset, so neither can be sampled directly. /dungeontrain debug nether-passes prints each band's core X range and biomes. BetterNether (+ BCLib, WorldWeaver, WunderLib) is a required dependency — see Compatibility.

BetterEnd in alternating End-islands bands

Since: v0.937.0 (PR #1503)

Every second End-islands band (the 2nd, 4th, 6th… crossing, i.e. odd cycle passes) is BetterEnd: New Dawn instead of stamped end stone; the 1st, 3rd, 5th stay vanilla (end stone islands, chorus, End cities). For those passes the band copies real End chunks: dedicated sampler threads run the End's own generator (noise → carvers → full biome decoration, so BetterEnd's trees, moss, lakes and crystals all come through) in the outer End at x + 16000 + pass × 4000, and the End's island Y band is shifted onto track level (End Y 56 → bed Y). Finished samples are written into the live chunk with raw section writes, only into air, clear of the track lane and train airspace, thinned across the band's fade edges. Band chunks just past each player's view distance are prefetched, so terrain is usually there on arrival. Column biome labels on those passes are the matching BetterEnd biome (fog/particles). Code: EndBandStyle, EndBandSampler, WorldEndBandEvents. BetterEnd is a required dependency — see Compatibility.

WWOO and Biomes O' Plenty in the second-lap overworld

Since: v0.938.0 (PR #1506)

On every odd lap of the world-gen cycle (cycle index 1, 3, 5… — the same parity as the BetterNether and BetterEnd bands above) the overworld gap before the Nether band takes its look from William Wythers' Overhauled Overworld and the gap after it from Biomes O' Plenty. Lap 0, even laps and every other stretch stay vanilla. At default config, lap 1 is WWOO over X 99,908–109,908 and BoP over X 117,036–127,036 (period 104,908). /dungeontrain debug overworld-laps prints each lap's ranges, a biome census sampled from the overworld source, Nether/End-core spot checks and the confinement counters. Rule: SecondLapOverworld + WorldGenCycle.overworldGapAt.

The two mods are confined in different ways because they work differently:

  • BoP adds its own biomes (through TerraBlender), so it is confined at biome choice: DT picks every overworld biome from its own climate tables — vanilla's preset table outside the BoP stretch (identical to a pre-BoP world, so existing saves keep their layout) and per-BoP-region tables inside it, laid out by TerraBlender's own region noise, with BoP's "deferred" points falling through to vanilla. Code: OverworldStretchBiomes. BoP's Nether and End biomes are re-sampled to vanilla inside DT's Nether/End bands (the real Nether and End dimensions do get them).
  • WWOO is a pure datapack that rewrites 53 vanilla biome files in place (features, colours, climate, spawns) and overrides a few vanilla features (lava lakes; its auto-loaded remove_ores pack), so choosing biomes can't confine it. Instead, outside its stretch: (1) its placed features are vetoed during decoration and the vanilla feature lists — rebuilt from code by VanillaWorldgenLookup, plus whatever NeoForge biome modifiers added — are placed step by step with a BiomeFilter override (VanillaBiomeFeatures, WwooDecorationPass); (2) each rewritten biome answers as its vanilla twin for grass/foliage/water/sky/fog colour, rain/snow/ice climate, ambient sound, music, particles and mob spawns — position-gated where the call has a position, camera-gated otherwise (VanillaBiomeTwins, BiomeMixin, ChunkGeneratorSpawnsMixin for ongoing spawning, NaturalSpawnerChunkGenSpawnsMixin for the animals a fresh chunk generates with — which reads the biome's list directly and never asks getMobsAt — and the same gate for a dimensional carriage room's natives, v0.955.0). Both layers are scoped to the files WWOO's jar ships (WwooDatapack) so other mods' edits to vanilla biomes (BetterNether, BetterEnd) stay everywhere. Still global: WWOO's five minecraft: block-tag edits and its stray pale_garden biome entry.

Transitions wear the neighbouring look (v0.960.0, PR #1555). The overworld-looking part of a band transition takes the look of the modded stretch it borders, so the look doesn't stop at a hard line:

Transition Carries the look of
Upside-down Reassembly + exit gap (the mirror section stays as it is) the overworld gap after it
Nether beach, mountain stages and core crossfade, on each side the overworld gap on that side
End overworld→void erosion fade, on each side the overworld gap on that side

In the default order that means WWOO carries through the Reassembly and up the Nether climb, and BoP carries down the Nether descent and into the End erosion. Nether mountain stages that border BoP climb through a BoP highland palette (NetherBandBiomes.BOP_ZONES: seasonal/maple/woodland → coniferous/highland/fir clearing → snowy coniferous/fir/maple; the bare peaks stay vanilla). SecondLapOverworld.lookAt is the look a column wears and drives decoration, twins and the biome pick. SecondLapOverworld.at stays the stretch itself (/dtp wwoo|bop, debug, dimensional-carriage sample sites). The rule is WorldGenCycle.bleedingOverworldStyleAt, which needs an ordered layout; classic single-period cycles get no bleed. The last line of /dungeontrain debug overworld-laps lists the transition ranges that carry a modded look.

Both mods (+ Cristel Lib, TerraBlender, GlitchCore) are required dependencies — see Compatibility.

No spawner structures in the upside-down band

The upside-down band (with its entry lead-in and exit fade) generates no dungeons (monster_room, monster_room_deep), trial chambers or mineshafts (mineshaft, mineshaft_mesa). Mirrored into the ceiling, their spawners dropped mobs past the train, and BandMobSpawnEvents can't cancel them because spawner spawns aren't natural spawns. Strongholds are kept, since the End portal and the stronghold rings depend on them. UpsideDownSpawnerStructures holds the ids and a per-chunk band gate, wired through the same seams as LegacyUnderground: StructureBasementMixin declines the start and ChunkGeneratorDecorationMixin vetoes the features and clips pieces reaching in from outside the band. Only newly generated chunks are affected. (v0.976.0, #1590)

Upside-down gravity

Inside the upside-down band and its entry lead-in (the zone that renders flipped), gravity points up. Sand, gravel, concrete powder, anvils and suspicious sand/gravel fall up (+Y) and settle against the ceiling. A falling block's gravity is negated, it lands when it hits something above, and it rests on the cell above. This runs on both sides, so the client animates the rise too. Blocks inside Sable ship plots keep normal gravity. UpsideDownGravity holds the rules. FallingBlockUpsideDownMixin, BrushableBlockUpsideDownMixin and FallingBlockEntityUpsideDownMixin apply them.

  • Held until the flip. The mirror is applied on a deferred drain, so a band chunk is live before it flips. While a chunk still carries NEEDS_UPSIDE_DOWN_MIRROR, gravity ticks are held and recorded instead of dropping blocks through the un-flipped terrain. When the flip lands, each one is replayed at the block's mirrored position (UpsideDownMirror.armFallUp). Only blocks that actually tried to fall are replayed. Gravel blobs in cave ceilings stay put, as they do in vanilla.
  • Real sand in the ceiling. In band columns the reflected ceiling keeps real sand and gravel. The hanging hills below the train and the lead-in/exit dithers are still anchored to sandstone and stone (FallingBlockAnchor), so nothing rises across the track.
  • Suspicious blocks come along. Suspicious sand and gravel are mirrored into the ceiling with their loot data. Before this they were deleted.
  • Debug. [DT-UDGravity] lines in debug.log show how many held ticks each chunk replayed after its flip.

Only chunks mirrored from now on are affected. (v0.977.0, #1589)

Mountains kept off the upside-down track

The band mirrors ordinary overworld terrain about the train plane, so any source terrain that rises above the train turns into a solid hang beneath it, and the train tunnels through it. Near the track, the band now weights the overworld's erosion climate input up toward a lowland floor (0.25). In 1.21 terrain height and biome both come from that climate noise, so the ground drops and the biome follows (snowy slopes and grove become snowy plains and taiga). Mountains further out stay as they are.

  • Noisy edge. Each side of the track has its own seeded edge that wanders along X. At its tightest, the ground is flat within 6 blocks and fully natural by 16, so mountains can stand one chunk from the line. At its widest it is flat to 32 blocks and fades out by 160.
  • Where. Across the entry lead-in, the band and the exit crossfade, ramping in and out over 160 blocks past each end.
  • How. UpsideDownTrackFlatten holds the weight maths. TrackErosionDensityFunction wraps every copy of the erosion noise node in the overworld router (RandomStateMixin), covering the terrain splines as well as the climate field.
  • Side effect. Flattening uncovers the river valleys the mountains sat on, so there is more water and ice near the track.
  • Config. upsideDownTrackFlatten (common config, default true).
  • Unflipped chunks fixed. NeoForge fires ChunkEvent.Load before the chunk is published FULL, so the 3×3 neighbour check behind the deferred mirror never saw the loading chunk. Band chunks beside the track could unload before they flipped. WorldUpsideDownEvents.promoteNeighbourhood now counts the loading chunk as loaded.

Only newly generated chunks are affected. (v0.981.0, #1596)

Chuncks — a mostly-void band of scattered floating chunks

Since: v0.485.0 (PR #810)

A late-cycle world-gen band (after the upside-down band, alongside the void & End bands) that the train crosses: mostly open void, sprinkled with the occasional real overworld chunk — some full-height islands, others just a floating slab of surface with a flat cut-off bottom. The void thickens gradually on approach (with an overworld buffer before the band) and the train keeps rolling across on its floating track. Fully configurable: band length, void density, slice ratio, entry fade, and lead-in gap.

Spheres — floating spheres of lifted natural terrain over void

Since: v0.930.0 (PR #1484)

After Chuncks behind a plain-overworld lead gap (and before Stacks). On the way in the ground crumbles away from below until only the track is left; then the train rolls out into open sky scattered with spheres of the natural world — each one cut from the vanilla terrain at its own X/Z and lifted to its own height, so a grassy cap, a beach, a pond or a stretch of ocean floats with the rock beneath it, some far above the track, some passing right under it. The bed cuts a clean slot through any sphere that crosses the line; tunnel shells, pillars and lane terrain dissolve with the ground they belonged to. The band is a 750-block entry fade (spheresFadeBlocks) plus 6 550 blocks of core, and runs as a progression (PR #1494, building on PR #1492; shortened and re-staged in PR #1581, v0.974.0 — it was 1 500 + 15 000). Offsets count from the end of the entry fade:

Offset Sky Spheres cut from Structures
0–1000 Overworld Overworld base (spheresStructureChance 0.08)
1000–1750 End Overworld base
1750–2500 End Overworld / Nether base
2500–3250 End Overworld / Nether / End base
3250–6250 End Overworld / Nether / End ramps ×5 → ×20 at the midpoint (4750) → ×5 (capped at every sphere)
5900–6450 End → Overworld thinning out: rarer and smaller spheres, down to none the boost still applies until 6250
6450–6550 Overworld none — a closing stretch of empty void —

The End sky (End dome, dark fog, flat End lighting, no clouds, End music) fades in over spheresEndSkyFadeBlocks (150) at spheresEndSkyStartBlocks (1000) and fades back to the overworld sky over spheresEndSkyExitFadeBlocks (550), finishing where the spheres run out, so the closing void is under an overworld sky. (The band used to end under a Nether sky; that stretch was removed in PR #1581.) Client-side only, through ClientVoidBand.endSkyIntensityAt; toggle spheresEndSky.

Thinning out. Over spheresExitTaperBlocks (550) before the closing spheresExitVoidBlocks (100), each placement cell's chance of holding a sphere and the sphere's radius both scale down linearly with SpheresSegments.exitTaper (radius floor 3), so the field thins and shrinks to nothing rather than ending at a wall. Spheres before the taper are byte-identical to an untapered band. The client reads its own COMMON config for these offsets.

Other-dimension spheres. Past spheresNetherMixStartBlocks (1750) a sphere may be cut from the Nether, past spheresEndMixStartBlocks (2500) from the End, weighted by spheresMix{Overworld,Nether,End}Weight (1:1:1). A Nether sphere is cut around a hashed height in Y 40–100 (cavern floors, walls, sometimes the lava sea); an End sphere is anchored to the End generator's island surface, and falls back to Nether over the End void. Spheres of any dimension may also be built around a structure (fortress, bastion, End city, stronghold, outpost, trail ruins…); spheresStructureBoost{Start,End}Blocks bound the ramp and spheresStructureBoost{,Peak}Multiplier set its ends and peak.

Those spheres are generated off the server thread (ForeignSphereSampler, spheresSamplerThreads 2) with the source dimension's real generator into a throwaway chunk — noise, surface rules, carvers, the structure, then full biome decoration — via OfflineChunkSampler (shared with the chunk-dimension portal rooms). The carve leaves their cells empty; WorldSpheresForeignEvents writes each finished sample into its chunk (spheresForeignApplyPerTick 4 per tick), only into cells still air, with block-entity NBT (structure chests keep their loot tables). A chunk attachment dungeontrain:sphere_pending records what a chunk is still owed, so a chunk that unloads before its fill is filled on its next load; entries clear once written. Expect a few seconds of pop-in after a teleport; decoration is per chunk, so trees and glowstone can be cut at chunk borders. [gen.timing] reports sphSample / sphApply. Every carved chunk is lit as open sky (the raw section writes the carve uses never reach the light engine, so the sky light is stamped to full daylight instead of recomputed). Ambient mobs don't spawn in the band — there is nothing for them to stay on. Water and lava inside a sphere stay put; nothing pours off its underside into the void. Fully configurable under [worldgen] spheres*: band length, entry fade, lead-in gap, placement cell size, density, radius range, how high the spheres float, and how much of each sphere sits below its natural surface (spheresSurfaceBias, default 0.65 — a surface cap with rock beneath). Disabling the band restores the previous cycle exactly.

Under the hood: a pure, seed-stable 3-D cell field (SphereField) that any chunk can rebuild from its own coordinates, with the natural surface sampled from the chunk generator's noise (getBaseHeight) rather than from neighbour chunks; a carve-and-lift pass on new-chunk load (WorldSpheresEvents) that gather-writes each column from a snapshot the way the upside-down mirror does; chunks no sphere touches skip terrain fill, surface rules and decoration entirely. Cost is about half a millisecond per carved chunk ([gen.timing] spheres=).

Stacks — a void of towers, each one vanilla structure piece repeated to the sky

Added: v0.931.0 — PR #1487

The last band of the world-gen cycle, after Spheres behind a long (10 000-block) stretch of plain overworld. The ground dissolves into void on the way in; then the train rolls out among towers — in scattered chunks, one vanilla structure piece (a village house, a bastion stair, an ancient-city wall, a trail-ruins hall, a mansion room, a fossil, a shipwreck, an End-city purpur tower…) is stamped straight up from the world floor to just under the build height, the same piece and rotation on every layer. Every tower is a different piece; ~8 % of chunks carry one; the two rows of chunks around the track corridor never do, so the floating track bed always passes between them. No bedrock plane under the void, and water or lava inside a tower stays put. The Stack Overflow advancement is granted 500 blocks into the band; Re-Over-World now waits until the towers are behind you (the true end of the cycle), and the second Nether band is the one after them. Fully configurable under [worldgen] stacks*: band length (stacksHoldBlocks, 5000), entry fade (stacksFadeBlocks, 1500), lead-in gap (stacksLeadGapBlocks, 10000) and tower density (stacksDensity, 0.08). Disabling the band restores the previous cycle exactly.

Under the hood: a chuncks-style seed-stable per-chunk classifier (StacksBand — TERRAIN / VOID / STACK, corridor rows excluded, memoised per chunk); a catalogue of every minecraft: structure NBT the server can load (VanillaTemplateCatalogue — StructureTemplateManager#listTemplates(), ~1010 pieces, trial chambers excluded, sorted for determinism); and a top_layer_modification feature on the worldgen worker (StacksFeature) that picks a piece per chunk (rerolling deterministically when it does not fit a one-chunk footprint) and stamps it layer after layer with jigsaw/structure-block processors and the position-pure StampRandom for loot. Void and tower chunks take the all-air fast path and skip vanilla decoration; the bedrock floor is skipped in-band; a FlowingFluid veto keeps liquid from pouring into empty chunks. Cost is about 27 ms per tower on the worker thread ([gen.timing] stacks=).

Legacy eras — the third leg's run through old Minecraft generators

Added: v0.949.0 — PR #1524 · Reordered, Superflat added: v0.951.0 — PR #1533

One slot of the cycle (after Spheres) in which ports of old world generators crossfade straight into one another. Each crossfade is the incoming era's own legacy<Era>FadeBlocks (480 for every era except Lost City's 750-block run-in, since v0.981.0). The default run, as core lengths in blocks:

Era Core Notes
Amplified 5000 vanilla preset, sunk 80 blocks (see below); since v0.998.0 crossfades straight into Beta — Lost City moved to its own run on Lap 2 (see below)
Beta 1.7.3 3500
Far Lands 4320 Beta read ~12.55M blocks out
Caves of Chaos 4000 void below
Skylands 5000 void below
Indev floating 2000 void below
Alpha 1.1.2 2000 back half is winter
Infdev 2000 plays newest first: 20100611 20% → 0420 20% → 0415 20% → 0227 40%
Classic 0.30 2000 256 × 256 levels tiled
Superflat 1000 grass, dirt, dirt, bedrock just under the track, void below, plains; one chunk (16 blocks) lower on each later loop, clamped at the world floor
Void 200 nothing at all

The sunk zone — Amplified rides high (v0.974.0, PR #1584)

The train used to spend most of Amplified inside its mountains. Amplified's terrain, and a short 500-block overworld approach right after Spheres (ow:sunk:500 in worldgenCycleOrder), are generated 80 blocks lower. That depth is the whole stock basement, so the train rides high over the valleys. Every ordinary chunk in Amplified's fades is sunk too, so the entry has no 80-block steps and no ocean pouring off chunk edges.

  • How: PresetTerrain builds the Amplified preset and a sunk overworld preset with the depth gradient, noise window and sea level shifted by AmplifiedDrop (sea 63 → −17). The window is capped under the lid, so there's no extra noise work. SunkZone says which columns are sunk. Surface, carvers and structure getBaseHeight run on the sunk preset, so bedrock, lava pools and villages sit at the sunk height.
  • Twins move up: the sunk terrain fills the basement, so dimensional-carriage twins in the zone go to an attic above an invisible barrier lid (AmplifiedDrop.lidY, y224 at stock height). The lid is stamped in air only. From train height the rooms sit above the clouds.
  • Floor-aware systems: WorldFloor.terrainFloorY / seaLevelAt feed the bedrock stamp, the worldgen write clip, structure starts, and the track's pillar and stairs probes, so pillars and stairs reach the valley floor.
  • No deep underground set in legacy: ancient cities, trial chambers, mineshafts, amethyst geodes, monster rooms and fossils are skipped in every legacy band and the sunk zone (LegacyUnderground). They sit at vanilla's absolute depths and would surface in the open. Strongholds stay.

Existing worlds keep chunks already generated, so there's a seam where new ones begin. Config v7 moves an unedited shipped order to the new default.

Lost City — ruined cities between Amplified and Beta (v0.981.0, PR #1599)

An era of ordinary modern overworld terrain, crowded with the ruins of the Big Lost City mod (a required dependency, see Compatibility).

  • Terrain is vanilla. LegacyBandKind.LOST_CITY answers usesVanillaTerrain(), so every terrain hook (NoiseBasedChunkGeneratorMixin, LegacyBiomes, LegacyDecorateFeature, ChunkGeneratorDecorationMixin) treats its chunks as plain overworld: no port, no preset generator, vanilla multinoise biomes. It stands at stock height, so there's a step against the sunk Amplified chunks in the run-in.
  • Cities only here. DT's dungeontrain:lost_city structure set (spacing 8, separation 4) places the mod's 42 structures densely. StructureBasementMixin drops every big_lost_city start that isn't on a chunk the era owns (LostCityStructures), which also silences the mod's own 42 sets everywhere else: other bands, other dimensions, worlds without a train.
  • Run-in and exit. Amplified → Lost City crossfades over 750 blocks (legacyLostCityFadeBlocks). A city may start on any crossfade chunk already rolled to Lost City, so cities thicken in across the run-in. On the way out a start must keep 128 blocks short of the core's end, because Beta's old generator drops structure pieces in its chunks and would cut a city in half.
  • Big buildings beside the track. The mod lets its skyscrapers, power plant, houses, store, warehouse and ferris wheel start almost only in plains. DT ships copies of those 13 (dungeontrain:lost_city/<name>, identical but for biomes: #dungeontrain:lost_city_trackside, i.e. vanilla land biomes) in the same set at the same weights, allowed only within 160 blocks of the track centre. Away from the track the mod's own biome rules stand.
  • Flat along the line. The upside-down band's erosion weighting (UpsideDownTrackFlatten) also covers the whole Lost City slot, so the ground settles low beside the track.
  • Not a mix-zone pick (plain terrain; its cities can't live in a lone mix chunk).
  • TrainPhase.LOST_CITY (protocol 96), /dtp lost_city, and a band advancement. Config v11 moves an unedited shipped order to the new default.

The journey's new route — Lost City on Lap 2, seated into WWOO (v0.998.0, PR #1613)

The default worldgenCycleOrder was reordered. Lap 1: overworld → Nether (vanilla>bop) → WWOO (4500) → one End band made of two joined pieces (vanilla 1200, then BoP 2000, one void fade, a ±200-block seed-stable crossfade at the seam — WorldGenCycle#endSourcePassAt) → upside-down. Lap 2: BoP overworld → BetterNether → Lost City as its own legacy run (legacy:wwoo:lost_city=4000) → BetterEnd. Then spheres → sunk → the legacy run (Amplified now crossfades straight into Beta) → chuncks → mix → stacks. The hidden "Plenty at the End" advancement is gone. Config v12 and v13 move an unedited order to the new default (v13 also catches an interim dev order without the WWOO style).

An order may hold several legacy: runs; eras live per run and each era belongs to exactly one. A legacy run can carry a style (legacy:wwoo:), and a run opening with a vanilla-terrain era gets a lead-in on the preceding Nether's exit mountains (CycleLayout#legacyLeadIn = mega-hold + rise).

The Lost City itself changed:

  • WWOO look over its whole run, lead-in included (WorldGenCycle#stretchStyleAt → SecondLapOverworld).
  • Flat only through mountains. The track's erosion flattening (UpsideDownTrackFlatten#mountainGate, also in the upside-down band) samples the untouched erosion on the track line and opens a valley only where the line would cut a mountain; the Lost City zone ramps in inside its slot and never reaches the Nether's mountains.
  • Buildings sit in the ground (LostCityGroundProcessor, attached to every big_lost_city template by SinglePoolElementMixin). Every template has a one-block pad: its natural ground (grass, dirt, moss, stone, sand, water…) and plant cover yield to the world wherever the world has ground or water there; roads, pavements, walls and props are placed as authored. Template air yields to the world's water at any height and to a contiguous column of natural ground (a shape test: dry solid full cube, not foliage, no block entity), so hills climb over the lower floors and rooms below the water line flood. Vanilla adaptation is off for these structures (StructureTerrainAdaptationMixin); BeardifierMixin adds the beard's fill without its carve (LostCitySeating#fillContribution), so ground slopes up to the pad within 12 blocks and fills under it; a footing column (dirt under ground pads, stone under paving, up to 24 deep) covers deeper drops.
  • Seated on the footprint. LostCitySeating#seat (from StructureBasementMixin) samples OCEAN_FLOOR_WG on an 8-block grid across the start's footprint and drops the pad to the 30th percentile, so ~70% of the footprint is at or under the natural ground; in water the pad sits on the seabed.
  • Every biome, everywhere in the era. DT's copies of the big buildings use #minecraft:is_overworld and start in any chunk the era owns (the trackside strip is gone); set spacing is 6.
  • Fades in (LostCityStructures#density): a deterministic per-chunk roll. Nothing on the Nether's exit range; from the foot of its fall (#fallFoot = mega-hold + the two tallest stages) half the placement grid, rising to full 2900 blocks on. Lap 1's WWOO stretch keeps 4% of its starts — a few ruins as a foretaste. The exit margin before BetterEnd stays 128 blocks.

Dungeon Train's own Lost City buildings (v0.1032.0, PR #1669)

Big Lost City is All Rights Reserved, so DT never modifies one of its templates (LostCityStructuresTest#noDtPoolNamesBigLostCity pins it). The Lost City's variety now comes from sixteen original buildings of DT's own, generated and shipped as data/dungeontrain/structure/lost_city/<name>.nbt: office tower, apartment block, hotel, hospital, civic hall (stage with a lectern, side offices), shopping strip, railway station (ticket office, seating), petrol station (shelving), radio mast, water tower, fallen overpass, cooling tower, silos, a leaning tower (sheared one block per four layers, crushed east foot, lifted west foot), a fallen block (an apartment slab toppled beyond a ragged stub, pancaked in places) and a snapped tower (top half hanging over the stub's lip on a slope of rubble, rebar at the break, chunks dangling on bars, facade pieces dug into the mound).

  • Generated, not hand-built. scripts/lost-city/build-templates.py renders each recipe in scripts/lost-city/lostcity/archetypes/ to NBT; the finish pass resolves every canonical block through a noise-patched stone mix (materials.py), punches holes, shears a corner, heaps rubble skirts and smooth mounds (damage.py), grows moss, vines, roof gardens, roots and weeds (weather.py), then drops loose rubble and grounds carpets, pots and torches (gravity.py) so nothing floats or falls on placement. Vanilla blocks and vanilla loot tables only; the datagen refuses to write any DT file that names a Big Lost City resource.
  • Ruined interiors. Furnishing kits per building type (furnish.py), then every room broken in with cobwebs, moss, rubble, vines and roots; mob spawners and tiered loot chests (mineshaft → dungeon → stronghold → outpost) are the only block entities. StructureTemplateClearMixin fixes the unattached-block-entity crash those re-open.
  • A different look each placement. scripts/lost-city/generate-variants.py emits a template pool and ~105 processor lists (weighted designs) per building: lost_city_stretch (taller/shorter/wider/podium/setback — the manifest declares exact floor and bay periods and the stretch finds them), lost_city_swap (recolour/decay), lost_city_bite, lost_city_facade, lost_city_truncate. LostCityFootprint resizes a stretched start's box from StructureBasementMixin.
  • Placement. All sixteen join structure_set/lost_city.json (weights 3–6) on the same random-spread grid and era rule as Big Lost City's buildings and DT's copies of them, so they roll in the Lost City run and the WWOO foretaste; the three wrecks weigh 4 each.
  • Tests. LostCityTemplatesTest (headers, pad, margin, the declared periods really stretch, nothing falls, no valuables, block entities only for spawners/loot), one JUnit class per processor, test_lostcity.py; CI runs the generator tests and both --check modes.
  • Known gap. Lost City Terrain Fit (v0.1031.0) seats and ground-processes only big_lost_city templates, so DT's own currently take vanilla heightmap placement until the sibling is widened to dungeontrain:lost_city/.

Shorter bands, a blended first Nether, no dead upside-down tail (v0.1030.0, PR #1668)

The default worldgenCycleOrder is shorter. The run-1 cycle drops from 116,028 to 97,428 blocks. Core lengths:

Band Before After
Lap 1 Nether 3000 2750
WWOO overworld 4500 3250
Upside-down Reassembly 5000 3000
BoP overworld 8000 4500
BetterNether 8000 4500
Lost City run 4000 3000
BetterEnd 8000 5000
Legacy: amplified / beta / caves_of_chaos / skylands 5000 / 3500 / 4000 / 5000 4000 / 2500 / 3500 / 4000
  • Lap 1 Nether split (nether:vanilla=1000~400+bop>bop:2750).
    • Syntax: a=N~M+b means look a for the first N core blocks, a mix over the next M, then look b. It applies on the first cycle only and is Nether slots only. ~M is optional; without it the switch is hard.
    • Cycle 1: 1000 vanilla, 400 mixed, 1350 BoP. Cycle 2 onward: all BoP.
    • How the mix works: in the mix, BoP's share rises 0 → 1, and a seed-stable coherent noise picks each column's look, so the two interleave in clumps roughly 9–24 blocks across. The core's terrain comes from the Nether density router whatever the look, so the ground stays continuous.
    • Code: the column-aware look is WorldGenCycle#netherLookAt(x, z, seed), and every Nether biome consumer calls it. /dungeontrain debug nether-passes labels the pass vanilla→BoP.
  • BoP Nether is all BoP. NetherCoreBiomes#resolveBopRegions now picks only BoP's TerraBlender Nether regions, not vanilla's own region. BoP's regions still hold some vanilla biomes.
  • No trailing fade before a Reassembly.
    • Before: the 600-block trailing fade after the upside-down core was held at full strength whenever a Reassembly followed, so it was just more upside-down.
    • Now: CycleLayout#udTrailingFade drops it in that case, and the band is entry fade → core → Reassembly → exit gap. A band with no Reassembly keeps it.
    • Still to know: the Reassembly's first ~20% still reads as mostly mirrored. The mirror-disperse gate compares a centre-heavy noise against a linear ramp, so mirror loss is S-shaped: about 2% gone at 20%, 50% at 50%, 98% at 80%.
  • Config v15 moves an unedited v14 order to the new default.

From Alpha on, the run goes back in time. The order comes from the legacy: token of worldgenCycleOrder, e.g. legacy:amplified=5000:beta=5000:…:superflat=1000:void=200. Named eras run in the order written (an era named twice keeps its first place); a bare legacy takes every enabled era in declaration order. Each era also has legacy<Era>{Enabled,HoldBlocks,FadeBlocks,LeadGapBlocks} keys. Superflat has no TrainPhase of its own, so its carriages use overworld styling and it has no /dtp target. Find its X with /dungeontrain debug cycle-layout.

Under the hood: CycleLayout.parseEras, LegacyChunkWriter (per-era fill; Superflat's sheet in writeSuperflat), LegacyBands (Y offsets, superflatGrassY), InfdevVersion.

Since: v0.797.0 (PR #1301)

A dimensional carriage whose pocket room is one chunk of genuine world generation — 16 × 16 across and two chunk sections tall — with real terrain, caves, decoration, a guaranteed structure and the mobs that live there, sealed in a box of sky-rendering skybox blocks and entered through doorways that stand on its ground. Seven variants, each pinned to one look: plain vanilla Overworld, the WWOO and Biomes O' Plenty overworld stretches, vanilla and BetterNether Nether, and vanilla and BetterEnd End (since v0.958.0, PR #1547). One room in ten borrows another dimension's structure — an End city in a meadow, a bastion in a birch forest. The vanilla Nether and End rooms show only vanilla content, and the vanilla End room has an End city six times in ten (since v0.964.0, PR #1559). Nothing is ever loaded or generated in the live world to build one: the chunk is sampled into a throwaway ProtoChunk and decorated in a WorldGenRegion built over it, which is what keeps heightmap-relative features landing in the room instead of on the train's surface, and what stops a tree at the sample's edge spilling into the sealed basement.

→ Read more

Dimensional carriage chests roll their own loot

Since: v0.1311.0 (PR #2050)

A container built into a dimensional carriage's own room template, rather than placed as a block-variant cell, now fills from the loot pool its author set up in the editor's C menu. Before, nothing rolled those cells, so they kept the items saved into the room's .nbt: the same chest in every copy of the room and every pair on the train, and the same loot on every Test the Carriage → Reseed (Cmd/Ctrl+R). PortalRoomPools rolls them on the same frames as the room's variant cells: seeded by the copy's identity (salted by a test's reseed, so Reseed shows a fresh roll), with the difficulty taken from the entry corridor's carriage index. A live copy re-stamped by the tiling window still shows the chest the player left. A variant cell still has the last word over its own chest. Containers an author filled by hand with no loot pool are unchanged, because there is nothing to roll them from.

Dimensional carriages match their stage, and doors come whole

Since: v0.954.0 (PR #1537)

The cart between a dimensional carriage's two corridors is built from the stage_stone_bricks placeholder, so it takes the current stage's bricks (cut sandstone in the desert, deepslate bricks in the deep dark, …) instead of plain stone bricks everywhere; the editor's portal_middle plot keeps the placeholder itself. The two cells behind each dummy door are always black concrete — now on short corridors too, not only long ones. The short corridor (portal_short) and the woodwooddoor door part also use stage blocks.

Stored templates now always stamp doors, beds and tall plants whole: DoubleBlockTemplateRepair completes a missing partner half (or makes a mismatched one match its lower half / bed foot) when any carriage, part, contents, group, whole-carriage or track/room template is loaded, and again before an editor save. A half door — lower with nothing above — had shipped in portal_short and fire, mismatched halves in the copper door part and twoway, and lone tall-grass lowers in several contents templates. Variant-rolled doors were already completed at placement by block variants' multi-space span.

Dimensional carriages tile exactly as their author built them

Since: v0.799.0 (PR #1305)

An Endless Repetition room's Room Walls setting now defaults to Kept: every copy is the room as drawn — no seam carved between neighbours, no outer face bricked up with mirrored blocks, and the corridor mouth's plane is the copy's own end wall rather than a plane of the room's floor block the game invented for open-sided rooms. Merged (the previous behaviour) is still available per room in the editor and is stored as an explicit sealed segment; rooms already standing in a world take the new look on their next re-stamp. The same PR makes the editor's Test the Carriage stamp a room at its template's true size (it read a plot-layout cache and could clip an edge off a freshly resized room), restores the Book room's floor and walls, and adds player-built rooms — Outside the Box, Foundation and Chess by Firefly_on_ytb, Suspicious Furniture Store and Nostalgia by Lil Detective Crunch, Peatear Griffin by Dot — plus Bigger, Smallpat, a second Single Pillar and an Ender room. See Editor → Portal rooms → Room Walls.

Dimensional carriages draw the whole room, and nothing vanishes while you're through the door

Since: v0.875.0 (PR #1402)

A dimensional carriage is two copies of one corridor — the carriage on the train and a twin at the world floor — and what you see of the other copy's occupants are puppets: per-tick stand-ins drawn on the client, not entities. A player clearing a horde reported the zombies going invisible while still hitting them and the frame rate collapsing over a few minutes; the room held 64 mobs but only 16 puppets could be drawn, the 16 were open to loot and XP orbs, and which 16 won was re-rolled by the level's scan order every tick. Now the budget is spent by tier (players, then living things, then scenery) and nearest-first with a stable tiebreak, and the snapshot is delta-encoded — a puppet is described once and thereafter costs a pose or a single key — so the cap is 96, enough for an authored room. Puppets move in the client tick at vanilla's own lerpTo(…, 3) pacing (no stutter), sit on the carriage's pose rather than its lagging bounding box (no shimmer on a moving train), stand at their own height on stairs and trapdoors, and flinch and fall over in step with the mob they stand for. Two behaviours came with it: XP orbs dropped in the other copy drift toward your mirrored position, cross the midpoint and are collected as normal; and mobs on the carriage side no longer despawn the moment you step into the twin — vanilla's 128-block rule saw you 140 blocks below them. Log floods fixed on the way: the "puppets capped" line fires only when the count changes, and the whole-train roster ([trains]) is behind /dungeontrain debug trains-trace. /dungeontrain debug puppet-trace logs per-tick puppet coordinates for chasing shimmer.

Dimensional carriages always lead somewhere

Since: v0.781.0 (PR #1273) · relay side dp-relay #537

A dimensional carriage is a three-carriage group — entry corridor, sealed cart, exit corridor — whose crossing is a teleport at the corridor midpoint into an identical twin corridor beside a pocket room under the world. When that teleport is refused (the twin's chunks fell behind the train, nothing to land on, no room that fits), the player used to walk up to the black plate between the pair's two doors and have to mine it. Two changes: either corridor now places and relocates the pair's room, both anchored on the entry corridor's origin, so walking the train backwards into an exit corridor connects it like a forward approach; and a corridor whose swap keeps being refused for about two seconds opens that plate itself (PortalWalkThrough), with an action-bar line saying so — the group becomes a walk-through until the next re-stamp closes it and the portal gets another try. Nothing is recorded; a severed pair (shell mined open past the midpoint) is still the only permanent state. Each life's crossings that worked and ones that didn't are tallied at zero per-tick cost and reported once at death (/telemetry/portal-stats, see Development → Data Explorer), which is how "do dimensional carriages break too often" becomes a number per version. /dungeontrain portal diagnose reports the walk-through state per corridor.

Since: PR #618 (first release after v0.381.0)

Direct two-way chat with the developer on the title screen. Once the dev has replied in a player's thread, an envelope button appears above the accessibility button; unread messages pop up a preview callout and pulse the envelope. It opens a dedicated chat window — the dev's replies stream in live, the player writes back from a send box, and offline sends queue durably until reconnect. The window shows conversation only (dev replies, the player's @dev-tagged or menu-sent lines, compact survey answers) — automated game reports are filtered out. In Discord the dev sees ✅ when a message reached the player's game and 👀 once it was actually read.

→ Read more

Since: v0.446.0 (PR #736)

A localization resource pack can ship a small assets/dungeontrain/localization_credits/<slug>.json alongside its lang/<locale>.json override, crediting a translator's name and an optional link. When a player's currently selected language has a credit, the title screen shows "Localized by <name>" immediately left of the vanilla language button — silent otherwise, and wrapping to a second line if the text wouldn't fit before the screen edge. The credit's human_reviewed flag also drives the language-selection list badge: reviewed languages show the DT logo solid, machine-translated ones show it faded with a small blue "AI" label (v0.490.0). Since v0.499.0 a blue AI-fraction ring around that logo shows how much of each translation is AI-generated without human review — full circle = pure machine translation, emptying as human review lands — fed by generated per-locale counts stamped from the provenance sidecars and lockstep-checked in CI.

→ Read more

Human translations only

Since: v0.1161.0 (PR #1837)

A person icon at the right end of the Language screen's header row (next to the search box) switches Human translations only on or off. It's off by default. When it's on, every Dungeon Train line that is still machine-translated and not reviewed by a person shows in English. Lines a person wrote or reviewed stay in your language, and so do relay-approved translations and your own translation-editor edits. It applies straight away with no restart, and is saved as translations.humanOnly in dungeontrain-client.toml. What counts as unreviewed comes from the shipped localization_provenance/<locale>.json manifests (HumanOnlyTranslations builds the English layer under TranslationOverrides). In single-player and as LAN host, unreviewed books, stories and death lore fall back to English too (narrative/HumanOnlyProse), as do AIN item names when that language's item names are entirely AI. On a dedicated server only the UI text follows the switch, because prose belongs to the server.

Translation editor — character cards and the Characters filter

Since: v0.1082.0 (PR #1734)

Many languages cannot write a sentence about a character without knowing their gender or age, and most stories are first person, so the English never says. assets/dungeontrain/translation_characters.json is a hand-written card per character (pronouns, gender, rough age, about, translator notes) for the 15 story narrators, Faulthurst, the Librarian, Nell Ardwick, the Conductor and Brennan/the Creator. In the in-game translation editor, a line tied to a character shows an About link beside View source, which opens the card. The card's Show their lines button jumps to the list filtered to that character.

In the list, From has a Characters option. Under it, a third cycle picks one character ("All" = every character's lines). A character's lines are their own book, every book whose English names them, and their mapped lang keys (e.g. the Conductor's chat lines, Brennan's death-screen text). Book ownership comes from books, lang lines from keys / key_prefixes, and mentions from a per-character mentions list matched as whole, case-sensitive words, so Della's letters show under Pip. State All is offered under Characters even before a first approved translation, because in an already-reviewed language every other State would hide a character's lines. Search also matches character names. TranslationCharactersTest checks that every referenced book and key still exists, every story narrator has a card, and every character has lines.

Translation editor — previews (v0.1084.0)

PR #1736

The edit screen's Preview button (a square item icon) shows the text in the box — saved or not — where the game shows it, so a translator sees fit and context before saving. A view is offered only when it is certain: the code or data fixes where the string goes, or this install has seen the game show it there. Views:

View Offered for Shows
Book book prose, stat-book and leaderboard lines the page; Full Context rebuilds the whole page/book (stat page, leaderboard with real relay rows, narrative cover, starting books paged on %PAGE%), Refresh re-rolls sampled parts
Death screen death lore, death.attack.* the death-screen layout, drawn by the screen's own DeathScreenText
Item item/block names, recorded tooltip lines the item in a slot with its real tooltip
Advancement popup titles with show_toast the vanilla toast (off when Advancement Plaques is loaded)
Button labels seen on buttons sample widths, or the recorded screen (Full Context); long labels scroll as in game
Tooltip widget tooltips seen on screens the tooltip over its control on the recorded screen
Chat / Action bar death messages; messages seen in chat / above the hotbar the recorded message with the typed text swapped in

Non-book previews open full-window; Back, the view switch (only places the string is used), Full Context and Refresh sit along the bottom. The list's From filter has one option per view.

Where strings are shown comes from assets/dungeontrain/translation_contexts.json, traced from the code (3,600+ keys), plus recorders that add what is seen in play (ButtonKeyRecorder, ButtonScreenLayouts, ChatKeyRecorder, ItemTooltipRecorder, files under dungeontrain/translations/). Keys tagged unused (76 dead strings) are hidden from the editor; keys that could not be placed with certainty are listed with the reason in localization/translation-contexts-unsure.json.

Since: v0.684.0 (PR #1137)

The "Dungeon Train…" screen off vanilla's Options menu is rebuilt on Minecraft's own OptionsSubScreen, replacing a single hand-laid-out column that ran off the bottom of the window (and the Done button with it) at small window sizes or high GUI Scale. Settings are now split across three tabs — General, Train, Editor — each with its own scrolling list and Done pinned in the footer. Rows pair two-to-a-row where their caption fits, full width where it doesn't. The Editor tab absorbs the former "Editor Settings…" sub-screen's display-scale steppers (now sliders) plus the four per-menu worldspace/screenspace choices. The Internet Connection row is temporarily removed — it could be turned off but not back on. Since v0.1150.0 (PR #1817) a Performance tab lists every lag tip from the death screen's bug-report answer, checked against the player's setup, each with a button to its setting.

Since v0.1235.0 (PR #1938) every row is a settings-list line — icon, name, grey hint, value-only control — with the explanation on hover over the name. See Settings list.

→ Read more

Double-tap forward to sprint while flying

Vanilla 1.21.1 only starts a sprint from a double-tapped forward key on the ground or underwater, so a player already in creative flight had to hold the sprint key. Dungeon Train applies the same rule in the air: two fresh forward presses within 7 ticks start sprint-flying, which also triggers the /dt flyspeed boost. Client-side only (LocalPlayerFlyDoubleTapSprintMixin, timing in player/FlyDoubleTapSprint); ground double-tap and stopping the sprint are untouched vanilla (PR #1546).

Since: v0.1023.0 (PR #1655)

When Minecraft has less than 5 GB of heap on a computer with about 8 GB of RAM or more, Dungeon Train says so once per launch:

  • a full-screen card over the title screen (How to do it / Got it / Don't show again);
  • one chat line on the first world join.

How to do it opens the memory guide page for the player's own launcher (CurseForge, Modrinth App, the Minecraft Launcher, Prism, MultiMC or ATLauncher), falling back to the hub when the launcher can't be told. Players can turn it off with Don't show again or in Options → Dungeon Train → General → Low Memory Warning (lowMemoryNoticeChat in the client config). The CurseForge modpack also recommends 6 GB (recommendedRam).

The warning and its link live in LowMemoryNotice, the card in LowMemoryPromptScreen. Launcher detection is LauncherDetector.launcher().

Open template files from the editor

Since: v0.1042.0 (PR #1679)

The X editor screen's icon row has a folder button (between Redo and Reset) that opens your file manager on the folder holding the selected template's .nbt and its sidecars — the copy the game actually loads (the active package, or whichever enabled package supplies it). For a built-in you have never saved, it opens the active package's folder for that kind, which is where your first save lands. Hover it to see the file's path inside the package, e.g. wholecarriages/<id>.nbt. Works for any selection, not only the plot you stand in. Greyed out on a multiplayer server (the files are on the server) and for Architecture. See Packaging for the folder layout.

The folder mapping lives in TemplateFileLocator; the button is EditorScreenActions.openFilesIcon.

Reroll the carriage around a contents template

Since: v0.1317.0 (PR #2061)

With a contents template selected, the X editor screen's icon row has a dice button (between Open template files and Reset). It stands the contents' plot in another carriage that would carry them on the train: one of their size, with a weight above 0 and the contents switched on in its Contents list, drawn by weight and never the one already shown. Only the carriage is swapped; the inside is copied and put back, so unsaved edits stay. It works from anywhere: if the plot isn't standing right now, the choice is used the next time it is. Chat names the new carriage, or says no other carriage carries the contents. The choice lasts for the session; reloading the world goes back to the usual carriage. Portal corridor contents always stand in their corridor. Test the Carriage still rolls its own carriage.

Command: /dungeontrain editor contents reshell <id>. The draw is ContentsShellPicker.pickOther, the swap is CarriageContentsEditor.rerollShell, and the session choice lives in ContentsShellOverrides.

Creative inventory — mod block tabs hidden (v0.1100.0)

The creative tabs added by the biome mods Dungeon Train depends on — BetterNether (Blocks, Items, Nature), BetterEnd (Blocks, Items, Plants), Biomes O' Plenty and VanillaBackport — are hidden by default, and their entries are left out of the creative search, so the inventory stays on the vanilla and Dungeon Train tabs. Two buttons on Options → Dungeon Train → Editor bring each half back independently: Mod Block Tabs and Mod Blocks in Search ([creative] modBlockTabs / modBlocksInSearch in dungeontrain-client.toml, both off by default). A change applies at once, with no relog. VanillaBackport blocks that the vanilla tabs also list stay in those tabs and in search either way; the disabled gear and ores of those mods stay hidden whatever the settings say (PR #1756).

Consent card — Livestreaming and Developer chat switches (v0.1197.0)

The first-launch "Connect to the internet?" card (drawn by Discord Presence, lines supplied by DT) gains a Livestreaming line with an ON/OFF pill: on by default under Adult, off under Kid, switchable either way. Developer chat becomes a pill under Adult; under Kid it stays a fixed off line, because the Kid block in RelayChatClient.canConnect and the server mirror is a safety rule rather than a preference (PR #1903, Discord Presence 0.66.0's ConsentBullet.toggle). config/ConsentDraft resolves each switch when the card closes as last click wins, else the confirmed mode's default, so the defaults land for players who never click; CONSENT_VERSION 2 re-shows the card once to existing players. Both switches also live under Options → Dungeon Train → General.

What the Livestreaming switch does: with it off (or the master consent denied), putting the broadcast headpiece on burns it and chat says "Live-streaming is disabled, turn it on in Options / Dungeon Train / General / Livestreaming", the path a clickable link that opens that page (/dungeontrain-options client command, LiveStreamEndedPacket.Reason.DISABLED). A Kid with it off never finds the headpiece in loot: a rolled headpiece is stamped LiveHeadpiecePendingTag and event/LiveHeadpieceLootGate swaps it for the slot's random book the first time it reaches that player, reading the switch from ContentModeMirror (synced with the content mode, protocol 121). The Developer chat switch gates the menu-chat envelope outbound and the inbound dev-message consent sync.

Same PR: the Dungeon Train Options tab bar no longer stacks a copy on every reopen — a cached relay answer had been rebuilding the screen from inside its own init(); rebuildIfShown now defers a tick.

Live Feed and Free Play (v0.1212.0)

Free Play can't go live. On a release build, putting the Live Eye on in a Free Play run (RunIntegrity.isCheated) burns it with "Free Play runs can't go live.", and a running stream ends — and burns — within a second of the run turning Free Play (event/LiveFeedEvents#streamBlocked, Exit.FREE_PLAY, LiveStreamPacket.Action.STOP_FREE_PLAY). Dev builds are exempt so streaming can be tested in creative (PR #1914).

Which feed a TV shows. Everyone watches the public feed. A Free Play run's TVs switch to the dev channel only while a dev is streaming: each poll asks the dev cap uncounted whether it is live, then polls (and is counted on) the channel it shows (client/live/LiveStatusPoller, LiveFeedClient#viewerChannel, DungeonTrain#devRelayBaseUrl). Free Play is read from the client's Free Play badge; when it flips the status is dropped and re-polled at once. Dev builds stream to the dev channel, so a dev stream never reaches the public feed. The dev channel is also on the web at the unlisted brennan.games/dungeontrain/livedev (/live/public-status?channel=dev, dp-relay #737, v0.244.13).

Live chat from Discord (v0.1257.0)

While a stream is on air, the streamer and anyone wearing a Vista TV see the stream's Discord channel (#👁️-live-eye) in their in-game chat as [Live] Name: text, and every non-command line they type is posted back into that channel as them (Minecraft name, head avatar, no @mentions) (PR #1964, relay dp-relay #753). The switch is Options → Stream → Live chat (default on; Kid mode keeps it off and greyed, like Show chat to viewers). A "Live chat connected to Discord" line with a link to that row appears when the bridge joins; a TV with nobody live is silent.

How: client/live/LiveChatBridge polls the relay's GET /live/chat?since=<seq> every 3 s (single-flight; the first poll takes the head seq, so joining never replays history) and sends through POST /live/chat/send, at most one line a second. The streamer reads its own relay; a viewer reads whichever channel its TVs show (LiveStatusPoller.channel()). Lines from a player in the same world are skipped (matched on the tab list by uuid or name) because vanilla chat already showed them. Rules are pure in LiveChatRules (LiveChatRulesTest). Relay side: live-chat.js keeps real-person posts from the gateway in a 200-line / 2 h in-memory ring (nothing persisted) and posts game lines through the channel webhook with the word-list guard and a per-player token bucket; env LIVE_CHAT_CHANNEL_ID + LIVE_CHAT_WEBHOOK_URL (a _DEV_ pair for the dev cap), /health.liveChat.

The same room on the website (relay 0.244.52)

The Live page at https://brennan.games/dungeontrain/live/ (and the unlisted dev feed page) shows the stream's chat room beside the picture, Twitch-style (dp-relay #757). Anyone can read it — Discord posts, in-game lines (green names) and lines typed on the site (yellow) — while someone is on air; off air it says chat opens when someone is live. Typing needs Log in with Discord (community members only, the same login as the mod-feedback and item-values pages on its own /live/chat cookie), and a sent line lands in #👁️-live-eye as that member (name + avatar, no @mentions) and in the streamer's and TV wearers' chat as [Live] Name: text. The input row is always there: Send once logged in, a Log in with Discord button in its place otherwise (dp-relay #760). The panel can be hidden (a 💬 Chat pill in the bottom row beside Fit brings it back; remembered per browser) and docks at the bottom on a phone. Relay side: cap-less GET /live/chat?channel=&since=, POST /live/chat/send, per-IP and per-member rate limits, the word-list guard; LIVE_CHAT_RETURN_URL + a third OAuth redirect switch the login on, otherwise the panel is read-only. No mod change — source: web lines render like Discord lines.

Twitch chat in the live chat (v0.1259.0)

The same live chat now carries the Twitch channel's chat too (PR #1968, relay dp-relay #756). While a stream is on air, Twitch viewers' lines appear in the streamer's and TV wearers' chat as [Live] Name: text and are mirrored into #👁️-live-eye as Name (Twitch); every non-command line typed in game is posted into Twitch chat by the dungeontrainbot account (a moderator of twitch.tv/brennanhattongames) as Name: text. Discord lines are not forwarded to Twitch. Names are coloured by room — Discord aqua, Twitch light purple, another world green — and the connected line and the Options → Stream → Live chat hint now say "Discord and Twitch". Relay side: live-twitch-chat.js keeps one IRC socket open only while the cap TWITCH_SOURCE_CAP_LABEL is on air (closed 30 s after it goes dark), posts through Helix "Send Chat Message" with a per-30 s send window, and skips the bot's own lines; twitch-user-token.js refreshes the bot's user token. /health.liveChat.twitch shows the socket, counts and token freshness.

Twitch moderation follows the line (v0.1271.0). When a Twitch moderator deletes a message, times out or bans a viewer, or runs /clear, the copied Twitch lines go too (PR #1983, relay dp-relay #767): the #👁️-live-eye mirror is deleted through the webhook, the Live page drops the line, and in game it turns into vanilla's grey "This chat message has been deleted by the server." (a few seconds later — vanilla holds back very new lines). Discord, website and in-game lines are never touched. Relay side: IRC CLEARMSG / CLEARCHAT remove ring lines as a seq-advancing tombstone, and GET /live/chat lists them as deleted: [seq…]; the mod signs each printed line with a client-only MessageSignature so vanilla deleteMessage can find it.

Twitch account link — /twitch, the Account tab and the chat shout-out (v0.1308.0)

Players connect their Twitch account to Dungeon Train so that wearing the Live Eye names them on the official stream (PR #2036, relay dp-relay #806). /twitch asks the relay for a six-letter code and opens the Connect page at brennan.games/dungeontrain/link/twitch/ (the same shape as /dtlink): Connect Twitch runs Twitch's OAuth sign-in (channel:read:stream_key channel:manage:broadcast, always re-confirmed) and the page returns to the game-facing status. While a linked player wears the Live Eye: the official channel's title becomes Dungeon Train · Live Eye: Name (twitch.tv/login) (back to the idle title a minute after the feed goes dark), the chat bot posts "Now playing: Name — twitch.tv/login" in twitch.tv/brennanhattongames chat (an announcement band when the bot token carries moderator:manage:announcements, a plain line otherwise, repeated every 15 min), the website Live page's streamer name and the Discord go-live post link to the channel, the in-game "watching" line carries the link, and the burned-in stream name tag reads Name · twitch.tv/login. /twitch on a linked account prints the state and a fresh code for re-linking a different channel; /twitch unlink forgets the link and revokes the Twitch grant.

Opt-in restream. Options → Account (next to the Discord link) shows Twitch: name with two switches, also reachable as /twitch restream on|off and /twitch title on|off: Also stream to my channel pushes the same picture to the player's own Twitch channel while they wear the Live Eye (a second ffmpeg on the relay box reading the same playlist), and Set my title & category while live sets their title to "Dungeon Train Live Eye" / Minecraft for the session. The stream key is never stored: the relay fetches it from Helix per session with the player's refresh token (AES-256-GCM at rest, TWITCH_LINK_KEY), hands it to the worker over a loopback-only /internal/live/restream call guarded by LIVE_WORKER_SECRET, and redacts it from logs. Raiding the player's channel when their session ends is built but off (TWITCH_RAID_ON_END=false), with no player-facing wording.

Drop your Twitch in chat. Going live with no Twitch saved prints "Drop your Twitch in chat for a shout-out." (grey, with a [Turn this reminder off] click → /twitch hint off; Options → Stream → Twitch shout-out reminder; never in Kid mode) and opens a ten-minute window in which a twitch.tv/x URL, @x, or a lone name typed in chat yields "Connect twitch.tv/x to your account? [Yes] [No]". Yes runs /twitch save x: the relay checks the channel exists on Twitch (5 saves per hour, never overwriting a verified link) and stores an unverified handle that gets every mention surface but no restream or title until the player verifies with /twitch — the Account tab says "(shout-out only)" and keeps the switches disabled. Relay side: twitch-links.js, twitch-oauth.js, twitch-link-web.js (server-side nonce store so a lost state cookie does not block the callback), twitch-channel.js (owner-token title sweep), live-twitch-chat.js shout-out, twitch-push.js restream pair under the dt-twitch worker; /health rows twitchLinks, twitchChannel, twitch.restream.

Developer log requests — /bug from Discord or the live chat (v0.1276.0)

Brennan can ask a player for their game logs without waiting for a death-screen bug report (PR #1987, relay dp-relay #771).

  • Where: /bug in the player's private Discord thread, or in the live chat (#👁️-live-eye, the website chatroom or Twitch). In the live chat /bug asks whoever is streaming and /bug <name> asks that player (they must be in the live chat: streaming or wearing a TV). Only Brennan's accounts count: his Discord id in the thread, and the relay's LIVE_ADMIN_DISCORD_IDS / LIVE_ADMIN_TWITCH_LOGINS in the live chat. Anyone else's /bug is ordinary chat.
  • What the player sees: a developer line, "The developer is requesting your dev logs to assess something.", with clickable [Send logs] / [No thanks]. Typing @Dev also sends.
  • On Send: the same capped, home-redacted log set as a bug report (latest.log, debug.log tails, newest crash report) plus a system-spec summary is posted to their thread as "🐛 Bug report — Developer request" and archived by the relay. A "📎 Logs sent at your request" post then @-mentions Brennan. The server only accepts that upload for a request it issued.
  • "Message didn't land — why": any developer message or log request that can't reach the player is answered in the thread (or in the player's thread for live-chat requests): Kid mode is on, Developer chat is switched off, they declined, they didn't answer within 10 minutes, they aren't in a world, or a request is already waiting. With Developer chat off, developer messages are now blocked in game too (before, the player still saw the @Dev consent prompt).
  • Too old for log requests (relay 0.244.68, dp-relay #772): before a /bug goes out, the relay checks the player's last known mod version: the live claim, then world-info, then the newest join event. Below v0.1276.0 (LOG_REQUEST_MIN_VERSION) nothing is sent and you get "Message didn't land (Name) — they're on v0.1269.0; log requests need v0.1276.0+": as a bot reply in their thread (the /bug is not relayed, so an old mod never shows it as a dev message) or in #👁️-live-eye and the website chat. An unknown version never blocks.

How: logrequest/LogRequestManager (server: one pending request per player, server-issued id, expiry, block reasons, the ping), LogRequestClient (prompt, the /dtlogs send|decline <id> client command the buttons run, upload), DeliveryFailureReport. Packets LogRequestPrompt / LogRequestAnswer / LogRequestFromLive; ConsentSyncPacket now carries the Developer chat switch (protocol 128). The thread path hooks DiscordCommandHooks in compat/DiscordInboundBridge. On the relay, live-admin-commands.js turns an admin /bug into a source: 'admin' ring line with cmd: 'log_request', target and reqId, which LiveChatBridge acts on. It is never mirrored to Discord or Twitch.

Developer /version lookup — private, from the relay's records (relay 0.244.89)

/version shows which Dungeon Train version a player runs. /version full adds their launcher, language, single or multiplayer, and every installed mod as id@version. The game is never asked. The relay answers from what it already records on every world join (dp-relay #794):

  1. the newest world-info row;
  2. else the newest join event;
  3. the streamer's live claim when that is fresher.

So it's instant, works whether or not the player is in a world, and shows nothing in game.

Example: **Steve** — Dungeon Train 0.1288.0 · joined 3h ago. The streamer's line ends · live now instead. The full answer is labelled as of their last world join. On a dedicated server the mod list is the server's.

Only you see the answer:

Where How Answer
A player's Discord thread /version [full] slash command ephemeral, in place
#👁️-live-eye /version [full] [player] slash command (default: the streamer) ephemeral, in place
Explorer Chat page type /version [full] in the compose box panel under the box (not posted or put in the inbox)
Live website chat type /version [full] [name] an "Only you" line in your browser
Twitch type /version [full] [name] Discord DM to the first LIVE_ADMIN_DISCORD_IDS id (your line stays in Twitch chat)
  • Who can use it: the slash command is admin-only (default_member_permissions: 0) and checks LIVE_ADMIN_DISCORD_IDS. To hide it from other server admins, use Server Settings → Integrations.
  • Typed messages: a typed "/version" in Discord is ordinary chat, because Discord can only show a reply to one person through a slash command. The mod swallows it (LogRequestManager.isVersionCommand in compat/DiscordInboundBridge), so a player never sees it.
  • History: a first version that asked the game over two new packets (PR #2007) was reverted before release (PR #2008), so the protocol stays at 129.

Live Eye streams only while worn; typed text never broadcast (v0.1284.0, menus v0.1289.0)

The streamer's client no longer trusts the server's START on its own (PR #1998, Live Eye review P0-5). No frame is read and no relay claim is made unless dungeontrain:live_headpiece is in the client's own head slot. A stream without it ends after a 20-tick grace (client/live/LiveHeadpieceGuard). The grace exists because at login the server sends START before the inventory sync may land. A start that never saw the Live Eye is refused silently with a log line; one that loses it ends with the Live Eye taken off pop-up. Either way the client reports LiveStreamEndedPacket.Reason.FAILED, the no-burn exit. A real take-off has already been burned and cleared by the server, so that report is a no-op; a desync only hands the Live Eye back.

Open screens are never broadcast. With any screen open (the chat box, a sign or book being written, an inventory, another mod's menu), the frame is read before the screen draws, under the same rule as the Options preview (StreamCapture.point → previewPoint). It is read after the GUI with Show chat on, and before the chat layer with it off. Show chat to viewers therefore only governs the chat lines on the HUD, which include whispers; what is being typed never goes out. The consent card's Livestreaming line and the Livestreaming / Show chat tooltips say so.

Since v0.1289.0 (PR #2006) listed menus stream again while Show GUI to viewers is on. The blanket rule above meant Show GUI covered only the HUD, and players reported that their inventory, Esc menu and advancements never reached viewers. client/live/StreamableScreens now allowlists screens by class lineage:

  • every AbstractContainerScreen except AnvilScreen (inventories, chests, furnaces, villagers, modded containers);
  • PauseScreen;
  • AdvancementsScreen, plus Better Advancements' screen, matched by name.

A listed screen is still withheld while the player is typing into it: a focused EditBox, a creative tab with a search bar, or an open recipe book whose search box is focused or holds text (read by reflection, failing closed). The chat box, signs, books, command blocks and unknown mod screens keep the pre-screen read.

StreamCapture.point gains a screenShown input:

  • A listed screen with chat on sends the finished frame.
  • With chat off it sends FRAME_END_HIDE_CHAT, which cancels the chat layer for that frame. Container screens dim the HUD rather than cover it, so chat lines would otherwise show through.
  • Show GUI off is unchanged: world and hand only.

Known gap: JEI's search overlay is not a screen child, so terms typed into it can show on an inventory.

Since v0.1301.0 (PR #2024) books, photos and albums stream too. The list adds BookViewScreen (and LecternScreen), BookEditScreen, Exposure's PhotographScreen (and ChildPhotographScreen) and AlbumViewScreen (and LecternAlbumScreen); the album itself (AlbumScreen) is a container, so it was already listed. Writing streams only where the Live Eye word filter masks it as it is typed:

  • a BookEditScreen is withheld when LiveTextCensorClient.bookFiltered() is false (an editor plot, where prop books are authored content and go unfiltered);
  • an album note being edited (an Exposure TextBox focused on the screen — its own widget, not an EditBox) is withheld, since album notes are not filtered;
  • AlbumSigningScreen stays off the list for the same reason.

Live Eye streams on through death; viewers can take the Live Eye (v0.1304.0)

Dying live no longer ends the stream (PR #2028, relay dp-relay #802).

  • The server still burns the Live Eye and sends STOP_DIED. A client already on air keeps its relay claim and streams the death moment and the recap pages (client/live/LivePostMortem).
  • Those screens go out even with Show GUI to viewers off; they are listed in StreamableScreens.
  • End Stream, right of LIVE in the viewfinder (LiveRecOverlay), ends it. It is drawn after the frame grab, so only the streamer sees it.
  • The stream also ends on leaving the world, a server respawn, a takeover, or when no DT death screen appears within 2 s (Train Builder, other servers).

The moment of death loops behind the first death page (client/live/LiveDeathClip, DeathClipBackground):

  • At death the client notes stream time (frames written ÷ fps). It waits for the segment holding the death to finish, copies the segments out, and cuts the last 5 s plus 0.5 s after.
  • The cut is 640×360, silent, with the top 22% cropped to drop the broadcast's own name pill. It is saved to dungeontrain/live-clips/death.mp4.
  • It loops in colour behind the opening page, under its dip-to-black and vignette (LiveHlsSession.localLoop, file-only ffmpeg). Until it is ready, the page shows the ride photo.
  • The clip is uploaded as the session's death.mp4. The relay turns it into the GIF on the #live-eye "has gone offline — died" post, ahead of the death photo.

Death takeover:

  • The dying client calls POST /live/died, which opens a 90 s offer (LIVE_DEATH_TAKEOVER_MS). It waits until viewers have seen the death (v0.1316.0, LiveDeathHandover): the call goes out once the uploader has published segment floor((deathS + 2 s) / S) + 2. Viewers join at the newest listed segment and trail it by up to one segment, so by then every TV has shown the death. At the default 10 s segments that is about 20–30 s after death. If uploads stall it goes out 60 s after death anyway.
  • Viewers wearing a TV on their head are offered it, through the status poll (every 3 s while an offer is open). They get a chat line, " died live. Their stream is up for grabs: [Take the Live Eye]", plus a button under the TV box (LiveTakeoverOffer, LiveTakeoverButton).
  • Not offered to Free Play runs on release builds, nor without internet consent, ffmpeg or Livestreaming.
  • The first to accept gets a one-use relay ticket. LiveTakeoverPacket (PROTOCOL 134) sends it to their server, which checks GET /live/takeover/verify and swaps the worn TV for a Live Eye.
  • The normal equip path starts the stream, and the claim carries the ticket past tenure. The dead streamer's next upload is refused, which cuts them off (toast Someone else went live).
  • Later clickers hear "Someone else took over first."

Live Eye takeover request — vote, prompt and Eye-to-TV (v0.1305.0)

Putting on a Live Eye while someone else is live no longer takes the stream straight away (PR #2029, relay dp-relay #803).

Tenure. A streamer's on-air time is protected for 10 minutes (LIVE_TAKEOVER_MIN_MS, was 60 s). It carries across re-wears and relogs when nobody else streamed in between and they are back within 30 minutes (LIVE_TENURE_GAP_MS; relay live.js tenureMs / carriedTenure).

The prompt. The new mod claims with confirmFlow:true. Against an on-air holder the relay answers 409 with holder, tenureMs, takeoverMinMs, streamCount (livestats.streamRuns) and, while another vote runs, pollEndsTs. The Live Eye then works as a TV (LiveStreamController state BLOCKED) and client/live/TakeoverRequestScreen shows "X has been streaming for Y, this is their Nth time streaming."

  • Inside X's protected minutes: Request takeover opens a vote.
  • After them: Take over — the wearer's own call; it claims with takeover:true.
  • Stay a viewer, or Esc: the Live Eye becomes a switched-on TV.
  • Taking the Live Eye off while undecided still burns it. One request per Live Eye.
  • While another player's vote is running no prompt is shown; the Live Eye waits for it to close and claims again (LiveTakeoverFlow#blocked).

The vote (relay live-takeover.js, 20 s, one per cap). The request closes the prompt; a countdown strip — "The viewers are voting… N s" — draws over the live box for the requester, the streamer and TV viewers (LiveHeadViewer#voteBanner, after the frame grab so it never goes on air).

  • A poll line goes into the Live chat ring (source:'poll'), Discord and Twitch. In game it has clickable [Yes] [No] (/dungeontrain-live-vote); the requester gets the line without buttons, and the streamer is told their vote's weight.
  • Votes are chat lines — yes, no, !yes, !no — on every surface. live-chat.js setVoteHook counts them and keeps them out of the room; the mod also keeps a typed vote out of vanilla chat, and the bot deletes a Discord vote post (needs Manage Messages).
  • One vote each, last one counts; the streamer's weighs ceil(20% × viewers); the requester can't vote. More Yes wins; more No loses; a tie goes the streamer's way (rejected if they didn't vote). Void if the streamer left the air.
  • The result prints as the verdict and the tally: "Takeover approved. Bob takes the Live Eye." / "5 - Yes | 2 - No". Approved → the requester gets a one-use ticket their next claim carries past tenure (ticket; a dead streamer's takeoverTicket from the death takeover above works the same way).

Server. A wearer is pending (LiveStreamers.addPending) until their client reports the relay's answer with net/LiveClaimResultPacket (LIVE, REJECTED, STAYED_VIEWER; PROTOCOL 135). LIVE makes them the streamer; the previous one on the same server is told STOP_REPLACED. Losing the stream to someone else — on the same server or by a cut-off from another world (Exit.REPLACED / CUT_OFF) — now turns the Live Eye into a switched-on TV (LiveFeedEvents#toTv, "The Live Eye moves on and leaves you a TV") instead of burning it; taking it off, death, Free Play and moderation still burn it.

Live Eye moments in Live chat: advancements, photos and books (v0.1312.0)

While a player streams with the Live Eye, three kinds of moment are shared into the stream's Live chat: the Discord channel, Twitch chat and the website Live page (PR #2053, relay dp-relay #812 + #813).

Moment Discord (as the player) Website / Twitch
Advancement earned 🏆 earned **[Title]** 👁️ Name 🏆 earned [Title]
Photo taken 📷 took a photo + the photo on its paper the line, with the photo under it on the website
Book signed 📖 wrote **"Title"** + the first lines quoted 📖 wrote "Title": first lines…
  • What counts.
    • Advancements: only ones vanilla announces in chat (DisplayInfo#shouldAnnounceChat), never dungeontrain:editor/*, never a login replay.
    • Photos: any Exposure frame except projected ones. The shot is encoded on regular photograph paper (PhotoPaperTextures), the same framing the tribute-photos channel uses.
    • Books: every signed book outside editor plots, after the Live Eye word filter. The excerpt is the first written page, cut at a word near 200 characters, ending in … whenever the book holds more.
  • In game nothing new renders. Players see only the original vanilla message. The relay keeps moment lines out of the game's chat read, and LiveChatRules.shouldShow drops them too.
  • Settings. Options → Dungeon Train → Stream has Share advancements / Share photos / Share books, after Live chat. They are on by default, and Kid mode keeps them off and greyed. They are stored client-side in dungeontrain-live-client.toml (liveMomentAdvancements, liveMomentPhotos, liveMomentBooks).
  • How it flows.
    • The server (event/LiveMomentEvents, compat/photo/LiveMomentPhotos) spots a moment for the player in LiveStreamers and sends LiveMomentPacket (PROTOCOL 139) to that player only.
    • The client (client/live/LiveMoments) checks the toggle, then posts POST /<CAP>/live/chat/moment {token, kind, title?, excerpt?, image?} with the stream token.
    • The relay (live-chat.js#sendMoment) names the poster from the live claim. It masks the text like typed lines, gives moments their own rate bucket (burst 6, then one per 10 s), and posts a photo to Discord as an attachment. The website shows only Discord CDN images.
    • An older relay answers 404; the client logs it once and drops the moment.

Live Eye stream sound (v0.1269.0)

Live Eye streams carry in-game sounds: everything the streamer hears from Minecraft and Dungeon Train, music included. They never carry the microphone, voice chat or other apps (PR #1981, relay dp-relay #764). It's on by default. Turn it off under Options → Dungeon Train → Stream → Stream game sound (liveFeed.streamAudio); the change takes effect from the next stream.

How it works: the mirror mixer (client/live/audio/)

  • While a stream with sound is live, every OpenAL call the sound engine makes on a Channel, SoundBuffer or Listener is replayed onto a twin in a second, speaker-less OpenAL Soft loopback context.
  • That context is made current per thread, so the streamer's own sound path is never touched: no rerouting and no added latency.
  • LiveAudioPump renders the mix at 48 kHz stereo, paced by the encoder's written-frame count, with 60 ms of leading silence for sync (measured offset: +13 ms average).
  • ffmpeg reads the mix from a loopback-only TCP port and muxes it as AAC into the same HLS segments.
  • Sounds decoded before go-live are re-decoded in the background (MirrorPredecoder).
  • If ffmpeg dies before the first upload with sound on, the stream carries on without sound.

Full volume

  • The stream is a full-volume mix whatever the streamer's sliders: the listener gain stays at 1 and each source's category slider is undone (StreamVolume).
  • DT's own music fades are kept.
  • While streaming with sound, a sound whose slider is at 0 still plays: silent on the speakers, heard on the stream.

Only allowed sounds (StreamSoundPolicy)

  • Only files served from vanilla or an approved mod's mod/<id> pack reach the stream.
  • Resource-pack and server-pack overrides are refused, and so is anything that never came through the sound cache (e.g. Vista TVs).
  • With an unapproved mod loaded on the client (which covers client-only mods on multiplayer servers), the stream goes out silent and the streamer gets a chat line. In single-player, Free Play already stops the stream.

Where viewers hear it

  • Twitch: the push copies the AAC track; streams without sound still get a silent track.
  • Live page: each rendition carries the sound. The page starts muted and shows a Sound on / Mute pill.
  • Homepage background and in-game TVs: stay silent.

Perf line fields: audio renderMs / calls / twins / twinMB / decodes / stalls / blocked.

Live Eye text filter — masked chat and books while live, strikes, bans and appeals (v0.1286.0)

The Live Feed is mirrored to Brennan's Twitch channel, so player-written text on a stream is a ban risk for the channel. While anyone is live, listed words in chat and book & quill text become **** and the writer is warned (PR #2003, relay dp-relay #789).

Where it runs. All matching is local and synchronous (moderation/TextCensor: fold → whole-word → phrase → collapsed-substring → CJK passes), so the stream never waits on it. The streamer's own client masks a word in the chat box or book page the moment it is completed (client/live/LiveTextCensorClient, as-you-type via ScreenEvent.CharacterTyped/KeyPressed.Post) and again as a chat line leaves (ClientChatEvent at HIGHEST, ahead of LiveChatBridge, so the relay's Twitch/Discord chat and the signed packet both carry the clean line). The server masks every player's chat while anyone on the server or the relay is live (event/LiveChatCensorEvents, ServerChatEvent at LOWEST — Discord Presence rebuilds the message from raw at NORMAL) and signed books (ServerGamePacketListenerImplSignBookMixin, the kept book is rewritten). Scope is moderation/LiveModerationScope: server = ServerLiveStatus.isAnyoneLive; client = streaming or in the live chat room; books on the client only on the writer's own stream. A client that masked a word reports it with LiveModerationStrikePacket (source, the line as typed, the terms), since the server only ever sees ****.

The word list (moderation/LiveWordList) is a small baked floor (assets/dungeontrain/moderation/live_words.json, hand-curated, no CC-BY source) plus the relay's overlay GET /live/moderation/words — a projection of the relay's moderation-wordlist.js (slur, sexual and profanity categories; LIVE_MOD_CATEGORIES, LIVE_MOD_EXCLUDE for words Twitch allows such as bollocks/prick/twat/nsfw) with an allow-list of innocent words (Nigeria, Scunthorpe, raccoon…). Fetched once per session at the title screen / server start, cached to config/dungeontrain-live-words.json; offline the baked list applies. Operator overrides on the relay word list flow through on the next launch — no mod release.

The ladder lives on the relay (livestrikes.js, cap-scoped): strikes 1–3 → "The Live Eye rejects that word. Warning N of 3." · 4th strike → "The Live Eye has no warnings left and burns away." — the Live Eye burns, the stream ends (LiveFeedEvents.Exit.MODERATED, STOP_MODERATED, toast Broadcast rules broken), strikes reset, removals +1 · 3rd removal → banned from the Live Eye for 1 week, then 1 month, then permanently (Exit.BANNED). Strikes decay after 30 clean days; two reports within 10 s count once. Banned players loot TVs instead of Live Eyes (LiveEyeLootResolver), equipping one burns it, and the relay refuses /live/claim with 403 banned. event/LiveModeration mirrors the verdict (fetched at login and on every refused Live Eye) and reports strikes through the durable RelayOutbox to POST /live/moderation/strike; a relay outage never bans anyone and never clears a cached ban.

Evidence and appeals. Every strike is logged on the relay with the line as typed and the matched terms (live_strike_log). The ban message lists the lines that earned it (masked), each with a clickable [Appeal]; /liveappeal lists them, /liveappeal <n> [why] appeals one line (one appeal per ban; command/LiveAppealCommand, on CommandAllowlist). The relay posts a notice to OPS_ALERT_WEBHOOK_URL (falls back to DEV_WEBHOOK_URL) and the explorer's Appeals page (dt.brennan.games/#/live-appeals) shows the appeal beside every logged strike with the appealed line highlighted. Approve lifts the ban and forgives one removal and one ban; deny blocks another appeal until the ban lapses.

Dev: /dungeontrain debug live-moderation force on|off makes the filter act as if someone were live; … strike walks the ladder. New packet + two appended enum constants ⇒ PROTOCOL_VERSION 129. No config switch — enforcement is relay-side.

Links and phone numbers (v0.1288.0). A second, pattern-based pass (moderation/PatternCensor, PR #2005, relay dp-relay #790) runs after the word pass on every surface: URLs (scheme or www.), bare domains with a known TLD, Discord invites, IPv4 addresses and phone numbers (7–15 digits in up to five groups; two-group numbers need a +, a ( or a 7-digit run) become ****. Never a strike — a grey "Links and phone numbers are hidden while the Live Feed is on air." at most once per 10 s per player (LiveModeration.warnLinkHidden). Same scope as the word filter, so off-stream chat keeps its invites. Version strings, coordinates, dates, times and scores are left alone (pinned in PatternCensorTest). The relay masks the same patterns in on-air chat before Twitch/Discord (livemoderation-words.censorPatterns).

Livestream leaderboards — Longest Stream and Longest Viewer (v0.1274.0)

Four Tallyman boards rank time on the Live Feed, each as All Lives and One Life (PR #1986, relay dp-relay #769):

Board Cover Heading
live_stream_time / live_stream_run Longest Stream Who's worn the Live Eye longest?
live_view_time / live_view_run Longest Viewer The longest TV stream viewer?

The relay measures both from traffic it already sees (livetime.js). Stream time is the gaps between a claimed session's presign heartbeats (one per ~10 s segment), counted once the first playlist is up. Watch time is the gaps between a viewer's /live/status polls (every 15 s while a TV block, a held TV or a worn TV shows the feed). It counts only while the channel is on air, never for the streamer watching themselves, and only when the poll carries lb=1. The mod sends that only behind the master internet consent. A gap over 30 s is a break and earns nothing. Beats live in memory and flush every 60 s into live_time / live_life_time, then onto the boards.

One life. The server sends each client its life id at login and every respawn (net/LiveLifePacket, the same PHOTO_LIFE_ID the photo boards use, via player/LifeKey). Stream claims carry lifeId/freePlay, and ranked polls carry life/fp/v. A life scores one-life only when it is not Free Play and states its version; it lands in that version's and year's eras. Free Play still counts all-lives. Mods older than v0.1274.0 earn all-lives stream time only.

Faulthurst stat book. Two subjects match the one-life boards: "You've worn the Live Eye for %s." and "You've watched TV streams for %s." Stream seconds are counted server-side while the player is the server's streamer. Watch seconds come from the client (net/LiveWatchedPacket) after each ranked live poll, credited no faster than real time (player/LiveWatchTally, 30 s cap).

Livestream ended pop-up (v0.1254.0)

Every end of a stream that went live raises a top-right pop-up on the streamer's screen: red "Livestream ended" with the reason under it — You died, You left the world (save & quit, disconnect), Live Eye taken off, Someone else went live (takeover or relay cut-off), Free Play run, The recording stopped (PR #1960, client/live/LiveStreamEndedToast). It replaces the old "You are no longer live" and death chat lines; the Live Eye's burn lines stay in chat. A start that never went live (no ffmpeg, relay unreachable, Livestreaming off) still explains itself in chat only. The pop-up is raised once the stream is idle, so it never appears in the broadcast. On save & quit it is held until the world is gone (LiveStreamController#endedAfterLeave) — vanilla's Gui.onDisconnected clears every toast after LoggingOut fires — so it shows over the title/server-list screen.

Carrying a TV (v0.1224.0)

A Vista TV in the hand is a portable set (PR #1926). Left-click with the crosshair on nothing switches it on or off — aimed at a block or an entity in reach the click stays an ordinary attack, and there is no swing either way. The main hand toggles when it holds a TV; the offhand toggles when the main hand is empty (a sword in the main hand keeps swinging). The state is the dungeontrain:tv_on data component (registry/ModDataComponents, helpers in compat/vista/TvItems); the client flips it at once and sends net/TvHeldTogglePacket, the server re-checks the hand and sets the real stack (protocol 122 → 123). The on/off change is excluded from the hand re-equip animation (mixin/vista/TVItemMixin), and F swaps a TV between hands instantly: client/live/HeldTvToggleClient takes the key before vanilla, swaps the client stacks and sends vanilla's own swap packet, so the server echo matches.

While on, the Live Feed plays on the screen in first and third person: mixin/vista/TvItemRendererMixin wraps Vista's TvItemRenderer.renderTvHead, turns and lifts the set so the screen faces the viewer (client/live/HeldTvScreen — main hand yaw 105°, offhand yaw 120°, both lifted; picked from a ten-pose sheet) and draws the same shared decode every TV block shows through Vista's CRT vertex consumers (LiveFeedSource#screenConsumer: snow, download bars, waiting pattern or the picture). Off, on the ground, in the GUI or on the head nothing changes — Vista's smile hat stays. Placing a TV that is on puts the block down switched on (TVBlockMixin#setPlacedBy through the latch in TvPowerToggle), so it tunes itself like any TV that comes on.

Dev tooling: ./gradlew runClient -PquickWorld="<save>" -PtvPoseShots opens the save and client/live/HeldTvPosePuppet photographs the held set in its candidate poses to run/screenshots/tvpose-NN.png; -PtvPoseSwap instead presses F and logs both hands per tick. Both are property-gated and inert in production.

Switching a placed TV (v0.1310.0)

A placed Vista TV is a latch (compat/vista/TvPowerToggle): a redstone pulse or a right-click flips it on or off, whatever is in hand (PR #2046; before it only an empty hand did). The held item is not used or placed — a clock no longer shows Vista's time overlay and a TV no longer extends the wall on a plain click. Sneak + item uses the item as normal (vanilla skips the block), which is how a block or another TV is placed against one; sneak + empty hand keeps Vista's pause/eject, and a Live Feed Cassette never ejects. A TV that comes on with an empty slot tunes itself to the Live Feed. Code: mixin/vista/TVBlockMixin#dungeontrain$clickToggles.

Live Feed headpiece — camera helmet with a target-sensing aerial (v0.1197.0)

The headpiece that broadcasts the Live Feed (dungeontrain:live_headpiece, worn in the head slot) is a full-face camera helmet: Exposure Polaroid's instant camera scaled to the head, dark base as the jaw, cream block as face and crown, a square lens on the eye line, the eject wedge at the nape, and a short aerial on the back right of the crown (PR #1893). The aerial tip shows what the wearer is looking at, to everyone nearby: red for a hostile mob, green for any other living thing (players, villagers, animals, neutral mobs), blue for a block, unlit for nothing within 32 blocks. In a hand, on the ground or in a slot it stays unlit.

No networking is involved: vanilla's CustomHeadLayer resolves a head item's model with the wearer as the item-property entity, so a client item property (dungeontrain:antenna_target, client/live/LiveHeadpieceAntenna) ray-casts for each wearer once per client tick and the model's overrides (models/item/live_headpiece.json → _red / _green / _blue) swap the tip texture. Classification lives in client/live/AntennaTarget. Body faces sample Polaroid's own camera texture; the aerial and tip textures are DT's. The Blockbench source and the design alternatives it was chosen from are under design/blockbench/helmet-cam/.

Live Eye (v0.1202.0). The item is named Live Eye in English (the chat lines and the viewer keybind label follow), and its inventory icon is a hand-drawn 16×16 sprite rather than the 3D helmet: front-on, turned on the Y axis, white faces with a dark lens, a red rec pip and the aerial on the crown, so it sits with vanilla's icons beside the Polaroid (PR #1902). The four item models are neoforge:separate_transforms wrappers: the 3D live_headpiece_base still renders on the head and in hands, the sprite in the GUI, item frames and on the ground, and the sprite's aerial tip recolours with the same overrides (live_headpiece_icon{,_red,_green,_blue}.png). The wrappers declare gui_light: front; without it a parentless wrapper is side-lit and the sprite renders at half brightness (LiveHeadpieceModelTest pins this).

Ender lens (v0.1208.0). The lens window inside the Polaroid bezel shows a zoomed-in Ender Pearl while the Live Eye is idle and an Eye of Ender while it is worn, which is when it is live (PR #1910). The glass face samples vanilla's own minecraft:item/ender_pearl / ender_eye sprites at uv [4,4,13,13], their largest centred square with no transparent pixel, so resource packs carry through. A second client item property, dungeontrain:live (1 while the stack is on its wearer's head, LiveHeadpieceAntenna#isWorn), selects live_headpiece_live; it is the first override, so the red/green/blue tip variants (which also carry the eye) still win when lit. The inventory icon's lens is the pearl. v0.1211.0 grew the lens to 10×10 of the 16-unit face (bezel [3,2]–[13,12], window 5.5×5.5) and dropped the Polaroid's cream photo-exit strip from the lower front — that face now samples the sprite's plain black body and the photo_slot overlay is gone (PR #1913); the inventory icon was left as it was.

Chat lines (v0.1226.0). The Live Eye tells the wearer what it is doing in plain lines (PR #1929). Once the stream is up and the relay's first viewer count arrives, the client says "You are now live, N people watching" (green; it falls back to 0 after 10 s with no count). After that it says "You have N viewers" (grey) each time the count settles in a new band — 0 · 1–10 · 11–100 · 101–1000 … — up or down. A new band must hold for 10 s before it is announced, so a count wobbling across a boundary stays quiet (client/live/LiveViewerBands). Each burn says why (LiveFeedEvents#burnLineKey): "The Live Eye burns away as you take it off", "The Live Eye moves onto the next streamer" (another player on the server, or a claim from another world cutting it off at the relay), or the old generic flare line for death and Free Play. When the burn ended a broadcast, a red "You are no longer live" follows (#wasLiveOn: not when Livestreaming was switched off, and not for a Free Play burn on the way on). The tooltip reads "Livestream the wearer's perspective." / "Destroyed when removed, or next person equips." Counts are pluralised per locale through PluralRules.clause.

Streamer's own pinned feed (v0.1232.0). While you stream, the pinned box showing your own broadcast decodes at the box's real pixel size (box GUI width × GUI scale, even, at least 160, never above viewerWidth) instead of the 640 px viewer width — unless a TV block has shown the feed in the last 15 s, which keeps the shared decode at full width. A size change swaps decoders behind the current picture, so the box never blanks (PR #1935; client/live/LiveDecodeWidth, LiveStatusPoller#tvWanted).

Stream settings cog (v0.1236.0). While you stream, the inventory (survival or creative — the E menu, not chat) shows a 20×20 cog button directly under the pinned box, flush with its right edge; it follows the box when you click the box to change its size. Hovering shows "Stream settings"; clicking opens Options → Dungeon Train → Stream, and Done returns to the inventory (PR #1939; client/live/LiveStreamSettingsButton). Like the streamer's box it is drawn in the post-frame pass after the frame grab, so it never appears in the broadcast; the click is caught in LiveHeadViewer#onMousePressed. The icon is a hand-drawn 16×16 sprite, textures/gui/sprites/icon/stream_settings.png.

Name on stream (v0.1253.0). The streamer's name sits top-middle of the broadcast picture as a pill — white with a black outline, a pulsing green dot (35%↔100% every 1.2 s), then Name in bold and a translated "is live" (gui.dungeontrain.live.name_tag, in the streamer's language; PR #1971, v0.1261.0; it was white text with a black outline before). With Live chat off (or in Kid mode) it reads "… is live with chat disabled" instead (gui.dungeontrain.live.name_tag.chat_off; PR #1973), so viewers know the streamer won't see their messages — on every frame — HUD, menus, GUI off — until the player turns it off (PR #1959; client/live/StreamNameTag). It is drawn into the capture target after the blit and before read-back (LiveFrameGrabber), and into the Options preview (StreamPreview), so viewers see it and the streamer's own screen never does. Options → Dungeon Train → Stream → Name on stream picks Minecraft (default), Discord or None (streamNameTag in dungeontrain-live-client.toml). Discord shows the linked Discord name (refreshed at stream start) and shows nothing, never the Minecraft name, if the account isn't linked. Kid mode forces None and greys out the row.

Disposable Camera (v0.1106.0)

A one-shot camera built on Exposure: Polaroid's instant camera (PR #1762). It needs no slides and takes 320 × 320 photos.

  • Take the photo: the viewfinder closes by itself so the print animation plays in your hand.
  • When the print finishes, the photo takes the camera's inventory slot and the camera drops and burns, like a book.
  • The photo burns after you have looked at it (opened from your hand and closed). Stacked with other photos, only the disposable-camera ones burn.
  • The photo also burns whenever it is dropped — thrown, dragged out of the inventory, lost on death or spilled from a container — like a DT book (PR #1765, v0.1109.0).
  • An unused camera does not burn when dropped; it is an ordinary item until it has taken its photo.
  • Its slide can't be taken out or topped up.
  • Creative: the Narrative tab. It is not in any loot pool yet.
  • Selfie hint (PR #1886, v0.1196.0): a dark key cap under the viewfinder reads [F5] Selfie, or [F5] Front in selfie mode. It shows the player's Toggle Perspective binding, moves with the viewfinder, and hides with F1, with the camera controls open, or when the key is unbound. Drawn from ViewfinderOverlaySelfieHintMixin → client/ViewfinderSelfieHint, inside Exposure's own overlay pose, because Exposure hides the rest of the HUD in the viewfinder.

Code: compat/DisposableCamera (the stack and its markers), compat/DisposableCameraEvents (shot → close viewfinder → print → burn), client/DisposablePhotoClientEvents + net/PhotographViewClosedPacket (photo viewed → burn), event/StartingBookEvents#dropAndBurn (the shared burn). Known gaps: a flagged photo placed in a photograph frame or album is shown without burning; a disposable camera on a camera stand does not close or burn.

Photo frames on the train (v0.1182.0)

Exposure's photo frames hung on a wall now stay visible on a moving carriage (PR #1864). Hanging one places DT's photograph_frame block, sized like Exposure's: the largest of 1×1, 2×2 or 3×3 that fits. It works just like the entity:

  • Use a photo on it to put the photo in.
  • Use glow ink to make it glow.
  • Right-click to turn the picture.
  • Left-click to take the photo out.
  • Breaking it drops the frame and the photo.

Frames saved in a build come back on the right wall when the carriage spawns mirrored. Floor and ceiling frames are still Exposure's entity. Details: Compatibility.

Photo albums (v0.1185.0)

Exposure's photo albums circulate on the train (PR #1868, relay dp-relay#713). There are two loot items:

  • dungeontrain:your_photoalbum becomes the holder's own album when it is taken in hand. A crafted exposure:album becomes its crafter's album too.
  • dungeontrain:random_playerphotoalbum becomes another player's album, read-only. With none available, it becomes the finder's own album instead.

They are placed in the writing barrel (random 2, yours 1, against the other items ×5), pot (1 each), and bookshelf and starter (random only, 1).

Your album is one album per player. Every copy of it is a window onto the same album: its pages are filled from the store when it opens, so editing one edits them all.

  • Opening it to look costs nothing.
  • Putting a photo in it burns it at once. It drifts off down the train, to be found again with that photo in it.
  • Taking a photo out or changing a note is saved when it closes.
  • It can't be signed, and it always carries its owner's name.

A player has two albums, chosen by the run:

  • the Live album, kept on the relay, which follows the player between computers and can be found by others;
  • the Free Play album, which never leaves the player's computer.

Both are stored in the game folder (dungeontrain/albums/<uuid>/live.json and free_play.json, plus images/), so they follow the player into every world.

Someone else's album is an Exposure signed album. It burns as soon as its view closes and never expires on the relay; each opening is only counted. It can't be put on a lectern.

The album owns its pictures. Each page is stored by picture hash: sha256 of the unfiltered PNG scanlines, the same hash the relay's community photos use. The picture itself is copied into the album's store, so a loose community photo expiring never empties an album page. On the relay the pictures are reference-counted, and a file is deleted only when no album names it. Rejecting a community photo blanks the same picture in every album.

Privacy:

  • Only pictures others may see reach the relay: the owner's own photos, community photos, and pictures that already came from albums.
  • Anyone else's picture stays a private page.
  • The Live album uploads only with the player's sharing consent.

Performance: pictures are decoded in the background and lent to a world's Exposure store (dt_album_<hash>) once per world. Album work is logged as [DT-Album] … took N ms when it reaches 20 ms.

Code: compat/photo/album/, the mixins AlbumMenuMixin, AlbumItemOpenMixin and AlbumSignC2SPMixin, client/FoundAlbumClientEvents, and net/AlbumViewClosedPacket (Exposure shows signed albums on a client-only screen, so the client reports the close).

PlayerMobs and cameras (v0.1146.0)

PlayerMobs treat a dropped Disposable Camera as something worth picking up, and a camera gifted to one — thrown to it as a player, not in Creative — asks it for a photo of you (PR #1813, requires PlayerMob 0.106.0).

  • Who takes the picture. A PlayerMob that feels neutral or better toward you (its feeling is read as it stood when you gave the camera, before the gift itself warms it) and is not fighting, fleeing, climbing back onto the train or on fire. A mob that dislikes you keeps the camera and takes no photo.
  • What you see. It puts the camera straight into its hand, raises it to its face (Exposure's own camera pose, viewfinder-open click), turns to you, fires the shutter, lowers the camera, and a few seconds later tosses you the print; the spent camera drops and burns as yours does. A pending photo outranks greeting and gift-giving, so it happens at once.
  • The photo. Rendered by your client from the mob's eyes (so you must be within 64 blocks), credited to the mob by name, 640 × 640. It is a disposable-camera print like any other: it burns after you have looked at it and whenever you drop it. A PlayerMob's own drop never ignites a photo — its hand-over, a gift, a backpack spill or its death loot land intact. A found photo also burns when a player drops it.
  • Discord. Each PlayerMob photo is posted top-level to the passenger log ("📸 mob took a photo of player") once your client has delivered the pixels, under the same network consent as shared books. It is never added to the found-photo pool.
  • A second gifted camera means a second photo. Nobody is awarded Exposure's frame stats or advancements for a mob's shot.

Code: mixin/PlayerMobCameraHolderMixin (the mob as Exposure CameraHolder + CameraOperator), compat/PlayerMobCameraBridge (pickup predicate, gift reaction, equip-on-credit), compat/PlayerMobPhotoGoal (viewfinder → shot → print → hand-over → burn; priority 0), compat/PhotoRequestPolicy, discord/PlayerMobPhotoReporter + SharedPhotos#queueMobPhotoPost, compat/PlayerMobDrops + event/StartingBookEvents (the burn-on-drop exemption).

Shared player photos (v0.1128.0)

A photo printed by the Disposable Camera is sent to the relay (losslessly, 640 × 640) and can turn up for other players as a found photograph in train loot. Projected (outside-image) photos are never sent, and nothing is sent without the same network consent shared books use.

  • Life of a photo. A photo can be opened ten times across all players (the photographer's first look does not count), then it is gone. Paying Tribute gives it ten more from wherever it is. The relay keeps 10,000 photos; the least recently added or tributed drop off first.
  • Viewer. Under a found photo: your place among its viewers (3 / 10), the Tribute button with its cost, and X. Space also closes. Closing burns the photo; paying Tribute burns it in green flames.
  • Your own photo. Under a photo you just took: Tribute (when offered), X and a trash icon. Hovering shows Tribute, Close and Trash. Trash burns the photo and takes it back from the pool so no other passenger finds it (PR #1859, v0.1180.0). If it hasn't been sent yet it never is; if it has, the relay retires it (POST /photos/withdraw, flag withdrawn, dp-relay #697), even when the withdraw arrives before the upload. A Free Play photo was never shared, so Trash just burns it.
  • Tribute costs emeralds: 1 for a photo's first Tribute, one more each time after. Emerald blocks are broken down to pay, with change returned. A Tribute that is the most the player has ever paid (across every world, clean runs only — stats/<uuid>-tribute.json) also posts the photo to the passenger log, drawn on the worn paper it had when tributed (tears transparent) with a line saying how many passengers have held it and how close it came to fading (PR #1798).
  • Wear. A found photo's paper gets more torn and stained as its views run out; tears cut into the picture.
  • No repeats. A player is not given a photo they have already opened unless someone else has paid Tribute since. A photographer whose photo was tributed finds it first.
  • For builders. Put dungeontrain:random_playerphoto in a container's contents; it becomes a found photograph when the carriage spawns (the slot is empty if none is available). A playerphotos loot prefab (a barrel of photos) is included for testing.
  • Advancements in the viewer. Opening a photo shows the advancements it earned its photographer as a column of advancement badges down the photo's left side (found photos from the pool, and your own fresh prints; PR #1930, pool field from dp-relay #744). Hidden advancements keep their secrets: earned ones show their description, ones you can see but haven't earned show their hint, and ones you haven't found yet show only the icon with ?. Stored on the photograph as dt_photo_advancements (compat/photo/PhotoAdvancementBadges; icons resolved on the server). The Tribute / X / trash row sits just below the photo.
  • Live Gallery (website). brennan.games/dungeontrain/tributes/ hangs every live, tributed photo as a polaroid on three drifting washing lines; scrolling pushes them along and keeps bringing in photos not yet shown. Each print shows the photographer, its total Tribute value in emeralds and the advancements the shot earned its photographer (uploads carry meta.advancements since PR #1923; older photos show none). A new Tribute drops into the middle line with a card naming who paid and how much. Relay: cap-less /tributes/* routes (dp-relay #742); Reject in the explorer removes a photo within a minute.

Code: compat/photo/ (SharedPhotos, OwnPhotoTribute, OwnPhotoTrash, PhotoPngCodec, TributePayment, WornPhotographs, PhotoPaperTextures), client/PhotoTributeButtons, net/OwnPhotoTrashPacket, discord/TributePhotoReporter + PhotoPaperComposite, advancement/GlobalTributeStats. Relay: playerphotos.js in dp-relay.

Versioning

mod_version lives in gradle.properties. Follows SemVer:

Change Example
Dev commits on a branch PATCH (0.3.1, 0.3.2, …)
Feature merged via Gate 3 MINOR, reset PATCH (0.3.x → 0.4.0)
Breaking save format MAJOR (0.x.x → 1.0.0)

Every MINOR/MAJOR bump gets a v<version> tag on the merge commit.

Books and photos advancement tab (v0.1164.0)

Books and photos have their own advancement tab (PR #1839). Like the rest of Dungeon Train's advancements, its entries are hidden until you get close, so this section describes how the tab behaves rather than what is on it.

  • What moved. The book advancements moved here from the Dungeon Train tab. They kept their ids, so progress you already earned (in a world, or banked across worlds) carries over. Nothing on this tab counts toward the Dungeon Train capstone, and the start-again reset leaves it all earned.
  • Camera advancements. These are earned by what a photo shows and where it was taken, by paying Tribute to photos (your own or someone else's), and by catching a photo a passenger took of you. Exposure's and Exposure: Polaroid's own advancement tabs are removed, though their recipe unlocks are kept.
  • Your photos are kept. The photo that earns a camera advancement is saved on your computer, on the paper it was printed on, under dungeontrain/user/advancement-photos/. Because it is saved locally, it follows the advancement into later worlds.
    • Hovering the earned advancement shows a small print.
    • Clicking it opens the full photo over the advancements screen. A click anywhere puts it away.
  • Collections. Collections show an album with a slot for everything they ask for. Each slot holds the photo logged for it, or a ??? frame while it is still missing. The album only shows photos that count in the current life: a photo from another world, or from a Free Play run, stays hidden until you take that subject again.
  • Which tab opens. The advancements screen opens on this tab when a camera, photo or book is in your hand, or you used one in the last 30 seconds. Otherwise it opens on Dungeon Train. Since v0.1164.0 one click always switches tabs; before, a tab could ignore clicks until another tab had been clicked.
  • Biome riding advancements. These now count every biome the train passes through and ask for more. They were re-issued, so everyone earns them again.
  • Livestream and TV advancements (v0.1256.0). Going live with the Live Eye, keeping a stream running, and handling the TVs (placed, held and worn) now earn advancements here. So do two photos that need good timing (PR #1962). The timed ones count unbroken stretches. The lifetime streaming total is kept per player (livestream_ticks), and every threshold comes from the advancement JSON, so the advancement editor can retune it. Code: event/LiveTvAdvancements, player/ServerLiveStatus#isSomeoneElseLive, compat/photo/PhotoScene (the emerging-Warden window).

Code:

  • advancement/EnchiridionAdvancements (tab membership and albums)
  • compat/photo/PhotoAdvancementEvents and PhotoSubjects (what a photo shows)
  • compat/photo/AdvancementPhotoCapture with net/EarnedPhotoPacket (which photo earned what)
  • client/EarnedPhotos, EarnedPhotoScreen, EarnedPhotoAlbumScreen and EarnedPhotoThumbnails
  • client/DefaultAdvancementsTab

Advancement tabs (v0.1195.0)

Dungeon Train's advancements are split into themed tabs: Dungeon Train, Train Explorer, Challenges, Others, The Enchiridion and The Darkroom. They are always in that order, and each has a final advancement. Clicking a final advancement rings in aqua everything it still needs. A finished tab turns gold, and so does the window border while it is selected. Other mods' tabs are hidden by default (PR #1883).

→ Read more

All at Once / The Challenge (v0.1270.0)

Two Challenges-tab advancements for keeping every "do without" challenge at once for a whole life: no apples, melon only, no armor, no broken blocks, no chests, no damage dealt, no ender chest. All at Once is at 100 carriages and The Challenge at 1,000. Breaking any one rule loses both for that life; the individual challenges are unchanged. Clicking either one highlights its rules, in game and in the advancement editor (PR #1982).

From v0.1311.0, a player still holding every rule gets one melon slice on every 5th flatbed once they are 10 carriages into the life. It drops as they reach the room next to that flatbed, on that flatbed's near half only, once per flatbed per life (PR #2052).

→ Read more

Gift your Live Eye to a first-time TV viewer (v0.1303.0)

PR #2026, relay dp-relay #801.

The streamer's prompt

When a viewer joins the stream's chat by wearing a TV, their client posts POST /live/chat/join {uuid, name, eligible}.

  • eligible means not in Kid mode, Livestreaming on, and no Live Eye carried or worn (LiveGiftClient#canReceive).
  • The relay passes a game-only joins[] notice to the streamer only when all of these hold:
    • the viewer is eligible;
    • the viewer isn't banned;
    • the viewer has never streamed (live_sessions);
    • the viewer hasn't joined in the last 12 minutes.
  • The streamer sees "X joins via a TV, they have never had the Live Eye. [Gift it to them]". This shows at most once every 12 minutes (LiveGiftClient.PROMPT_GAP_MS).

The offer

Clicking [Gift it to them] offers the streamer's own Live Eye (POST /live/gift, proved by the stream token).

  • The viewer sees "X offers you the livestream. Take the Live Eye? [Accept] [No]".
  • The offer is silently skipped if the viewer is no longer eligible.

Accepting

[Accept] sends LiveGiftAcceptPacket (protocol 133). The viewer's own server then:

  • redeems the gift with the relay (POST /live/gift/claim, which works once and only for that uuid);
  • puts the Live Eye on their head, which starts their stream;
  • moves the TV to their inventory.

A donation, not a copy

  • Redeeming ends the giver's stream. The stop reason is gifted, so the giver's next upload is refused and their Live Eye burns ("The Live Eye moves onto the next streamer").
  • The recipient takes the channel at once. The minimum time a streamer must hold the channel before anyone can take it over doesn't apply.
  • A void gift expires. If the giver has already stopped or been replaced, the gift expires.

Server-side checks

The server repeats the eligibility checks only as a logged anti-cheat backstop; they never show a chat line. Joins and gifts are seq steps beside the chat ring's lines (ring.events), so Discord, Twitch and the website never see them.

/streamer in Live chat (v0.1315.0)

Anyone in the Live chat can type /streamer (or !streamer) to ask who is on air: on the website Live page, in #👁️-live-eye, on Twitch, or in game while connected (streaming, or wearing a TV while someone is live) (PR #2059, relay dp-relay #815). The relay answers once, for the whole room, from records it already holds:

Streamer: Steve · Twitch: steve_plays · Discord: SteveTheBuilder · Live for 1h 23m · Streamed 4 times before · Language: Deutsch (German)

  • Fields: MC name and on-air time (live.tenureMs, so a relog doesn't reset it); Twitch from the claim or the linked account; Discord only if linked with /dtlink; past streams from livestats.streamRuns (a relog is not a new stream); language from the claim's locale. Missing fields are left out. Nobody live → "Nobody is live right now".
  • Throttle (relay, in memory, per channel): one answer per 2 min for the room, one per 15 min per person per streamer (website + Discord share the Discord id; Twitch keys on user id; game on uuid). A different streamer clears both; a relog does not. Developers (LIVE_ADMIN_DISCORD_IDS, LIVE_ADMIN_TWITCH_LOGINS, LIVE_ADMIN_GAME_UUIDS) are exempt and start no timer. A throttled ask stays ordinary chat; only the website sender gets a private "Try /streamer again in …".
  • Plumbing: relay streamer-command.js via live-chat.js setCommandHook (runs after the line joins the ring), answering with an info ring line (game read, website, Twitch) plus the Discord webhook. In game it is a client command (client/live/StreamerCommand) that sends /streamer through LiveChatBridge.sendCommand; out of Live chat it does nothing. Old clients render the info line as [Live] Dungeon Train: ….

Test controls in the editor

Since: v0.1318.0 (PR #2063)

While a test is running (the bottom button reads Exit Test Mode), the X editor screen's template pane gets controls for the test itself. None of them change the template.

Test toolbar. A second icon row sits under the editing one, above the preview:

Button What it does Command
Reload Stands the test up again on the same rolls, re-reading the saved templates, so an edit saved elsewhere shows in place /dungeontrain portal test reload
Re-roll the rest Keeps the template under test as it is and rolls everything around it again (the carriage around contents, the contents in a carriage, the line around a track piece, the frame and ground around a room) /dungeontrain portal test reroll others
Re-roll this template Rolls only the template under test (the old Shift+Reseed) /dungeontrain portal test reseed focus
Re-roll everything Rolls the whole test again (Reseed) /dungeontrain portal test reseed

Test settings. While testing, the data sheet gives way to three rows. Changing one stands the test up again on the same rolls, so the setting is the only thing that differs:

  • Stage: Auto, or any stage. Auto keeps the old behaviour (the editor's focused stage, or the default palette for a dimensional carriage) until the level or band is moved, then uses the stage play would pick there.
  • Diff level: Auto (the level a test has always used) or any level. It drives template level gates, contents, loot and mob gear in the test copy. Click +1, Shift/right-click -1, Cmd/Ctrl-click to type, and a blank value means Auto.
  • Band: Auto or any band of the route, for template phase gates and stage choice.

Each player's settings are kept in memory between tests until the server stops: /dungeontrain portal test set stage|level|band <value|auto>. The carriage test commands take reload and reroll others too (/dungeontrain editor test reload). Overrides only apply inside a test stamp (template/TestGate), so a live train never sees them.

Clone this wiki locally