Skip to content

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

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

fix(core): reject spawn intents after the spawn phase#4831
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:

  • a spawn intent relayed after the phase is dropped (no territory placed)
  • a spawn intent during the phase still spawns normally

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 covering both the blocked post-phase intent
and the still-allowed in-phase intent.

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: b685e764-3f27-4146-b327-d4f14fe80738

📥 Commits

Reviewing files that changed from the base of the PR and between 753c66e and 7d82dc2.

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

Walkthrough

The execution manager now converts out-of-phase client spawn intents into NoOpExecution. Regression tests cover both rejected late spawns and valid in-phase spawns.

Changes

Spawn phase enforcement

Layer / File(s) Summary
Spawn guard and regression coverage
src/core/execution/ExecutionManager.ts, tests/SpawnAfterPhase.test.ts
Executor.createExec returns NoOpExecution for out-of-phase client spawn intents. Tests verify that valid in-phase intents create SpawnExecution and grant tiles.

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

Possibly related PRs

Suggested labels: Bugfix

Suggested reviewers: evanpelle

Poem

Spawn waits for its proper phase,
Late intents fade without a trace.
Valid turns claim tiles anew,
Tests confirm the path stays true.

🚥 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 related regression 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.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@github-actions github-actions Bot added the auto-closed-needs-issue PR closed by gate — see comment for next steps label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Hi @Foulks-Plb, thanks for the contribution.

This PR was automatically closed because it doesn't fit our contribution workflow:

  • You aren't currently assigned to an issue labelled approved, and
  • The change is larger than 50 lines (our cap for unsolicited contributions).

To contribute to OpenFront:

  1. For bugs or small quality-of-life improvements: open an issue. A maintainer will label it approved if it's something we'll work on.
  2. For feature ideas: discuss in the dev Discord first. We don't accept unsolicited feature PRs — even if they're good ideas, every merged feature is a permanent maintenance burden.
  3. Once an issue is labelled approved, comment asking to be assigned. After you're assigned, you can open a PR referencing that issue.

If you believe this was closed in error, please reach out on our Discord or comment below.

See CONTRIBUTING.md for the full contribution process.

Automated PR gate. Source.

@github-actions github-actions Bot closed this Aug 2, 2026
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-closed-needs-issue PR closed by gate — see comment for next steps

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

2 participants