Skip to content

fix(replay): re-apply server wire blanking of clanTag/friends when replaying - #4855

Merged
evanpelle merged 1 commit into
mainfrom
fix/replay-wire-blanking
Aug 3, 2026
Merged

fix(replay): re-apply server wire blanking of clanTag/friends when replaying#4855
evanpelle merged 1 commit into
mainfrom
fix/replay-wire-blanking

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

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; anonymizeNamesclanTag nulled and friends dropped. Returns the info untouched when neither flag is set.
  • src/client/Main.ts — the replay path (lobby.gameRecord.infogameStartInfo) 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

🤖 Generated with Claude Code

…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>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 56ecfe78-7a10-499d-b91f-2b8d81623314

📥 Commits

Reviewing files that changed from the base of the PR and between 9daa0c2 and c4ba9f8.

📒 Files selected for processing (4)
  • src/client/Main.ts
  • src/core/Util.ts
  • tests/ToWireGameStartInfo.test.ts
  • tests/replay/ReplayGame.ts

Walkthrough

toWireGameStartInfo now sanitizes multiplayer game-start data. Client lobby joins and replay construction use the sanitized data. Tests cover masking, passthrough behavior, preserved identity fields, and input immutability.

Changes

Game start wire sanitization

Layer / File(s) Summary
Wire-format transformation and tests
src/core/Util.ts, tests/ToWireGameStartInfo.test.ts
Adds toWireGameStartInfo. It blanks clan tags when configured and removes friends during name anonymization. Tests cover passthrough cases and immutability.
Client and replay integration
src/client/Main.ts, tests/replay/ReplayGame.ts
Client joins and replay construction convert archived game-start data before delivery. Explicit lobby data remains preferred.

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
Loading

Possibly related PRs

Suggested reviewers: celant, ryanbarlow97, variablevince

Poem

Game starts cross the wire,
Clan tags fade when rules require.
Friends stay hidden, fields stay true,
Replays share the same view.
Clean data travels through.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the replay fix and the affected clanTag and friends wire blanking behavior.
Description check ✅ Passed The description accurately explains the replay desynchronization, the wire blanking fix, affected paths, exemptions, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@evanpelle evanpelle added this to the v33 milestone Aug 3, 2026
@evanpelle
evanpelle merged commit 0668045 into main Aug 3, 2026
15 of 16 checks passed
@evanpelle
evanpelle deleted the fix/replay-wire-blanking branch August 3, 2026 21:38
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Aug 3, 2026
evanpelle added a commit that referenced this pull request Aug 3, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant