Skip to content

fix(coop): serve the client on a mid-battle campaign resume - #120

Merged
NonPolynomialTim merged 3 commits into
mainfrom
claude/campaign-load-battle-phase-946745
Aug 1, 2026
Merged

fix(coop): serve the client on a mid-battle campaign resume#120
NonPolynomialTim merged 3 commits into
mainfrom
claude/campaign-load-battle-phase-946745

Conversation

@NonPolynomialTim

Copy link
Copy Markdown
Collaborator

Problem

Loading a mid-battle campaign save, having the second player rejoin, and pressing RESUME CAMPAIGN left the client stranded on the lobby while the host dropped into the battle alone. Reported by a player; reproduced live. SEPARATE and SHARED both affected; geoscape (non-battle) resume was fine.

Root cause

LobbyMenu's ctor sets _resumeToGame = battleRunning || (geoRunning && sessionLocked). Loading a mid-battle save makes LoadGameState build [GeoscapeState, BattlescapeState] before the session is live, so battleRunning is true in the resume lobby — the battleRunning term had no sessionLocked guard (the comment assumed a battle can't exist before a session starts, which a save-load violates). RESUME then ran returnToRunningGame() — popping the host into its own battle and serving nobody — instead of resumeCampaign(). Geoscape resume escaped because its term requires sessionLocked.

Fix

Gate _resumeToGame on lobbyMode != 2. lobbyMode == 2 is set only by CoopSession::adoptResumeSave() — the resume-from-save lobby, which must always serve the client via resumeCampaign(). The in-game coop menu (playtest B7) and a mid-battle reconnect are never mode 2, so they still return straight to the running game.

Tests

The existing test_coop_resume_battle_control / test_shared_resume_battle_control missed this because their helper pressed RESUME via the lobby_resume_campaign command, which calls LobbyMenu::resumeCampaign() directly and bypasses the _resumeToGame branch where the bug lives. Changed session.resume_campaign_battle to press the real button (lobby_actionbtnCancelClick). Both tests now reproduce the bug without the fix and pass with it.

Validated RED → GREEN:

🤖 Generated with Claude Code

NonPolynomialTim and others added 3 commits August 1, 2026 12:20
Loading a mid-battle campaign save leaves a BattlescapeState on the stack
(the host's own battle) before the session is live, so the resume lobby's
ctor set _resumeToGame=true. RESUME then ran returnToRunningGame() - popping
the host into its own battle and serving nobody - instead of resumeCampaign().
The second player was stranded on the lobby while the host played on alone.

The battleRunning term had no sessionLocked guard (the geoscape term does),
on the assumption that a battle cannot exist before a session starts - which
is false when a mid-battle save is loaded. Gate _resumeToGame on lobbyMode!=2:
a resume-from-save lobby (the only mode-2 lobby, set by adoptResumeSave) must
always serve the client. The in-game coop menu and a mid-battle reconnect are
never mode 2, so they still return straight to the running game.

The existing resume-battle tests missed this because session.resume_campaign_battle
pressed RESUME via the lobby_resume_campaign command, which calls
LobbyMenu::resumeCampaign() directly and bypasses the _resumeToGame branch.
Press the real button (lobby_action -> btnCancelClick) instead, so
test_coop_resume_battle_control and test_shared_resume_battle_control both
reproduce the bug without the fix and pass with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mid-battle resume bug hid from the suite because the resume helper pressed
RESUME via the lobby_resume_campaign command, which calls
LobbyMenu::resumeCampaign() directly and bypasses btnCancelClick (where the
_resumeToGame branch lives). The same bypass existed for campaign start
(lobby_start_campaign -> startCampaign()) and geoscape resume, so any
btnCancelClick-level bug on those paths would hide too.

Audit of every TestServer command: the only ones that call a leaf method below
the real button were resumeCampaign() and startCampaign(); all others click the
real btn*Click, or use a harness* wrapper that itself calls btnOkClick/viewClick
(the recruit-stores-full bug proves those hit the real validation). Route
session.new_campaign and session.resume_campaign through the real button
(lobby_action -> btnCancelClick; start goes via ConfirmStartCampaignState +
lobby_confirm_ok). Validated end to end: SEPARATE + SHARED start, geoscape
resume, and mid-battle resume all still pass through the real UI path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the harness-fidelity work: no test should reach a lobby action by a
path a player cannot take. Added session.start_campaign_via_button (lobby_action
-> ConfirmStartCampaignState -> lobby_confirm_ok) and
session.resume_campaign_via_button (lobby_action after the host sees the peer),
and routed new_campaign / resume_campaign / resume_campaign_battle plus every
inline caller through them:

- test_lobby_gating: lone-host rejection now presses the real START button and
  asserts it no-ops (no confirm dialog, no base placement, still in the lobby);
  the confirm-dialog case opens via btnCancelClick, not openStartConfirmDialog.
- test_lobby_dialogs, test_reconnect_dialog, scenario_dialog_shot: start via the
  real button.
- test_resume_flow, test_coop_basedef_temp_ufo_uaf: resume via the real button.

The lobby_start_campaign / lobby_resume_campaign commands remain in TestServer
(unused by tests now) - harmless, and kept so a future gate-rejection test could
assert on their error path directly. Validated GREEN: test_lobby_gating,
test_lobby_dialogs, test_resume_flow, test_reconnect_dialog (plus the earlier
SEPARATE/SHARED start + mid-battle resume).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NonPolynomialTim
NonPolynomialTim enabled auto-merge (squash) August 1, 2026 16:57
@NonPolynomialTim
NonPolynomialTim merged commit 57cdf06 into main Aug 1, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant