fix(core): reject spawn intents after the spawn phase - #4832
Conversation
A modified client could relay a `{ type: "spawn", tile }` intent after the
spawn phase ended and materialize a base on the running map (or teleport an
existing one by relinquishing and re-conquering elsewhere). The client-side
guard in ClientGameRunner is UX only — a modified client skips it — so the
simulation accepted the intent on every client.
Gate the spawn intent where it becomes an execution: Executor.createExec now
drops a spawn intent to a NoOpExecution once inSpawnPhase() is false. This is
the single authoritative choke point for untrusted client input and runs
identically on every client, so the drop is deterministic. Internal spawns
(nations, bots, random spawn) construct SpawnExecution directly and are
unaffected, as is a human's last-tick click, whose intent is still created
while the phase is active.
Adds tests/SpawnAfterPhase.test.ts asserting a post-phase spawn intent is
dropped instead of placing territory.
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 (2)
WalkthroughSpawn intents now create ChangesSpawn intent phase validation
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: 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 |
Problem
The spawn phase is only enforced on the client.
ClientGameRunnershows aspawn_failedmodal and drops the click once the phase ends, but that guard isUX only — a modified client skips it and relays a
{ type: "spawn", tile }intent after the phase. The server relays spawn intents without a phase check
(it has no
inSpawnPhasenotion), and the simulation'sSpawnExecutiononlyblocks re-spawns by already-spawned players:
A player who never spawned during the phase has
hasSpawned() === false, so theintent falls through and places starting territory on the already-running map.
The same path can relocate an existing base (relinquish + re-conquer elsewhere)
after the map has developed.
Fix
Gate the intent at the single choke point where untrusted client input becomes
an execution —
Executor.createExec. OnceinSpawnPhase()is false aspawnintent becomes a
NoOpExecution:This runs identically on every client, so the drop is deterministic (no desync).
Internal spawns — nations, bots, random spawn — construct
SpawnExecutiondirectly and are unaffected, and a human's last-tick click still works because
its intent is created while the phase is still active.
Tests
tests/SpawnAfterPhase.test.tsasserts a spawn intent relayed after thephase is dropped (no territory placed). In-phase spawning stays covered by the
existing
SpawnExecution/TribeSpawnersuites.Full test suite shows no new failures vs.
main.🤖 Generated with Claude Code