Feature Death Screen Buttons - bh679/dungeon-train-mc GitHub Wiki

Death screen β€” the narrative recap

Since: v0.315.0 β€” PR #427 (paginated narrative rewrite) Current layout: v0.325.0 β€” advancements + chest/book icons on the cargo page (PR #471)

When you die on a singleplayer Dungeon Train world, the vanilla DeathScreen is replaced wholesale by NarrativeDeathScreen β€” a slow, single-button retrospective the train tells about you. It walks through six kinds of page, one at a time, over a cinematic cross-fade, and ends on a platform offering a fresh world or the title screen. In Dungeon Train, death ends the run β€” there is no respawn-in-place, so even hardcore worlds get this screen.

This page documents the current narrative screen. The pre-#427 layout β€” a static "two-panel run recap" with a New World / Title Screen button row (v0.177.0 – v0.288.0) β€” has been superseded; the run-stats it showed now live across the fall, deeds and cargo pages below.

The renderings on this page are schematic mock-ups, not in-game screenshots β€” they are generated from the screen's actual layout code (palette, geometry, on-screen text) by tools/render-death-screens.py, drawn over the flat-overlay background (the no-photo fallback). In game, a ride photo usually sits behind the text.

Page flow

Death screen page flow

A single centred Continue advances; a bare back-arrow returns. The number of creator's ledger pages depends on how many feedback-survey questions you have left unanswered (often one or two; none once you've answered them all). reboard and leave the line sit in the top corner throughout.

# Page (Kind) Kicker What it shows
1 FALL the fall / death cause This-life headline: carriage, distance, time aboard
2 DEEDS the deeds This-life combat grid + a portrait of the passenger the run was about
3 GEAR the cargo Most-used weapon + worn armour, loot containers, books read
4 LIVES all your lives Cross-world lifetime totals
5 SURVEY the creator's ledger One unanswered feedback-survey question per page
6 MODREC the recommendations Which of your own mods you'd recommend β€” or flag as a cheat; only if you run any
7 PLATFORM the platform Epitaph + Board anew / Leave the line

The pages

1 Β· the fall

The fall page

The opening headline. Since v0.318.0 the title is the second-person cause of death ("You fell from a high place"), captured from the damage source; if the server sent none it falls back to the static the fall kicker. Below the train the line asks its first question β€” by default "What is the Dungeon Train?" β€” over a rolled narration ("Carriage thirty-seven. That is where the dark reached you…"), then three cells: carriage (carriages traversed), distance (metres ridden), time aboard (run duration). Stat figures woven into the narration are coloured white. The exact lines are rolled from the death_lore pool β€” see that section for every variant.

2 Β· the deeds

The deeds page

The line asks "Why are you here?" over a 3Γ—2 grid of this-life combat: mobs felled, passengers met (distinct PlayerMobs encountered), slain, befriended, damage dealt, damage taken. The narration recounts the same figures ("You felled eighty-four things in the dark, and bled for it with two hundred and five hearts…"). Since v0.319.0 an animated full-body portrait of the passenger the run was about sits beside the grid β€” the one you befriended (drawn left) or, failing that, killed (drawn right), with their name beneath. The grid shrinks to make room; if neither applies it stays full-width.

3 Β· the cargo

The cargo page

The line asks "What are you meant to do?" over a gear row: the run's most-used weapon

  • your four worn armour pieces, then a chest icon and a purple (enchanted) book icon carrying the loot-containers-opened and books-read counts in the native item-stack number style (hover any slot for its tooltip β€” the gear shows item tooltips; the chest/book show "Containers Opened" / "Books read"). The default narration: "Iron and edge, gathered from the dark and carried this far. None of it was enough β€” perhaps that was never the point."

Below the gear row sits the advancements row (since v0.325.0) β€” see Advancements on the cargo page.

4 Β· all your lives

All your lives page

Here the line stops asking and reflects: by default the question is the wry "These are all good questions" (a callback to the three it just posed), over six cross-world lifetime totals (from GlobalPlayerStats, persisted by player UUID): total aboard, carriages, distance, deaths, friends made, books read. The narration weaves in your death count ("…Across all ten β€” every dark you have boarded…"). On your first death a different variant runs β€” question "Is one life so easily spent?" with the sub-line "your first fall on the line."

