Skip to content

fix(core): reject spawn intents after the spawn phase - #4832

Open
Foulks-Plb wants to merge 1 commit into
openfrontio:mainfrom
Foulks-Plb:fix/spawn-after-spawn-phase
Open

fix(core): reject spawn intents after the spawn phase#4832
Foulks-Plb wants to merge 1 commit into
openfrontio:mainfrom
Foulks-Plb:fix/spawn-after-spawn-phase

Conversation

@Foulks-Plb

Copy link
Copy Markdown

Problem

The spawn phase is only enforced on the client. ClientGameRunner shows a
spawn_failed modal and drops the click once the phase ends, but that guard is
UX 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 inSpawnPhase notion), and the simulation's SpawnExecution only
blocks re-spawns by already-spawned players:

if (!this.mg.inSpawnPhase() && player.hasSpawned()) return;

A player who never spawned during the phase has hasSpawned() === false, so the
intent 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. Once inSpawnPhase() is false a spawn
intent becomes a NoOpExecution:

case "spawn":
  if (!this.mg.inSpawnPhase()) {
    return new NoOpExecution();
  }
  return new SpawnExecution(this.gameID, player.info(), intent.tile);

This runs identically on every client, so the drop is deterministic (no desync).
Internal spawns — nations, bots, random spawn — construct SpawnExecution
directly 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.ts asserts a spawn intent relayed after the
phase is dropped (no territory placed). In-phase spawning stays covered by the
existing SpawnExecution / TribeSpawner suites.

Full test suite shows no new failures vs. main.

🤖 Generated with Claude Code

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>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Aug 2, 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: f18bd69e-a9e4-42a3-a9b4-e70232055316

📥 Commits

Reviewing files that changed from the base of the PR and between 753c66e and 33cd62b.

📒 Files selected for processing (2)
  • src/core/execution/ExecutionManager.ts
  • tests/SpawnAfterPhase.test.ts

Walkthrough

Spawn intents now create NoOpExecution after the spawn phase. Spawn-phase intents keep the existing SpawnExecution flow. A test verifies that late players remain unspawned and own no tiles.

Changes

Spawn intent phase validation

Layer / File(s) Summary
Guard late spawn intents
src/core/execution/ExecutionManager.ts, tests/SpawnAfterPhase.test.ts
The spawn intent handler returns NoOpExecution outside the spawn phase. The test verifies that a late player remains unspawned and owns no tiles.

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

Possibly related PRs

Suggested labels: Bugfix

Suggested reviewers: evanpelle

Poem

After spawn, the gate is closed,
A no-op waits where troops once posed.
No tiles change, no claims are made,
Late arrivals stay unplayed.
The phase guard keeps the map in shade.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes rejecting spawn intents after the spawn phase.
Description check ✅ Passed The description explains the post-phase spawn issue, the fix, and the added test, all matching the changeset.
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.

@github-actions github-actions Bot added the small-fix Small fix (≤ 50 lines) — auto-applied by PR gate label Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

small-fix Small fix (≤ 50 lines) — auto-applied by PR gate

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

2 participants