Feature Book Voting - bh679/dungeon-train-mc GitHub Wiki
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.
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.pngbounds) 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 onInit.Post; everything on the page draws inRender.Postwith manual hit-testing (BookVoteClientEvents). Re-built on window resize. Lecterns excluded.
Clicking a thumb or pressing Y / N from any page commits instantly:
-
BookVotePacket(C2S,PROTOCOL_VERSION 47) — the server re-resolves the held main/off-hand stack viaBookIdentityand drops mismatches (thrown/swapped/spoofed). - 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). - 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).
- Consent-gated (
NetworkConsentMirror, fail-closed):BookVoteReporterenqueues{uuid, player, bookType, bookId, vote, clientTsMs}to the durableRelayOutbox→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.
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.
-
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/statsasvotesUp/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).
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.
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.
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.