5 Β· the creator's ledger

The creator's ledger page

One page per unanswered feedback-survey question β€” a 0–N rating row plus an optional comment, or a free-text answer box for open-ended questions. All pages share the fixed intro lang line "Before the train carries you back into the dark, the line's creator keeps a ledger β€” not of carriages, but of the travellers who ride." The actual questions:

Order Question From Type
1 "How likely are you to recommend this game to a friend? (0 = not at all, 10 = extremely)" Discord Presence (built-in NPS, SurveyRegistry) 0–10 scale + comment
100 "If you could change one thing about the mod, what would it be?" Dungeon Train (data/dungeontrain/dp_surveys/change_one_thing.json) free text

The survey system lives in the bundled Discord Presence mod; answers post to the community Discord through its public API, and an answered question is never re-asked. If you have no unanswered questions, this page set is empty and the flow skips straight to the platform.

6 Β· the recommendations

Since: v0.544.0 β€” PR #889 Hack reports: v0.628.0 β€” PR #1057

"Which of these would you recommend to the creator?" β€” a scrollable grid of the mods you're running that aren't Dungeon Train's or its modpack's, ordered by how many other Dungeon Train players run each. Pick a tile, write a line in the comment box, and Send; the tile turns green, drops to the bottom of the grid, and you can pick another. A pinned "Something not installed…" tile takes the name of a mod you don't have and would like the train to support β€” each sent request leaves its own card below the grid.

Every installed-mod tile also carries a small ⚠ at its right edge β€” the page's other half. Clicking it flags that mod as a cheat or hack instead of recommending it: the tile turns red, the prompt changes to "What does it let you do?", and the button reads Report. A flagged mod sinks to the bottom of the grid with a ⚠ where a recommended one keeps its βœ“. Clicking the tile body rather than the icon still starts an ordinary recommendation, unchanged.

The "Something not installed…" tile has no icon: there is nothing to report about a mod you don't have. A mod is recommended or flagged, never both β€” sending the other one replaces its tile rather than leaving two.

This page usually does not exist. It only enters the deck when your loaded mod list contains something outside the roster, so a player on the plain modpack with nothing added never sees it.

A few deliberate constraints:

  • A comment is required β€” for a report too. Send stays disabled until the box has text β€” a bare tile click says nothing the telemetry doesn't already say.
  • Popularity orders the tiles but is never shown. No counts, no percentages. The ranking comes from GET /<CAP>/mods/popular on the relay (distinct players per modId, cached 15 min); if that fetch fails or times out, the grid falls back to alphabetical and the page still works offline.
  • Your comment reaches the creator privately, and nowhere public. It posts to the private Discord survey channel, and β€” since v0.550.0 β€” also travels with the /telemetry/mod-rec report so it can be read alongside the numbers instead of only by scrolling Discord. A hack report travels the same way, marked with a hack flag (🚩 in Discord); the relay does not store that flag yet, so for now a report is read in Discord. The relay keeps the two apart on arrival: the analytics event still carries just the modId and a "was a request" flag (no player text, the rule the book-read and world-info reports follow), while the mod's name and your comment go to a separate capped table that only the private data explorer can read. Nothing you write here is ever shown to other players. Before v0.550.0 the relay stored no text at all, so a request β€” which has no modId, only the name you typed β€” left no record of what had been asked for.

The roster (what counts as "ours") is generated into the jar at build time by the generateModRoster Gradle task, from neoforge.mods.toml + modpack/modpack.config.json, so it can't drift from the modpack that actually ships. Matching is on a normalised key tried against both modId and display name, because the config stores CurseForge slugs while ModList reports modIds. Six exceptions are hand-listed with reasons in build.gradle: iris, highlighter, khi, sablejade (modIds matching neither slug nor name), and veil + sablecompanion (Sable's own dependencies, which neither source file knows about). The Forgified Fabric API's ~44 fabric_* modules are excluded by raw-id prefix rather than enumeration.

