Feature Random Books - bh679/dungeon-train-mc GitHub Wiki

Home > Features > Random Books

Random Books

Standalone flavour books that spawn in carriage chests as vanilla written books — rolled from a JSON pool at carriage-spawn time, tracked per-world so re-reads don't repeat until everything's been seen.

Overview

A parallel content track to the Narrative Lectern. Where the lectern delivers long-form ordered narratives one Letter at a time, Random Books are short standalone musings that get scattered through carriage loot at varied weights. As of v0.184.0, both systems share world-scoped progression.

Two layers of randomness:

  • Pool layer: which book file wins the slot (weighted by each book's weight field).
  • Variant layer: which musing within that book is shown (uniform within the book's variants array).

A single editor-only placeholder item dungeontrain:random_book is the only thing authors interact with. At chest-spawn time, ContainerContentsRoller.rollItemStack substitutes it with a stamped vanilla WRITTEN_BOOK from the pool. Players never see the placeholder — only the rolled written book sits in the chest.

Bookshelf drops (v0.265.1)

A second, organic source alongside the editor placeholder. When a player breaks a vanilla minecraft:bookshelf, each book that would drop has a 1-in-N chance — server config narrative.randomBookFromBookshelfOneIn, default 100 (≈1%); 0 disables — to be replaced by a Random Book rolled from the same pool via RandomBookFactory.rollFromPool. The total drop count is unchanged: one plain book is swapped for one narrative book.

Handled by RandomBookDropEvents, a server-side BlockDropsEvent listener:

  • Only minecraft:bookshelf — Silk Touch drops the block (no books → natural no-op), and chiseled bookshelves are excluded so player-stored books are never converted.
  • Only player-broken blocks.
  • New ItemEntitys inherit the replaced drop's position + velocity, so a book dropped on a moving Sable carriage rides the ship exactly like the books it replaces.

Freshly-rolled books carry no held marker, so per BurnableBookTag (see Starting Books' burn lifecycle) they are non-burnable until a player picks them up — they land on the ground and can be collected, just like a random book falling out of a broken pot.

World-scoped read tracking (v0.184.0)

Every spawned random book carries an identity tag (bookBasename, variantIndex) in DataComponents.CUSTOM_DATA. When any player on the world equips the book (hand-held in main or off-hand), a LivingEquipmentChangeEvent handler:

  1. On first equip of that tuple → mark it seen on the world in NarrativeProgressData, do nothing else.
  2. On equip of an already-world-seen tuple → ask RandomBookFactory.pickUnseenForWorld for an alternative, mutate the stack's WRITTEN_BOOK_CONTENT and identity tag in place, mark the new tuple world-seen.

Either branch stamps a sticky dt_random_book_held marker on the stack. v0.397.0 (PR #655) fixed the handler to bail out immediately when that marker is already set — previously, reselecting the same held book in the hotbar (which re-fires LivingEquipmentChangeEvent for MAINHAND) would re-run step 2 every time and keep swapping the stack to a new random title/body. Now a book's content locks permanently the first time it's held; subsequent hotbar reselects are no-ops.

When every variant of every loaded book has been seen by the world, the random-book tracking resets silently and the cycle starts over.

Pre-mutation happens on equip (not on right-click to read) because WrittenBookItem.use opens the book screen client-side from the local stack before the server's response arrives — mutating later is too late.

Author workflow

  1. Drop a JSON file in data/dungeontrain/narratives/random_books/<basename>.json:

    {
      "id": "musings_of_faulthurst",
      "title": "Musings of Faulthurst",
      "author": "Faulthurst",
      "generation": 0,
      "weight": 3,
      "variants": [
        "First musing body...",
        "Second musing body...",
        "..."
      ]
    }
    • weight = pool-level rarity vs other books
    • generation = vanilla 0–3 (Original / Copy / Copy of Copy / Tattered)
    • Each variants entry becomes one chosen-at-spawn book body, auto-paginated to vanilla limits
  2. Place dungeontrain:random_book into any container variant via the Container Contents Menu (C-key editor) or directly in a <prefab>.json loot template. Set its count and weight like any other item entry.

  3. /dungeontrain narrative reload to pick up new pool files without restarting the server.

Commands

Command Purpose
/dungeontrain narrative randombook list Chat-print every loaded random book with variant count + weight + generation
/dungeontrain narrative randombook give [basename] Give the calling player a rolled vanilla written book; no basename → pool-weighted random pick
/dungeontrain narrative randombook progress Show the world's seen-variant count per book
/dungeontrain narrative randombook reset Clear the world's random-book read tracking
/dungeontrain narrative reload Re-scan both random books and stories

All gated on permission level 2 (OP).

Where it ships

v0.182.0 (PR #228) seeds dungeontrain:random_book into 16 prefab loot tables at varied weights — pot is the most book-heavy at weight 9, narrative/themed prefabs (mining, villager, horse, loot_irongold) at 2, the rest at 1. The bundled pool ships one book — musings_of_faulthurst (16 short musings on the train and its passengers).

v0.265.1 (PR #328) adds bookshelf-break drops (see above) as a second source, and rebalances the bricks/fish/mining prefab pools (raising non-book weights / lowering random_book weight) so random books read rarer in chest loot.

Creative tab

A new Dungeon Train — Narrative creative tab (icon: vanilla written book) groups the two narrative-system items:

  • Random Book (orange Knowledge Book parent — visually distinct so authors don't confuse it with real written books in the editor)
  • Narrative Lectern (also still in vanilla Functional Blocks)

Persistence

World-scoped tracking lives in the existing NarrativeProgressData saved-data (overworld-keyed). The map shape is Map<bookBasename, Set<variantIndex>> serialised under random_books at the root of <world>/data/dungeontrain_narratives.dat. Pre-v0.184.0 saves had a per-player layout (TAG_PLAYER_RANDOM_BOOKS under each player entry); on first load with v0.184.0+ that legacy data is silently dropped and the world starts fresh.

Multiplayer

Tracking is world-scoped, so every player shares the same seen-set. The first player to encounter a (book, variant) tuple marks it world-seen; subsequent holders of an already-seen tuple get swapped to an unseen pick. The mutate-on-equip approach still races if two players grab tuples on the same tick — fine for single-player and small-server use.

Out of scope

  • Lectern-mounted random books (lecterns remain narrative-story-only)
  • Per-player chest view (the second opener of a chest sees what the first opener resolved)
  • Custom 16×16 texture for the placeholder (Knowledge Book parent works for MVP; can upgrade)
  • Tag-based per-biome pool filtering (tags is parsed but unused — reserved for a future placement system)

Related

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