Feature Book Voting - bh679/dungeon-train-mc GitHub Wiki

Book voting — 👍/👎 on community books

Since: v0.489.0 (PR #816) · relay counterpart dp-relay #169 (v0.52.0)

Players vote thumbs-up/down on the community books other players wrote. Votes tint the book's burn, and feed back into the relay's loot selection so well-liked books surface more often for everyone.

The vote page

A virtual page appended after the last real page of every player-written (shared) book — dev-authored content (random/starting/narrative books, letters, deathnotes) is not votable, enforced client-side and again in the packet handler.

  • The page is dimmed with a warm leather tint (full paper coverage, sampled from book.png bounds) so it visibly is the train's page, not the author's parchment.
  • Rust-orange "The train asks," + one of 10 questions, picked deterministically per book (hash of bookType:bookId), so a book always asks the same thing.
  • 18×18 thumb sprites in the vanilla page-arrow palette (widget/thumbs_*, auto-stitched); hover/selected states are green (up) and red (down). Labels (Y)es / (N)o.
  • Implementation is mixin-free: BookViewScreen.setBookAccess (public vanilla API) swaps in a pages-copy + one empty page on Init.Post; everything on the page draws in Render.Post with manual hit-testing (BookVoteClientEvents). Re-built on window resize. Lecterns excluded.

Casting

Clicking a thumb or pressing Y / N from any page commits instantly:

  1. BookVotePacket (C2S, PROTOCOL_VERSION 47) — the server re-resolves the held main/off-hand stack via BookIdentity and drops mismatches (thrown/swapped/spoofed).
  2. The server stamps dt_book_vote (±1) into the stack's CUSTOM_DATA (BookVoteTag) — the vote works fully offline (burn color + reopen seeding survive without any network).
  3. The book closes immediately and the train answers with one random gray chat line from the matching set plus a general set (10 yes / 10 no / 10 general — 20 candidates per vote).
  4. Consent-gated (NetworkConsentMirror, fail-closed): BookVoteReporter enqueues {uuid, player, bookType, bookId, vote, clientTsMs} to the durable RelayOutbox → POST /books/vote.

Changing your mind is allowed — last-write-wins on clientTsMs, so out-of-order outbox re-deliveries can't roll a newer vote back, and a same-vote retry is deduped relay-side before the rate limiter (idempotent retries never 429). Closing or throwing without voting registers nothing. Reopening a voted book shows the previous vote still lit.

Burn colors

StartingBookEvents' burn lifecycle derives a FlameVariant per burn: soul (Death Note — takes precedence) → approved (green) / rejected (red) from the stamped vote → default orange. Voted burns mix colored DustParticleOptions 50:50 with the vanilla orange flame so the fire still reads as fire; the client-only ghost fire block stays orange (no green/red fire block exists), and the soul-only sounds/block are untouched.

Relay weighting (dp-relay ≥ 0.52.0)

  • POST /<CAP>/books/vote — validate → shared-id existence (404) → same-vote dedupe → per-uuid hourly rate limit (429) → LWW upsert. Per-cap (dev votes never bleed into live), ring-bounded, metadata-only analytics fold.
  • GET /<CAP>/books/vote-summary[?type=] — grouped tallies, hottest first.
  • Author's own tallies ride /books/stats as votesUp/votesDown.
  • Selection: votes become factor = clamp(1 + 0.5·(up−down)/(up+down+K), 0.5, 1.5) (K=10), applied as an Efraimidis–Spirakis weighted sample within the operator's bookflags weight tier in /books/pool — curation tiers stay absolute, worst within-tier skew is 3:1, and the 0.5 floor keeps a downvoted book rare, never impossible. With no votes the selection path is byte-identical to before. Kill switch: BOOK_VOTES_ENABLED=false (endpoints 503, uniform selection).

Localization

Since v0.494.0 (PR #821) all 43 vote strings — the ask-prefix, 10 prompts, both thumb labels, and 30 chat responses — are localized into all 19 non-English locales, following each locale's established register and quote conventions (»…« de, «…» es/it/pt_PT/ru, « … » fr, „…” pl, 「…」 ja/zh_TW, "…" zh_CN, straight quotes elsewhere) and train vocabulary (Schaffner/Revisor/Contrôleur/Capotreno/Maquinista…). The keybinds stay Y/N in every locale (hardcoded GLFW keys), so the parenthesised (Y)/(N) hotkey markers remain literal — embedded where the local word starts with that letter (nl (N)ee, fr (N)on, pt (N)ão), prefixed otherwise (ja (Y) はい). Machine translation (Opus 4.8), unreviewed — flagged by the AI language label in the language list.

Telemetry guards

The vote page is excluded from book-read timings: BookReadClientEvents.accumulate() range-checks the current page, so vote-page dwell is dropped (not attributed to the last real page) and a Y/N jump can never fake completed=true.

Related fix shipped alongside

Gate-2 testing surfaced a pre-existing wedge: when the community pool had nothing new to offer, a pending chest-book placeholder re-fired a pool refresh every sweep and never resolved — and the early return skipped the held-marker, so the book never burned. Fixed: the exhausted-window refresh throttles to once per 10 s and falls through to the selector (repeats allowed), unresolved placeholders get normal random-book treatment (seen-tracking + held marker → they burn), and a book that resolves while already in hand is held-marked immediately.

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