If the roster resource is missing or unreadable the page fails closed β€” no leftovers, so it simply never opens, rather than offering you Dungeon Train's own components to recommend.

7 Β· the platform

The platform page

The end of the line. The line asks "Will you board again, or step off into the quiet?" over a closing narration ("The platform is empty. The whistle sounds far down the line…") and a muted epitaph ("the tenth to fall. perhaps not the last."), then the two actions β€” Board anew (the prominent primary) launches a fresh world; Leave the line exits to the title screen. There is no Continue button here; the train motif has filled the whole rail, "a train that completes itself exactly as the narrative ends."


The narrative text (death_lore)

Every question, narration, sub-line and epitaph on pages 1–4 + 6 is data-driven, rolled per death from a weighted pool β€” data/dungeontrain/death_lore/default.json. Per-install files in config/dungeontrain/user/death_lore/*.json add more variety (they never override by id). At death the server rolls one weighted entry per page whose conditions match, substitutes placeholders, and ships the result inside DeathStatsPacket (see DeathLoreStore). The creator's ledger (page 5) is not here β€” those come from the survey system.

Placeholders are substituted as English words and drawn white in-screen: {carriage} {friends} {books} {mobs} {met} {slain} {hearts} {deaths} β€” plus {distance} as grouped numerals. So {deaths} = 10 β†’ "ten".

⚠ Voice note for any new copy: because {deaths} renders as a word, the platform epitaph template the {deaths}th to fall yields "the tenth to fall" β€” clean at 10, but "the twenty-threeth to fall" at 23 (no ordinal grammar). Avoid {number}th ordinals in new lines, or phrase around them.

The pool, verbatim

the fall β€” the question is always "What is the Dungeon Train?"

Wt Condition Narration
3 β€” "Carriage {carriage}. That is where the dark reached you. Do you know it now? The Dungeon Train does not stop β€” it only goes on, deeper, forever."
1 β‰₯100 carriages "{carriage} carriages into the dark. Few ride so far before the line takes them. Fewer still board again."
1 ≀3 carriages "Carriage {carriage}. Barely aboard, and already the dark had you. The line is patient β€” it can afford to be."

the deeds

Wt Condition Question Narration
3 β€” "Why are you here?" "You felled {mobs} things in the dark, and bled for it with {hearts} hearts.
Of the {met} souls you met, {slain} you ended β€” and {friends} you called friend."
1 β‰₯5 friends "Were they worth it?" "You made {friends} friends on the line, and still it took you. Perhaps that is the only number that was ever yours to keep."
1 β‰₯200 mobs "How much of the dark did you answer in kind?" "{mobs} things fell to your hand before you fell to theirs. The line does not judge. It only counts."

the cargo (gear)

Wt Condition Question Narration
3 β€” "What are you meant to do?" "Iron and edge, gathered from the dark and carried this far. None of it was enough β€” perhaps that was never the point."
1 β‰₯5 books "What are you meant to do?" "You read {books} of the line's books and still carried steel. Knowledge and a blade, and the dark took both."

all your lives (lives)

Wt Condition Question Narration / sub-line
3 β‰₯2 deaths "These are all good questions" "These are not the marks of a single life. Across all {deaths} β€” every dark you have boarded, every time the line took you and still you returned β€” this is the whole of your journey."
1 =1 death "Is one life so easily spent?" sub-line "your first fall on the line." β€” "There are no other lives to weigh against this one β€” not yet. This is the whole of you: everything done since first you boarded. The line will remember it. So should you."
1 β‰₯10 deaths "These are all good questions" "These are not the marks of a single life. {deaths} times the dark has taken you, and {deaths} times you boarded again."

the platform

Wt Condition Question Narration + epitaph
3 β‰₯2 deaths "Will you board again, or step off into the quiet?" "The platform is empty. The whistle sounds far down the line. It runs without end β€” and so, it seems, do you." Β· epitaph: "the {deaths}th to fall. perhaps not the last."
1 =1 death "Will you board again β€” now that you know how it ends?" "The platform is empty, the same as it was when first you stepped aboard. The whistle sounds far down the line. You have fallen once. Whether there is a second time is, as ever, yours to decide." Β· epitaph: "the first to fall. the line is patient β€” it will wait for the rest."
1 β‰₯20 deaths "Need you even ask?" "The platform knows your face by now. The whistle sounds the way it always does. You already know what comes next." Β· epitaph: "the {deaths}th to fall β€” and not, it seems, the last."

Voice summary (for new copy): second person, addressed to the dead passenger by the line itself; archaic and unhurried; recurring imagery β€” the dark, the line, the platform, the whistle, boarding/falling, the train that does not stop. The fall/deeds/cargo questions are existential ("What is…", "Why are you…", "What are you meant to…"); all your lives deflects ("These are all good questions"); the platform turns outward ("Will you board again…?").


Chrome & controls

  • Top bar β€” the dungeon train brand on the left; reboard and leave the line chips on the right, present on every page. They mirror the platform's two actions, so you can bail to a fresh world or the title at any point.
  • Footer β€” a centred Continue on every page except the platform, and a bare back-arrow (←) to its left (hidden on the first page).
  • Advancing β€” only a click on Continue advances. Since v0.323.0 Space and Enter no longer advance the screen (they were dismissing it instantly on respawn); a click during a running fade skips that fade rather than acting on the controls beneath it.

Backgrounds & transitions

  • Ride-photo backgrounds β€” since v0.317.0 the game quietly takes third-person photos of your journey and uses them as page backdrops. Each page is assigned one photo up-front, preferring unused shots so pages don't repeat (v0.319.0), chosen from thematic chains (scenic / combat / gear / social). With ride snapshots off or none captured, the screen falls back to the flat dark overlay shown in the renderings above. Captures are down-scaled to a 1080px long edge (RideSnapshotCapture.MAX_EDGE β€” 640px before v0.469.0) and stored losslessly (PNG) for the in-game gallery/disk cache; the copy sent over the network (to the server for the Discord embed below) is re-encoded as JPEG at that point instead, since a lossless 1080px PNG of a busy 3D scene runs close to the 1 MB packet cap.
  • Cinematic page turns β€” since v0.321.0 moving between pages plays a slow fade: the UI fades out over the held photo (fully revealing it), the bare photo holds for a beat, then the next page's UI fades in while the photo dips to black and the next one rises from black beneath it over a long ~10 s reveal.
  • The death moment β€” since v0.1198.0 the recap no longer opens on the frame you die. For ~0.75 s the live world keeps rendering β€” vanilla's death-camera tilt, a red wash, the train still rolling with you carried along on your carriage (DeadRiderCarry; Sable only carries living riders) β€” and the recap's photos are decoded, uploaded and JPEG-encoded off the render thread meanwhile (DeathMomentScreen, DeathPhotoUploads). Before this, all of that ran synchronously on the death frame and read as a lag spike that "killed you". The moment is hard-capped at death tick 19: vanilla removes the dead player at tick 20, which drops the train's Sable tracking, so the recap must be up before then. Click / Space / Enter skips it after half a second. The recap then opens fully black and its first photo rises from black over the same slow reveal a page turn uses.
  • Train engine β€” the engine sound is full on the first page (as if still aboard) and fades evenly to silence by the last, rising again if you step back. The screen does not pause the world (it keeps ticking behind the fade), and Esc does nothing.

How it works

The swap

DeathScreenLayoutHandler listens for ScreenEvent.Opening. When the screen being opened is a vanilla DeathScreen and an integrated (singleplayer) server is present, it swaps in a DeathMomentScreen, which opens a fresh NarrativeDeathScreen itself once the death moment has played (see above). Neither replacement is a DeathScreen, so the re-fired Opening event for it is a no-op β€” no recursion. LAN / dedicated-server deaths are left on the vanilla screen: the singleplayer guard (getSingleplayerServer() == null) bails, because the mod can't tear down and recreate a world it doesn't own.

Data flow

Everything the screen renders is gathered server-side per life and pushed to the dying client in one packet:

  • RunStatsEvents accumulates counters on PlayerRunState (a per-player attachment that resets on respawn). On LivingDeathEvent (LOW priority, so other kill-credit handlers finish first) it builds a DeathStatsPacket carrying: the per-run figures, worn armour + resolved most-used weapon, the cross-world lifetime totals (from GlobalPlayerStats), the second-person death cause, the DEEDS portrait subject, and a DeathNarrative.
  • DeathNarrative is the server-rolled story (question / narration / sub-line / epitaph per page), drawn from the death_lore data pool by DeathLoreStore. The client lacks the death context and the pool, so it renders exactly what the server picked. The creator's ledger pages are not in here β€” they come from the Discord Presence survey system.
  • DeathStatsCache holds the packet client-side; the screen reads it live each frame, so a one-tick-late packet simply fills in. Any change to the packet layout must bump DungeonTrainNet.PROTOCOL_VERSION.

World launch β€” Board anew / reboard

DeathScreenLayoutHandler.launchWorld(...) reads the live integrated server for the dying world's settings and creates a fresh save:

  1. Capture LevelSettings / WorldOptions / data-pack config from the running server.
  2. Read DungeonTrainWorldData from the overworld and stash it in PendingWorldChoices (train Y, starts-with-train, carriage dims) along with the generation mode + group size; roll a fresh starting dimension into PendingStartingDimension. WorldLifecycleEvents commits these to the new world on first start.
  3. If keepInventory is on, snapshot the player's inventory + XP (re-applied on the next world's first login by KeepInventoryCarryEvents) and mirror the gamerule onto the new world.
  4. Pre-drain Sable train sub-levels on the server thread, then tear down the integrated server with the vanilla "Save and Quit to Title" sequence (mc.level.disconnect() β†’ mc.disconnect(Screen)) before createFreshLevel(...).

Leave the line runs the same pre-drain + disconnect dance, then mc.setScreen(new TitleScreen()) β€” skipping vanilla's "Are you sure you want to give up?" confirm.

What's carried over

Board anew reuses the dying world's settings on a fresh random seed:

Setting Source Carried?
World seed WorldOptions.randomSeed() New random seed
Game mode, difficulty, hardcore, allowCommands WorldData.getLevelSettings() Same
Data pack configuration WorldData.getDataConfiguration() Same
generateStructures, generateBonusChest WorldData.worldGenOptions() Same
Train Y, starts-with-train, carriage dims DungeonTrainWorldData on the live overworld Same (via PendingWorldChoices)
Generation mode, group size DungeonTrainConfig (per-save TOML) Same (via PendingWorldChoices)
Starting dimension always Overworld as of PR #643 (v0.390.0) β€” previously rolled fresh 94 % Overworld / 5 % Nether / 1 % End StartingDimension.OVERWORLD (via PendingStartingDimension)

Sable sub-level pre-drain (Sable issue #679)

Train carriages are Sable sub-levels. launchWorld / goToTitleScreen delete them and pump ServerSubLevelContainer.tick() for up to ~1 s per level (10 s hard ceiling) before triggering disconnect, so MinecraftServer.stopServer's while (chunkMap.hasWork()) loop doesn't spin forever on the "freshly-created world β†’ immediate disconnect" path. See the project's "save-and-quit hang" notes for upstream context.

Discord mirror

When a server enables deathReportToDiscord, the same recap is posted to Discord as a "πŸ’€ <player> β€” Run Ended" embed through the bundled Discord Presence mod: the death cause as the description, the run-stats + combat figures as embed fields, and the most-used weapon + worn armour composed into the embed image. RunStatsEvents.postRunSummary builds the field list from the same DeathStatsPacket the screen renders, so the embed stays in lockstep with the on-screen pages. It is off by default and posts via the same relay / webhook the Discord Presence integration uses. Damage values reuse the rounded-HP / kΒ·M abbreviation via the shared, unit-tested DeathReportFormat formatters. The death message in #minor-updates is in third person (the screen's is second person β€” see SecondPersonDeathMessage).

Separately, a top-level dev/public report (DeathReportBuffer) waits up to 5 s for the client to deliver this run's scenic ride photo (ride.jpg, JPEG-compressed as of v0.469.0 per the note above) via DeathPhotoPacket, using it as the embed image; if it never arrives (ride snapshots off, none captured, vanilla client, or a dropped packet) the report posts with a gear-composite icon instead so it always lands.

Hardcore & multiplayer

  • Singleplayer (incl. hardcore): the narrative screen replaces the vanilla death screen. Death ends the run regardless of hardcore β€” Board anew / Leave the line are the only ways off.
  • LAN / multiplayer: getSingleplayerServer() is null, so the swap is skipped and the vanilla death screen is shown β€” we can't recreate someone else's world.

Advancements on the cargo page

Since: v0.325.0 β€” PR #471

Shipped as a second row on the cargo page, below the gear row: the Dungeon Train advancements you earned this life, each drawn in its real vanilla advancement frame box (per-tier β€” ⬛ task / 🟑 goal / 🟣 challenge), floating on the Dungeon Train advancements-tab background (the same texture the L-screen tiles) inside a thin framed panel.

  • Scope: Dungeon Train advancements only β€” excludes vanilla, other mods, and the Editor tab β€” matching the "see all advancements" button, which opens the Dungeon Train tab.
  • Hover a box for the menu-style tooltip (title + tier-coloured description).
  • Scrollable when you've earned more than fit (~10): the row clips to a scroll viewport you can wheel through; a thin scrollbar shows position.
  • The "see all advancements" icon button sits outside the scroll viewport and opens the vanilla advancements screen on the Dungeon Train tab (via the existing DefaultAdvancementsTab auto-select); closing it returns to the cargo page (new AdvancementsScreen(advancements, this)).
  • The whole section fades with the page on Continue β€” background + frame boxes via setColor + RenderSystem.enableBlend() (the vanilla LoadingOverlay pattern), border / scrollbar / button via the page fade. The item icons can't alpha-fade (Minecraft's GUI item renderer ignores tint β€” the same limit the loadout row has), so they pop in/out with the page's other item icons.

Data flow

Per-life earned advancements are tracked server-side: AchievementEvents.onAdvancementEarn records each genuine (non-login-replay) dungeontrain: non-editor earn into a per-life set on PlayerRunState (in-memory β€” the codec is at its 16-field cap, like narrativeLetters), shipped in DeathStatsPacket (PROTOCOL_VERSION 30β†’31) and resolved client-side from the connection's advancement tree (icon / title / description / tier). Frame boxes are blitted directly from the obtained-frame textures (task/goal/challenge_frame_obtained) so they fade with the page.

See Advancements for the full tab + cross-world-persistence model.


Files

Path Purpose
NarrativeDeathScreen.java The paginated screen β€” page renderers, chrome, transitions, click handling.
DeathScreenLayoutHandler.java Swaps in the screen on Opening; world launch + Sable pre-drain.
DeathScreenMoveTextMixin.java Vanilla-death-screen text tweak (the fallback / multiplayer path).
DeathStatsPacket.java / DeathNarrative.java Carry the run + lifetime stats, narrative, death cause, portrait to the client.
DeathStatsCache.java Client-side cache the screen reads each frame.
RunStatsEvents.java / PlayerRunState.java Per-run stat tracking; builds the packet; the Discord mirror.
client/snapshot/ (DeathBackgroundAssigner, DeathBackgroundPainter, RideSnapshotGallery) Ride-photo capture, per-page assignment, and painting.
DeathPortraitRenderer.java The DEEDS-page animated PlayerMob portrait.
assets/dungeontrain/lang/en_us.json gui.dungeontrain.death.* keys (kickers, cell labels, button text).
wiki/tools/render-death-screens.py Generates the schematic renderings on this page.

Regenerating the renderings

The mock-ups are generated from the layout code, so re-run the tool after a layout change:

cd wiki
python3 tools/render-death-screens.py     # writes images/death-screen/*.png

It mirrors NarrativeDeathScreen's palette + geometry with Pillow (no game launch required). Edit the example stat values / narration at the top of each page_* function. If the on-screen layout changes materially, update the matching draw helper so the renderings stay faithful.

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