fix(tribes): carry custom tribe names into the archived game record - #4854
Conversation
GameServer fetches purchased bot tribe names at prestart and embeds them in the game start info, but archiveGame() never passed them to createPartialGameRecord, so the analytics record infra ingests for the tribe leaderboard (info.tribes) was always missing — and replays, which rebuild GameStartInfo from the record, spawned organic names instead of the purchased ones. Verified against prod: records stamped with the v0.33.0 commit still have no info.tribes key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
WalkthroughThe change preserves selected tribe data when games are archived. The record builder accepts optional tribes, the server passes ChangesArchived tribe metadata
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…4854) ## Problem The tribes leaderboard (`GET /leaderboard/tribes`) is empty. The original diagnosis was that prod servers predated the custom-tribes integration (#4727) — but after today's v0.33.0 deploy, fresh game records stamped with the v0.33.0 commit (`7a7ca5be`) **still** have no `info.tribes` key (checked `3DM5jo9P`, `TKDVms7Y`, `6e6gvscX` via the public game API). ## Root cause `GameServer.fetchTribes()` fetches the purchased-name pool at prestart and `start()` embeds it in the game start info sent to clients — but `archiveGame()` never passed it to `createPartialGameRecord`, which builds the record `info` from an explicit field list. So the tribes were dropped from the analytics record, and infra's ingest (`maybeSaveTribeNameStats`) silently no-ops on the missing field. `custom_tribe_name_stats_daily` has never been written. This also affects replays: they rebuild `GameStartInfo` from `record.info` (`GameEndInfoSchema` extends `GameStartInfoSchema`, so `tribes` is already part of the record schema), meaning replays currently spawn organic bot names instead of the purchased ones the live game showed. ## Fix - `createPartialGameRecord` takes an optional `tribes` param and includes it in `info`. - `archiveGame()` passes `this.gameStartInfo.tribes`. - Client callers (singleplayer/local archive paths) are unchanged — those games never have tribes and are skipped by ingest anyway. ## Tests - New regression tests in `ArchivePlayerRecord.test.ts`: tribes survive archiving; absent tribes stay absent. - Full suite passes (31 files, 291 tests), `tsc --noEmit` clean. ## Post-deploy verification - `curl -s "https://api.openfront.io/public/game/<id>?turns=false" | jq '.info.tribes'` on a finished public game with bots should return the name array. - `custom_tribe_name_stats_daily` should start accruing rows; `/leaderboard/tribes` populates within the 1-hour cache window. Note: this needs to ride a v0.33.x hotfix release to reach prod. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…playing (#4855) ## Problem Team games played with `disableClanTags` or `anonymizeNames` desync the moment a replay starts, with a desync error on every hash checkpoint. Live clients never simulate with players' real `clanTag`/`friends` under those settings: the server blanks them identically for every client (`GameServer.start()` `wireGameStartInfo` / `startInfoFor()`), because both fields feed deterministic team assignment (`TeamAssignment.ts`). The archived record intentionally keeps the real values (analytics ingest reads them), and replays rebuild `GameStartInfo` straight from `record.info` — so the replay derives a different team split than every live client did, diverges at the first cross-team interaction, and mismatches every recorded hash from then on. (FFA is unaffected — those modes never run team assignment, which is why public FFA records replay fine despite `disableClanTags: true`.) ## Fix Re-apply the server's blanking at replay time instead of stripping the archive: - **`src/core/Util.ts`** — new `toWireGameStartInfo()`, mirroring the server's rules: `disableClanTags` → every `clanTag` nulled; `anonymizeNames` → `clanTag` nulled and `friends` dropped. Returns the info untouched when neither flag is set. - **`src/client/Main.ts`** — the replay path (`lobby.gameRecord.info` → `gameStartInfo`) passes through the helper. Live games are unaffected: their start info arrives from the server already blanked, so the transform is a no-op there by construction. - **`tests/replay/ReplayGame.ts`** — the headless harness applies the same transform, so `npm run replay:game` verifies records exactly the way the client replays them. **Singleplayer records are exempt**: those games simulate and archive without a server, so their real values *are* the simulation inputs — blanking them would introduce a new desync. ## Tests - New `tests/ToWireGameStartInfo.test.ts`: blanking under each flag, the identity fast-path, the singleplayer exemption, and input non-mutation. - `tsc --noEmit` clean, lint clean; TeamAssignment/Team/ArchivePlayerRecord/ArchivedRecordSchema suites pass. ## Notes - Applies to any record that archived the real values — i.e. everything since #4819 preserved `friends`/`teamIndex`. Pre-#4819 records remain unreplayable (fields were never stored), same as before. - Sibling of #4819 (teamIndex) and #4854 (tribes): third case of a live-simulation input not surviving the record → replay round trip. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…playing (#4855) ## Problem Team games played with `disableClanTags` or `anonymizeNames` desync the moment a replay starts, with a desync error on every hash checkpoint. Live clients never simulate with players' real `clanTag`/`friends` under those settings: the server blanks them identically for every client (`GameServer.start()` `wireGameStartInfo` / `startInfoFor()`), because both fields feed deterministic team assignment (`TeamAssignment.ts`). The archived record intentionally keeps the real values (analytics ingest reads them), and replays rebuild `GameStartInfo` straight from `record.info` — so the replay derives a different team split than every live client did, diverges at the first cross-team interaction, and mismatches every recorded hash from then on. (FFA is unaffected — those modes never run team assignment, which is why public FFA records replay fine despite `disableClanTags: true`.) ## Fix Re-apply the server's blanking at replay time instead of stripping the archive: - **`src/core/Util.ts`** — new `toWireGameStartInfo()`, mirroring the server's rules: `disableClanTags` → every `clanTag` nulled; `anonymizeNames` → `clanTag` nulled and `friends` dropped. Returns the info untouched when neither flag is set. - **`src/client/Main.ts`** — the replay path (`lobby.gameRecord.info` → `gameStartInfo`) passes through the helper. Live games are unaffected: their start info arrives from the server already blanked, so the transform is a no-op there by construction. - **`tests/replay/ReplayGame.ts`** — the headless harness applies the same transform, so `npm run replay:game` verifies records exactly the way the client replays them. **Singleplayer records are exempt**: those games simulate and archive without a server, so their real values *are* the simulation inputs — blanking them would introduce a new desync. ## Tests - New `tests/ToWireGameStartInfo.test.ts`: blanking under each flag, the identity fast-path, the singleplayer exemption, and input non-mutation. - `tsc --noEmit` clean, lint clean; TeamAssignment/Team/ArchivePlayerRecord/ArchivedRecordSchema suites pass. ## Notes - Applies to any record that archived the real values — i.e. everything since #4819 preserved `friends`/`teamIndex`. Pre-#4819 records remain unreplayable (fields were never stored), same as before. - Sibling of #4819 (teamIndex) and #4854 (tribes): third case of a live-simulation input not surviving the record → replay round trip. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Problem
The tribes leaderboard (
GET /leaderboard/tribes) is empty. The original diagnosis was that prod servers predated the custom-tribes integration (#4727) — but after today's v0.33.0 deploy, fresh game records stamped with the v0.33.0 commit (7a7ca5be) still have noinfo.tribeskey (checked3DM5jo9P,TKDVms7Y,6e6gvscXvia the public game API).Root cause
GameServer.fetchTribes()fetches the purchased-name pool at prestart andstart()embeds it in the game start info sent to clients — butarchiveGame()never passed it tocreatePartialGameRecord, which builds the recordinfofrom an explicit field list. So the tribes were dropped from the analytics record, and infra's ingest (maybeSaveTribeNameStats) silently no-ops on the missing field.custom_tribe_name_stats_dailyhas never been written.This also affects replays: they rebuild
GameStartInfofromrecord.info(GameEndInfoSchemaextendsGameStartInfoSchema, sotribesis already part of the record schema), meaning replays currently spawn organic bot names instead of the purchased ones the live game showed.Fix
createPartialGameRecordtakes an optionaltribesparam and includes it ininfo.archiveGame()passesthis.gameStartInfo.tribes.Tests
ArchivePlayerRecord.test.ts: tribes survive archiving; absent tribes stay absent.tsc --noEmitclean.Post-deploy verification
curl -s "https://api.openfront.io/public/game/<id>?turns=false" | jq '.info.tribes'on a finished public game with bots should return the name array.custom_tribe_name_stats_dailyshould start accruing rows;/leaderboard/tribespopulates within the 1-hour cache window.Note: this needs to ride a v0.33.x hotfix release to reach prod.
🤖 Generated with Claude Code