fix(replay): re-apply server wire blanking of clanTag/friends when replaying - #4855
Conversation
…playing Live clients never simulate with players' real clanTag/friends when the host sets disableClanTags or anonymizeNames: the server blanks them identically for every client (GameServer.start() wireGameStartInfo / startInfoFor) because both feed deterministic team assignment. The archived record intentionally keeps the real values for analytics, and replays rebuild GameStartInfo straight from record.info — so a team game with either setting replays with different team assignment inputs than the live game used, diverges at the first checkpoint, and fires desync errors for the whole replay. Add toWireGameStartInfo() in core, mirroring the server's blanking rules (disableClanTags -> clanTag null; anonymizeNames -> clanTag null and friends dropped), and apply it on the two replay paths: the client (Main.ts, record -> lobbyConfig.gameStartInfo) and the headless harness (tests/replay/ReplayGame.ts). Singleplayer records are exempt — those games simulate and archive without a server, so their real values ARE the simulation inputs. 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 (4)
Walkthrough
ChangesGame start wire sanitization
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant ReplayGame
participant Main.handleJoinLobby
participant toWireGameStartInfo
participant joinLobby
ReplayGame->>toWireGameStartInfo: Convert replay game-start data
toWireGameStartInfo-->>ReplayGame: Return sanitized data
Main.handleJoinLobby->>toWireGameStartInfo: Convert archived replay data
toWireGameStartInfo-->>Main.handleJoinLobby: Return sanitized data
Main.handleJoinLobby->>joinLobby: Pass explicit or sanitized start info
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 |
…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
Team games played with
disableClanTagsoranonymizeNamesdesync the moment a replay starts, with a desync error on every hash checkpoint.Live clients never simulate with players' real
clanTag/friendsunder 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 rebuildGameStartInfostraight fromrecord.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— newtoWireGameStartInfo(), mirroring the server's rules:disableClanTags→ everyclanTagnulled;anonymizeNames→clanTagnulled andfriendsdropped. 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, sonpm run replay:gameverifies 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
tests/ToWireGameStartInfo.test.ts: blanking under each flag, the identity fast-path, the singleplayer exemption, and input non-mutation.tsc --noEmitclean, lint clean; TeamAssignment/Team/ArchivePlayerRecord/ArchivedRecordSchema suites pass.Notes
friends/teamIndex. Pre-fix(server): preserve teamIndex, friends, isLobbyCreator in archived game records #4819 records remain unreplayable (fields were never stored), same as before.🤖 Generated with Claude Code