Skip to content

fix(tribes): carry custom tribe names into the archived game record - #4854

Merged
evanpelle merged 1 commit into
mainfrom
fix/tribes-in-archived-record
Aug 3, 2026
Merged

fix(tribes): carry custom tribe names into the archived game record#4854
evanpelle merged 1 commit into
mainfrom
fix/tribes-in-archived-record

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

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

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>
@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: d224221f-99f6-4fce-8113-d848d29b0b2b

📥 Commits

Reviewing files that changed from the base of the PR and between 7a7ca5b and adc1291.

📒 Files selected for processing (3)
  • src/core/Util.ts
  • src/server/GameServer.ts
  • tests/server/ArchivePlayerRecord.test.ts

Walkthrough

The change preserves selected tribe data when games are archived. The record builder accepts optional tribes, the server passes gameStartInfo.tribes, and tests verify populated and absent data.

Changes

Archived tribe metadata

Layer / File(s) Summary
Add tribe metadata to game records
src/core/Util.ts
createPartialGameRecord accepts optional Tribe[] data and writes it to record metadata.
Pass and validate archived tribe data
src/server/GameServer.ts, tests/server/ArchivePlayerRecord.test.ts
archiveGame passes selected tribes. Tests cover custom tribe names and missing tribe data.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: ryanbarlow97, flopinguin

Poem

Tribes travel with the game,
Names remain inside the frame.
Archives keep the chosen lore,
Empty data stays out the door.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix that carries custom tribe names into archived game records.
Description check ✅ Passed The description clearly explains the root cause, fix, affected paths, regression tests, and deployment verification.
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 9daa0c2 into main Aug 3, 2026
15 of 16 checks passed
@evanpelle
evanpelle deleted the fix/tribes-in-archived-record branch August 3, 2026 20:44
@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
…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>
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>
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