Skip to content

Conversation

@YoussfeCantCode
Copy link
Contributor

@YoussfeCantCode YoussfeCantCode commented Dec 20, 2025

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #2491

Description:

Adds pause/unpause functionality for private multiplayer games. Only the lobby creator can pause the game, and all players see a pause overlay when the game is paused.

Key features:

  • Lobby creator sees pause/play button in control panel (alongside existing singleplayer/replay controls)
  • Server validates that only lobby creator can toggle pause
  • All players see "Game paused by Lobby Creator" overlay when paused
  • Game state freezes (no turn execution) while paused
  • Unpause resumes normal gameplay

Implementation details:

  • Server-side pause state (isPaused) prevents turn execution during pause
  • Each client receives isLobbyCreator flag in GameStartInfo to show/hide pause button
  • Added TogglePauseIntent that broadcasts to all clients via NoOpExecution
  • New PauseOverlay component (shows in single player also)

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

furo18

Screenshot 2025-12-20 at 15 16 33

- Only lobby creator can pause/unpause the game
- Show pause overlay when game is paused
- Pause state communicated via worker messages
- Server validates lobby creator before accepting pause intent
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

Adds a server-controlled pause flow: a new toggle_pause intent, execution handler, game paused state and update, client UI emitting pause-intent events, lobby-creator metadata propagation, and localized pause messages.

Changes

Cohort / File(s) Summary
Translations & i18n
resources/lang/en.json
Added heads_up_message.pause.singleplayer_game_paused and heads_up_message.pause.multiplayer_game_paused.
Intent Schemas & Types
src/core/Schemas.ts
Added TogglePauseIntentSchema / TogglePauseIntent; included in Intent union; added isLobbyCreator?: boolean to PlayerSchema.
Execution & Engine
src/core/execution/PauseExecution.ts, src/core/execution/ExecutionManager.ts
New PauseExecution class; ExecutionManager now returns PauseExecution for toggle_pause intents.
Game State & Models
src/core/game/Game.ts, src/core/game/GameImpl.ts, src/core/game/GameUpdates.ts, src/core/game/GameView.ts, src/core/game/PlayerImpl.ts
Game gains isPaused state, setPaused/isPaused API and GamePaused update; players now carry isLobbyCreator flag and expose isLobbyCreator(); player updates include isLobbyCreator.
Server Behavior
src/server/GameServer.ts
Tracks isPaused, enforces lobby-creator-only toggle handling, annotates start info with isLobbyCreator, blocks turn advancement while paused, and processes toggle_pause intents to pause/resume.
Client Transport & Local Server
src/client/Transport.ts, src/client/LocalServer.ts
Renamed/exported event PauseGameEventPauseGameIntentEvent; Transport sends toggle_pause intents to server; LocalServer removed pause()/resume() and handles toggle_pause intents locally for local games.
UI: Controls & Messaging
src/client/graphics/layers/GameRightSidebar.ts, src/client/graphics/layers/SettingsModal.ts, src/client/graphics/layers/HeadsUpMessage.ts
UI emits PauseGameIntentEvent; pause button visibility restricted to lobby creator in multiplayer; HeadsUpMessage listens for GamePaused updates and selects proper localized message.
Runner & Init
src/core/GameRunner.ts, src/client/ClientGameRunner.ts
Player initialization now receives isLobbyCreator flag; ClientGameRunner filters toggle_pause intents during replay mode.

Sequence Diagram(s)

sequenceDiagram
    actor LobbyCreator as Lobby Creator
    participant UI as GameRightSidebar / SettingsModal
    participant Transport as Client.Transport
    participant Server as GameServer
    participant Executor as ExecutionManager
    participant Game as GameImpl
    participant UpdateBus as Update Stream / Clients

    rect rgb(220,240,255)
    note over LobbyCreator,UI: User initiates pause
    LobbyCreator->>UI: Click pause button
    UI->>Transport: emit PauseGameIntentEvent(paused: true)
    Transport->>Server: sendIntent(type: "toggle_pause", paused: true)
    end

    rect rgb(240,220,255)
    note over Server: Server validates and applies intent
    Server->>Server: isLobbyCreator(clientID)?
    alt is lobby creator
        Server->>Server: Add intent, end current turn, set isPaused = true
        Server->>Executor: Deliver toggle_pause intent
        Executor->>Executor: Create PauseExecution(player, paused:true)
        Executor->>Game: PauseExecution.init -> setPaused(true)
        Game->>UpdateBus: Emit GamePausedUpdate(paused: true)
    else not lobby creator
        Server->>Server: Log and ignore intent
    end
    end

    rect rgb(240,255,220)
    note over UpdateBus,UI: Clients receive pause update
    UpdateBus->>UI: GamePausedUpdate(paused: true)
    UI->>UI: HeadsUpMessage shows localized pause text
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Feature - Backend, Feature - Frontend, UI/UX

Suggested reviewers

  • evanpelle

Poem

🎮 The lobby leader taps — a hush across the board,
The server catches breath and freezes time adored.
Players wait, then read the message calm and clear,
A pause for mending matches — fairness drawing near. ⏸️

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main change: adding pause functionality for private multiplayer games, which matches the core objective of PR #2491.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the pause/unpause feature, key design decisions, implementation details, and linking to issue #2491.
Linked Issues check ✅ Passed The PR implementation successfully addresses all requirements from issue #2491: pause control restricted to lobby creator, game state frozen during pause, clear indicators displayed to players, and distinct messages for singleplayer vs multiplayer modes.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing pause functionality and supporting infrastructure; no unrelated modifications detected outside the requirements of issue #2491.

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/server/GameServer.ts (1)

351-381: Authorization check is correct, but consider adding clarity comment.

The authorization check properly restricts pause toggling to the lobby creator. The pause/unpause logic is correct but subtle: when pausing, endTurn() executes before setting isPaused so the current turn completes; when unpausing, isPaused is cleared before endTurn() so the next turn can execute.

📝 Optional: Add comment to explain the order
 case "toggle_pause": {
   // Only lobby creator can pause/resume
   if (client.clientID !== this.lobbyCreatorID) {
     this.log.warn(`Only lobby creator can toggle pause`, {
       clientID: client.clientID,
       creatorID: this.lobbyCreatorID,
       gameID: this.id,
     });
     return;
   }

   const paused = !!clientMsg.intent.paused;

   if (paused) {
-    // Pausing: send intent while game is still running, then pause
+    // Pausing: send intent and complete current turn before pause takes effect
     this.addIntent(clientMsg.intent);
     this.endTurn();
     this.isPaused = true;
   } else {
-    // Unpausing: unpause first, then send intent
+    // Unpausing: clear pause flag before sending intent so next turn can execute
     this.isPaused = false;
     this.addIntent(clientMsg.intent);
     this.endTurn();
   }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e554ffb and 1e2f877.

📒 Files selected for processing (14)
  • resources/lang/en.json (1 hunks)
  • src/client/ClientGameRunner.ts (3 hunks)
  • src/client/Transport.ts (4 hunks)
  • src/client/graphics/GameRenderer.ts (5 hunks)
  • src/client/graphics/layers/GameRightSidebar.ts (4 hunks)
  • src/client/graphics/layers/PauseOverlay.ts (1 hunks)
  • src/client/graphics/layers/SettingsModal.ts (2 hunks)
  • src/client/index.html (1 hunks)
  • src/core/Schemas.ts (6 hunks)
  • src/core/execution/ExecutionManager.ts (1 hunks)
  • src/core/worker/Worker.worker.ts (1 hunks)
  • src/core/worker/WorkerClient.ts (3 hunks)
  • src/core/worker/WorkerMessages.ts (3 hunks)
  • src/server/GameServer.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/graphics/layers/ControlPanel.ts:160-183
Timestamp: 2025-06-07T20:01:52.720Z
Learning: The user its-sii prefers simpler, more readable code over complex type-safe approaches when the benefits don't justify the added complexity. They value maintainability and clarity in code implementation.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/PauseOverlay.ts
  • src/client/ClientGameRunner.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/PauseOverlay.ts
  • src/client/ClientGameRunner.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/core/Schemas.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/core/Schemas.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/ClientGameRunner.ts
🧬 Code graph analysis (6)
src/client/graphics/layers/PauseOverlay.ts (3)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/EventBus.ts (1)
  • EventBus (7-44)
src/client/Transport.ts (1)
  • GamePausedEvent (36-38)
src/core/execution/ExecutionManager.ts (1)
src/core/execution/NoOpExecution.ts (1)
  • NoOpExecution (3-12)
src/client/graphics/layers/SettingsModal.ts (2)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (32-34)
src/client/LocalServer.ts (1)
  • pause (100-102)
src/client/ClientGameRunner.ts (2)
src/client/graphics/GameRenderer.ts (1)
  • createRenderer (46-302)
src/client/Transport.ts (1)
  • GamePausedEvent (36-38)
src/client/graphics/layers/GameRightSidebar.ts (1)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (32-34)
src/client/Transport.ts (1)
src/core/EventBus.ts (1)
  • GameEvent (1-1)
🔇 Additional comments (19)
src/core/execution/ExecutionManager.ts (1)

126-128: LGTM - Clean server-side pause handling.

The toggle_pause case correctly returns a NoOpExecution since pause state is managed server-side. The comment clearly explains the behavior.

resources/lang/en.json (1)

743-745: LGTM - Clear pause message translation.

The new pause section with the "game_paused" key is well-structured and the message clearly indicates who paused the game.

src/core/Schemas.ts (2)

360-363: Clarify the default value for paused field.

The paused field has .default(false), which means if a toggle_pause intent is sent without the paused field, it will default to false (unpause). Is this the intended behavior? Typically, toggle intents would require the field to be explicitly set.

Consider whether the paused field should be required instead:

export const TogglePauseIntentSchema = BaseIntentSchema.extend({
  type: z.literal("toggle_pause"),
-  paused: z.boolean().default(false),
+  paused: z.boolean(),
});

This would ensure clients always explicitly specify whether they're pausing or unpausing.


449-449: LGTM - Optional isLobbyCreator field is appropriate.

Making isLobbyCreator optional maintains backward compatibility and only the lobby creator client needs this flag set.

src/client/index.html (1)

356-356: LGTM - Pause overlay element added.

The pause-overlay element is appropriately positioned in the DOM structure alongside other game overlay components.

src/core/worker/Worker.worker.ts (1)

71-80: LGTM - Clean pause state propagation.

The worker correctly detects toggle_pause intents in processed turns and notifies the main thread of the pause state change. The type guard ("paused" in pauseIntent) is good defensive programming.

src/client/Transport.ts (2)

32-38: LGTM - Clean event separation.

Good design to separate PauseGameIntentEvent (user action) from GamePausedEvent (state notification). This makes the event flow clear and prevents confusion.


583-601: Verify multiplayer pause feedback flow.

In multiplayer mode (lines 594-600), the client sends a toggle_pause intent but doesn't emit GamePausedEvent locally. This means the UI won't update until the server processes the intent and broadcasts it back through the turn system.

Verify that:

  1. The server broadcasts the pause state change to all clients (including the sender) via the turn intents
  2. The worker detects this and triggers GamePausedEvent (as seen in Worker.worker.ts lines 71-80)
  3. There's acceptable UI feedback delay for the lobby creator who clicked pause

If the delay is noticeable, consider emitting an optimistic GamePausedEvent immediately for the sender, then reconciling with server state.

src/client/ClientGameRunner.ts (2)

198-203: LGTM - isLobbyCreator flag properly propagated.

The isLobbyCreator flag is correctly extracted from gameStartInfo with a sensible default of false, and passed to the renderer for UI control decisions.


338-340: LGTM - Pause state change handler wired correctly.

The worker's pause state changes are properly bridged to the event bus via GamePausedEvent, allowing UI components to react to pause state changes.

src/client/graphics/layers/SettingsModal.ts (1)

17-17: LGTM - Updated to use PauseGameIntentEvent.

The modal correctly uses PauseGameIntentEvent to signal pause intent, aligning with the new event model introduced in Transport.ts.

Also applies to: 110-110

src/core/worker/WorkerMessages.ts (1)

17-17: LGTM! Clean pause state message definition.

The new PauseStateMessage interface is simple and correctly typed. Including it in WorkerMessage (worker-to-main direction) aligns with the expected message flow where the worker notifies the main thread of pause state changes.

Also applies to: 61-64, 136-136

src/client/graphics/GameRenderer.ts (1)

46-51: LGTM! Pause overlay integration follows established patterns.

The isLobbyCreator parameter propagates correctly from createRenderer to the right sidebar. The PauseOverlay setup follows the same DOM query and initialization pattern used for other layers, and placing it near the end of the layers array (line 287) ensures it renders on top as overlays should.

Also applies to: 159-159, 240-244, 287-287

src/core/worker/WorkerClient.ts (1)

21-21: LGTM! Callback pattern is consistent and simple.

The pauseStateCallback follows the same pattern as the existing gameUpdateCallback, maintaining consistency. The message handler correctly invokes the callback when pause_state messages arrive, and the registration method provides a clean API for consumers.

Also applies to: 47-51, 99-101

src/server/GameServer.ts (3)

65-65: LGTM! Simple pause state flag.

The isPaused field is initialized to false as expected for a new game.


523-558: LGTM! Defensive client lookup and correct isLobbyCreator propagation.

The websocket-to-client lookup includes a defensive guard that logs a warning and returns early if the client isn't found. All call sites provide valid websockets, so this guard handles unexpected edge cases gracefully. The isLobbyCreator flag is correctly determined and included in the start message for each client.


560-564: LGTM! Efficient pause guard.

The early return when isPaused is true prevents turn execution and saves unnecessary work. Placing the check before creating the turn object avoids allocations while paused.

src/client/graphics/layers/GameRightSidebar.ts (1)

12-12: LGTM! Clearer naming and improved conditional logic.

The refactoring makes the button rendering logic more readable. Using isReplayOrSingleplayer and showPauseButton as intermediate variables clarifies the conditions. Renaming to PauseGameIntentEvent better expresses that this is an intent to pause (user action) rather than a notification that pause occurred.

Also applies to: 22-22, 99-99, 153-184

src/client/graphics/layers/PauseOverlay.ts (1)

1-47: LGTM! Simple, focused pause overlay component.

The PauseOverlay component is straightforward and does one thing well. It listens for GamePausedEvent, updates state, and renders a full-screen overlay with a centered message. The use of translateText("pause.game_paused") assumes the translation key exists (mentioned in the PR description). Avoiding shadow DOM by overriding createRenderRoot() is consistent with other UI components in this codebase.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 20, 2025
@FloPinguin
Copy link
Contributor

Really great PR and good description!

But blurring the screen on pause is probably not good? You want to be able to check out the state of the game while waiting for it to continue.

Also I think we don't need the PauseOverlay in singleplayer. The player knows that he just stopped the game, we don't need to inform about that.

@YoussfeCantCode
Copy link
Contributor Author

@FloPinguin Thank you, really appreciate it!

But blurring the screen on pause is probably not good? You want to be able to check out the state of the game while waiting for it to continue.

Yes, makes sense to me, I'll remove it

Also I think we don't need the PauseOverlay in singleplayer. The player knows that he just stopped the game, we don't need to inform about that.

I was worrying about game consistency and wanted to make sure the same behaviour shows in all game modes. That said, I do see your point as well. I’m not entirely sure what the best approach is here.

@iiamlewis
Copy link
Contributor

@YoussfeCantCode

Have had a discussion with Evan regarding this, I think the consistency between single player and the multiplayer private lobby is best so have the execution be the same across both

@FloPinguin
Copy link
Contributor

@Sgt-lewis But we should use a text like "Game paused" instead of "Game paused by Lobby Creator" for singleplayer.

Currently only "Game paused by Lobby Creator" is added to en.json

@iiamlewis
Copy link
Contributor

@Sgt-lewis But we should use a text like "Game paused" instead of "Game paused by Lobby Creator" for singleplayer.

Currently only "Game paused by Lobby Creator" is added to en.json

Yep I agree sorry if I miss understood your point but I agree, would be good to have the "Game Paused" for single player and "Game Paused by Lobby Host" for the multiplayer games

- removed background blur
- added text for singleplayer game type
- removed completely the overlay in replays
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e2f877 and 24f7509.

📒 Files selected for processing (3)
  • resources/lang/en.json (1 hunks)
  • src/client/graphics/GameRenderer.ts (5 hunks)
  • src/client/graphics/layers/PauseOverlay.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/GameRenderer.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/graphics/layers/ControlPanel.ts:160-183
Timestamp: 2025-06-07T20:01:52.720Z
Learning: The user its-sii prefers simpler, more readable code over complex type-safe approaches when the benefits don't justify the added complexity. They value maintainability and clarity in code implementation.
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/PauseOverlay.ts (3)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/EventBus.ts (1)
  • EventBus (7-44)
src/client/Transport.ts (1)
  • GamePausedEvent (36-38)
🔇 Additional comments (4)
resources/lang/en.json (1)

743-746: Translation strings look good.

The pause messages appropriately differentiate between singleplayer and multiplayer contexts as discussed in the PR comments.

src/client/graphics/layers/PauseOverlay.ts (3)

1-7: Imports are clean and focused.

All imports are used and necessary for the overlay implementation.


15-17: Correct pattern for global styles.

Returning this from createRenderRoot bypasses the shadow DOM, allowing the overlay to use global Tailwind classes.


26-30: Correct replay handling.

The early return correctly hides the overlay in replay mode, as discussed in the PR objectives.

@github-project-automation github-project-automation bot moved this from Peer Review to Development in OpenFront Release Management Dec 20, 2025
@YoussfeCantCode
Copy link
Contributor Author

@Sgt-lewis But we should use a text like "Game paused" instead of "Game paused by Lobby Creator" for singleplayer.

Currently only "Game paused by Lobby Creator" is added to en.json

Done! I've also completely removed the pause overlay in replays. Figured it would make sense to keep the map navigable when reviewing gameplay.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 20, 2025
@iiamlewis iiamlewis moved this from Development to Final Review in OpenFront Release Management Dec 20, 2025
@iiamlewis
Copy link
Contributor

Great looks good. I'll pass this over to @DevelopingTom and @evanpelle to review

@iiamlewis iiamlewis moved this from Final Review to Development in OpenFront Release Management Dec 21, 2025
YoussfeCantCode and others added 2 commits December 21, 2025 23:52
- Store isLobbyCreator on PlayerView via PlayerSchema data pipeline
- Replace PauseStateMessage with GamePausedUpdate in game update stream
- Create PauseExecution with Game.setPaused()/isPaused() methods
- Unify pause handling: LocalServer detects toggle_pause intents
- Remove unused pause()/resume() methods from LocalServer
- Simplify Transport to always send pause intent
- Optimize Client constructor parameter order
coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 22, 2025
@YoussfeCantCode
Copy link
Contributor Author

@evanpelle All feedback has been addressed! Let me know if that works out.

- Remove GamePausedEvent, use GameUpdateType.GamePaused directly in UI
layers
- Remove isLobbyCreator variable from Client
- Fix pause functionality in replays by handling pause intents before
replay check
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/client/graphics/layers/PauseOverlay.ts (1)

12-13: Add definite assignment assertions for externally-set properties.

The game and eventBus properties are set by GameRenderer after construction, but TypeScript strict mode will complain. Add the ! assertion to signal these will be assigned before use:

-  public game: GameView;
-  public eventBus: EventBus;
+  public game!: GameView;
+  public eventBus!: EventBus;

Also, eventBus is imported and declared but never used in this file. If it's not needed, remove it to keep the code clean.

🧹 Nitpick comments (1)
src/client/graphics/layers/GameRightSidebar.ts (1)

105-108: Local isPaused state may drift from server state.

The sidebar toggles its local isPaused on button click, but the actual pause state is server-controlled. If the server rejects a pause request (e.g., non-lobby-creator tries to pause), the UI will show the wrong icon until the next GamePaused update arrives.

Consider syncing isPaused from GamePaused updates in tick() (like PauseOverlay does) rather than toggling locally:

 tick() {
   const updates = this.game.updatesSinceLastTick();
   if (updates) {
     this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
+    if (updates[GameUpdateType.GamePaused].length > 0) {
+      this.isPaused = updates[GameUpdateType.GamePaused][0].paused;
+    }
   }

This keeps the button icon in sync with actual game state.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bad2b71 and 877e5d6.

📒 Files selected for processing (6)
  • src/client/ClientGameRunner.ts
  • src/client/LocalServer.ts
  • src/client/Transport.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/client/graphics/layers/PauseOverlay.ts
  • src/server/GameServer.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/client/ClientGameRunner.ts
  • src/server/GameServer.ts
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/graphics/layers/ControlPanel.ts:160-183
Timestamp: 2025-06-07T20:01:52.720Z
Learning: The user its-sii prefers simpler, more readable code over complex type-safe approaches when the benefits don't justify the added complexity. They value maintainability and clarity in code implementation.
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-07-14T20:41:57.645Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1432
File: src/client/graphics/layers/NameLayer.ts:143-143
Timestamp: 2025-07-14T20:41:57.645Z
Learning: In the OpenFrontIO project, the Layer system (like NameLayer) doesn't use Lit framework and has no disconnectedCallback() or unmount mechanism available. This creates challenges for proper cleanup of event listeners and timeouts, leading to potential memory leaks. This is a known architectural limitation that should be addressed in future work.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/graphics/layers/PauseOverlay.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
🧬 Code graph analysis (3)
src/client/Transport.ts (1)
src/core/EventBus.ts (1)
  • GameEvent (1-1)
src/client/graphics/layers/PauseOverlay.ts (2)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/game/GameView.ts (1)
  • GameView (584-910)
src/client/graphics/layers/GameRightSidebar.ts (1)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (32-34)
🔇 Additional comments (9)
src/client/Transport.ts (3)

32-34: Good rename to clarify intent-based pause.

The name PauseGameIntentEvent better describes what this event does - it signals user intent to toggle pause, which gets sent as an intent to the server. Clear naming.


241-241: Event binding looks good.

The event handler registration follows the same pattern as other intent events in this file.


579-585: Clean intent-based pause handling.

This handler sends toggle_pause intent through the standard sendIntent path - same as all other game actions. This means pause works the same way for local and remote servers. Matches the past review feedback from evanpelle about having transport always send the pause intent.

src/client/LocalServer.ts (2)

110-125: Pause/unpause order is correct and well-commented.

The logic here is subtle but correct:

  • Pause: Push intent → endTurn() (delivers the pause intent) → set paused = true (blocks future turns)
  • Unpause: Clear paused = false first (so endTurn() won't bail at line 182) → push intent → endTurn()

The inline comments explain the reasoning. This ensures the pause/unpause intent reaches clients via the turn system.


127-130: Replay intent blocking is correct.

Non-pause intents are ignored during replays. The toggle_pause intent is handled above and returns early, so pause still works during replays. Simple and clear.

src/client/graphics/layers/GameRightSidebar.ts (2)

39-39: Lobby creator detection works correctly.

The one-way transition (falsetrue only) is a good pattern here since lobby creator status never changes during a game. The requestUpdate() call triggers a re-render to show the pause button.

Also applies to: 62-66


161-192: Clear conditional rendering logic.

The separation of isReplayOrSingleplayer and showPauseButton makes the intent easy to follow:

  • Fast-forward button: only for replays and singleplayer
  • Pause button: replays, singleplayer, OR multiplayer lobby creators

Good use of template literals with conditional rendering.

src/client/graphics/layers/PauseOverlay.ts (2)

20-27: Tick-based pause detection is straightforward.

Reading GamePaused updates in tick() keeps the overlay in sync with actual game state. The check for length > 0 before accessing [0] is correct.


29-55: Render logic is clean and handles replay edge case.

Good decisions here:

  • Replays skip the overlay so players can inspect the map while paused
  • Singleplayer vs multiplayer text selection uses simple conditional
  • The overlay blocks context menu to prevent accidental interactions

One small thing: the double-space in the class string at line 38 (bg-opacity-50 z-50) is harmless but looks like a typo.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 24, 2025
@YoussfeCantCode
Copy link
Contributor Author

@evanpelle Done! While implementing your feedback, I also noticed and fixed an issue where pause intents where not handled properly during replays.

@evanpelle
Copy link
Collaborator

@evanpelle Done! While implementing your feedback, I also noticed and fixed an issue where pause intents where not handled properly during replays.

thanks!

@iiamlewis
Copy link
Contributor

@YoussfeCantCode can you check the resolve comments when you have done them just so when it's skimmed over we know what's been actioned and what's outstanding?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/client/LocalServer.ts (1)

110-127: Consider blocking toggle_pause intents during replays.

The toggle_pause intent is processed even when a game record exists (replay mode). While the pause state might not cause harm, allowing pause toggling during replays could lead to unexpected behavior since replay turns are pre-recorded.

Proposed fix
   if (clientMsg.type === "intent") {
+      // Don't process any intents during replays
+      if (this.lobbyConfig.gameRecord) {
+        return;
+      }
+
       if (clientMsg.intent.type === "toggle_pause") {
         if (clientMsg.intent.paused) {
           // Pausing: add intent and end turn before pause takes effect
           this.intents.push(clientMsg.intent);
           this.endTurn();
           this.paused = true;
         } else {
           // Unpausing: clear pause flag before adding intent so next turn can execute
           this.paused = false;
           this.intents.push(clientMsg.intent);
           this.endTurn();
         }
         return;
       }
-      // Don't process non-pause intents during replays
-      if (this.lobbyConfig.gameRecord) {
-        return;
-      }
src/server/GameServer.ts (1)

522-534: Consider removing verbose logging in production.

The added logging in sendStartGameMsg is helpful for debugging, but logging on every start message (including rejoins) may be excessive in production. Consider using debug-level logging or removing the detailed logs once the feature is stable.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 877e5d6 and c90ad49.

📒 Files selected for processing (5)
  • src/client/LocalServer.ts
  • src/client/graphics/layers/HeadsUpMessage.ts
  • src/core/execution/ExecutionManager.ts
  • src/core/execution/PauseExecution.ts
  • src/server/GameServer.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/graphics/layers/ControlPanel.ts:160-183
Timestamp: 2025-06-07T20:01:52.720Z
Learning: The user its-sii prefers simpler, more readable code over complex type-safe approaches when the benefits don't justify the added complexity. They value maintainability and clarity in code implementation.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GameServer.ts
  • src/core/execution/PauseExecution.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/layers/HeadsUpMessage.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/core/execution/PauseExecution.ts
📚 Learning: 2025-08-28T22:47:31.406Z
Learnt from: BrewedCoffee
Repo: openfrontio/OpenFrontIO PR: 1957
File: tests/core/executions/PlayerExecution.test.ts:16-39
Timestamp: 2025-08-28T22:47:31.406Z
Learning: In test files, PlayerExecution instances must be manually registered with game.addExecution() because the setup utility doesn't automatically register SpawnExecution, which would normally handle this during the spawn phase in a real game.

Applied to files:

  • src/core/execution/PauseExecution.ts
🧬 Code graph analysis (2)
src/core/execution/PauseExecution.ts (1)
src/core/game/Game.ts (3)
  • Execution (356-361)
  • Player (531-669)
  • Game (671-767)
src/core/execution/ExecutionManager.ts (1)
src/core/execution/PauseExecution.ts (1)
  • PauseExecution (3-27)
🔇 Additional comments (6)
src/client/graphics/layers/HeadsUpMessage.ts (2)

28-36: LGTM!

The pause state handling via GamePaused updates is clean. The visibility logic combining spawn phase and pause state is correct, and the getMessage() helper keeps the render method simple.


39-50: LGTM!

The getMessage() method is straightforward and correctly selects the appropriate translation key based on game type when paused. Good separation of concerns.

src/server/GameServer.ts (2)

351-378: LGTM!

The toggle_pause handler correctly validates that only the lobby creator can pause/resume the game. The pause sequence (add intent → end turn → set flag) and unpause sequence (clear flag → add intent → end turn) ensure proper turn synchronization across clients.


555-559: LGTM!

The early return when isPaused is true correctly prevents turn advancement while the game is paused. Simple and effective.

src/core/execution/ExecutionManager.ts (1)

127-128: LGTM!

The toggle_pause case follows the established pattern for intent handling. Clean integration with PauseExecution.

src/core/execution/PauseExecution.ts (1)

1-27: LGTM!

Clean implementation following the Execution interface. The defense-in-depth check in init() (verifying lobby creator or singleplayer) provides a good safety net even though the server already validates this. The activeDuringSpawnPhase() = true correctly allows pausing during the spawn phase.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 25, 2025
@YoussfeCantCode
Copy link
Contributor Author

I noticed a problem with using Pause Intent in replays that I didn't catch before (since replays weren't showing pause messages previously), when watching a replay, the viewer can see the original game's pauses flash briefly on screen because it's processing original gameplay intents. Additionally, when the viewer presses pause during a replay, it actually pauses, but the HeadsUpMessage doesn't get the update to show the pause message because the viewer's pause intent gets discarded when endTurn() replaces this.intents with the archived replay intents.

if (this.replayTurns.length > 0) {
if (this.turns.length >= this.replayTurns.length) {
this.endGame();
return;
}
this.intents = this.replayTurns[this.turns.length].intents;
}

Should I maybe filter out pause intents upon replay creation? Or maybe change the approach to this altogether without using intents?

@evanpelle
Copy link
Collaborator

I noticed a problem with using Pause Intent in replays that I didn't catch before (since replays weren't showing pause messages previously), when watching a replay, the viewer can see the original game's pauses flash briefly on screen because it's processing original gameplay intents. Additionally, when the viewer presses pause during a replay, it actually pauses, but the HeadsUpMessage doesn't get the update to show the pause message because the viewer's pause intent gets discarded when endTurn() replaces this.intents with the archived replay intents.

if (this.replayTurns.length > 0) {
if (this.turns.length >= this.replayTurns.length) {
this.endGame();
return;
}
this.intents = this.replayTurns[this.turns.length].intents;
}

Should I maybe filter out pause intents upon replay creation? Or maybe change the approach to this altogether without using intents?

when watching a replay, the viewer can see the original game's pauses flash briefly on screen

could we have GameRunner ignore pause intents on replays?

it actually pauses, but the HeadsUpMessage doesn't get the update to show the pause message because the viewer's pause intent gets discarded when endTurn() replaces this.intents with the archived replay intents.

I think this is okay. I don't think we really need a heads up message when pause on replay (or even singpleplayer for that matter) since the user knows they paused it.

…ng pause

- Filter out pause intents from archived replays in ClientGameRunner
- Prevent processing intents while game is paused in LocalServer and
GameServer
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/client/graphics/layers/GameRightSidebar.ts (1)

40-40: Consider resetting isLobbyCreator in init() to avoid stale state.

The isLobbyCreator flag uses a one-way transition (false → true), but it's never reset when init() is called. If the same component instance is reused between games, a player who was lobby creator in one game could retain that status incorrectly in a subsequent game.

Proposed fix
  init() {
+   this.isLobbyCreator = false;
    this._isSinglePlayer =
      this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
      this.game.config().isReplay();
    this._isVisible = true;
    this.game.inSpawnPhase();

    this.requestUpdate();
  }

Also applies to: 46-54

src/server/GameServer.ts (1)

525-531: Early return when client not found could silently fail for late joiners.

If sendStartGameMsg is called for a websocket that isn't in activeClients (e.g., during a race between joinClient and sendStartGameMsg), the function returns early with only a warning. This could leave a late joiner without the start message.

The current code path in joinClient (line 224) and rejoinClient (line 271) should always have the client in activeClients before calling this, but the defensive return could mask issues.

Consider throwing an error instead of silently returning, or verify that all callers guarantee the client is in activeClients:

#!/bin/bash
# Find all call sites of sendStartGameMsg
rg -n "sendStartGameMsg" --type=ts -C2
src/client/LocalServer.ts (1)

111-121: Add comment explaining pause flag ordering.

The ordering of when this.paused is set is subtle but important:

  • When pausing: intent is pushed and endTurn() called before setting paused = true, so the pause intent is sent before the pause takes effect.
  • When unpausing: paused is cleared before calling endTurn(), so the unpause intent can be sent.

Consider adding a brief comment explaining this sequencing to help future maintainers.

📝 Suggested clarifying comments
     if (clientMsg.intent.type === "toggle_pause") {
       if (clientMsg.intent.paused) {
-        // Pausing: add intent and end turn before pause takes effect
+        // Pausing: send the pause intent in a turn, then set paused flag
+        // so subsequent turns are blocked by endTurn() gate at line 179
         this.intents.push(clientMsg.intent);
         this.endTurn();
         this.paused = true;
       } else {
-        // Unpausing: clear pause flag before adding intent so next turn can execute
+        // Unpausing: clear pause flag so unpause intent can be sent,
+        // then normal turn execution resumes
         this.paused = false;
         this.intents.push(clientMsg.intent);
         this.endTurn();
       }
       return;
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7540574 and c0a417c.

📒 Files selected for processing (4)
  • src/client/ClientGameRunner.ts
  • src/client/LocalServer.ts
  • src/client/graphics/layers/GameRightSidebar.ts
  • src/server/GameServer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/ClientGameRunner.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/graphics/layers/ControlPanel.ts:160-183
Timestamp: 2025-06-07T20:01:52.720Z
Learning: The user its-sii prefers simpler, more readable code over complex type-safe approaches when the benefits don't justify the added complexity. They value maintainability and clarity in code implementation.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GameServer.ts
  • src/client/graphics/layers/GameRightSidebar.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/GameServer.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/graphics/layers/GameRightSidebar.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/GameRightSidebar.ts (1)
src/client/Transport.ts (1)
  • PauseGameIntentEvent (32-34)
🔇 Additional comments (8)
src/client/graphics/layers/GameRightSidebar.ts (2)

106-109: Local isPaused toggle is optimistic but acceptable here.

The local state is toggled before the server confirms the pause. This works because the pause button is only visible to lobby creators (checked in maybeRenderReplayButtons), so the server should always accept the request. For singleplayer/replay mode, there's no server validation anyway.


164-195: Clean conditional rendering logic.

The computed flags isReplayOrSingleplayer and showPauseButton make the render logic easy to follow. The separation of replay controls (fast-forward) and pause controls is clear.

src/server/GameServer.ts (4)

351-379: Pause toggle logic is correct and well-ordered.

The sequencing handles the state transitions properly:

  • Pause: broadcast intent → end turn → then freeze (clients see the pause intent)
  • Unpause: unfreeze → broadcast intent → end turn (turn can execute normally)

Auth check ensures only the lobby creator can toggle. Good work.


380-386: Intents silently dropped while paused.

When the game is paused, player intents are discarded without acknowledgment. This is the expected behavior for a frozen game, but players won't receive feedback that their actions aren't being queued. Consider if this could cause confusion during long pauses.

Confirm this matches the intended UX - are players expected to know their inputs won't queue while paused, or should the client disable input controls?


558-562: Pause guard in endTurn correctly freezes turn execution.

When paused, no turns are broadcast to clients, effectively freezing game state for all players. This pairs well with the intent-dropping logic above.


498-498: Good: isLobbyCreator propagated to all clients via player records.

This aligns with the approach discussed in past reviews - storing lobby creator status in PlayerSchema so all clients know who the creator is. This enables future features like in-game kicks without additional round trips.

src/client/LocalServer.ts (2)

124-127: LGTM!

The guard correctly blocks non-pause intents during replay or while paused, and the comment clearly explains the intent.


178-181: LGTM!

The pause gate in endTurn() correctly prevents turn execution while paused, and works correctly with the pause/unpause flows in lines 111-121.

Comment on lines +110 to 123
if (clientMsg.intent.type === "toggle_pause") {
if (clientMsg.intent.paused) {
// Pausing: add intent and end turn before pause takes effect
this.intents.push(clientMsg.intent);
this.endTurn();
this.paused = true;
} else {
// Unpausing: clear pause flag before adding intent so next turn can execute
this.paused = false;
this.intents.push(clientMsg.intent);
this.endTurn();
}
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure
git ls-files | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 632


🏁 Script executed:

# Search for GameRunner or ExecutionManager classes
find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "class GameRunner\|class ExecutionManager" 2>/dev/null | head -10

Repository: openfrontio/OpenFrontIO

Length of output: 91


🏁 Script executed:

# Search for how pause intents are handled during replay
rg -n "toggle_pause|TogglePauseIntent|isReplay" --type ts -A 3 -B 3 | head -100

Repository: openfrontio/OpenFrontIO

Length of output: 5847


🏁 Script executed:

# Check LocalServer.ts for the toggle_pause handling and endTurn method
cat -n src/client/LocalServer.ts | sed -n '105,190p'

Repository: openfrontio/OpenFrontIO

Length of output: 3568


🏁 Script executed:

# Search for pause overlay or UI components related to replay
rg -n "PauseOverlay|pause.*replay|replay.*pause" --type ts --type tsx -C 2

Repository: openfrontio/OpenFrontIO

Length of output: 94


🏁 Script executed:

# Check GameRunner for pause intent handling and replay mode
cat -n src/core/GameRunner.ts | head -50

Repository: openfrontio/OpenFrontIO

Length of output: 1920


🏁 Script executed:

# Search for PauseExecution class
rg -n "class PauseExecution" --type ts -A 20

Repository: openfrontio/OpenFrontIO

Length of output: 1343


🏁 Script executed:

# Search for UI components or conditions preventing pause during replay
rg -n "pause.*button\|PauseButton\|canPause\|allowPause" --type ts --type jsx -C 3

Repository: openfrontio/OpenFrontIO

Length of output: 94


🏁 Script executed:

# Check how archived replay intents are used and if they include pause intents
rg -n "replayTurns\|gameRecord" --type ts -C 2 | head -80

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check if there's any code preventing toggle_pause during replay in UI or configuration
rg -n "gameRecord" --type ts -B 3 -A 3 | grep -A 5 -B 5 "pause\|toggle" | head -40

Repository: openfrontio/OpenFrontIO

Length of output: 685


🏁 Script executed:

# Check Game config and replay related code
rg -n "isReplay\|gameRecord" --type ts | grep -v node_modules | head -30

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Look at how the game handles pause state during replay more completely
cat -n src/core/execution/PauseExecution.ts

Repository: openfrontio/OpenFrontIO

Length of output: 804


🏁 Script executed:

# Search for any UI code that might prevent pause button during replay
find . -type f \( -name "*.ts" -o -name "*.js" \) | xargs grep -l "pause.*disable\|disabled.*pause" 2>/dev/null | head -5

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check game.setPaused to see if it has any replay safeguards
rg -n "setPaused" --type ts -B 5 -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 1935


🏁 Script executed:

# Check if UI prevents sending toggle_pause during replay
rg -n "gameRecord\|isReplay" --type ts | xargs -I {} sh -c 'grep -l "pause\|Pause" $(echo {} | cut -d: -f1)' | sort -u | head -10

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Look for client-side code that might disable pause during replay
find . -type f -name "*.ts" | xargs grep -l "clientMsg.intent.type.*pause\|toggle_pause" 2>/dev/null

Repository: openfrontio/OpenFrontIO

Length of output: 243


🏁 Script executed:

# Check if there's any filtering of intents based on replay status before they reach LocalServer
rg -n "sendIntent\|clientMsg" --type ts -B 3 -A 3 | grep -B 5 -A 5 "gameRecord\|isReplay" | head -50

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# More complete view of how pause intents are handled - check if archived intents can contain pause intents
rg -n "turn.*intents\|intents.*turn" --type ts | grep -i "replay\|gamerecord\|archive" | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check ClientGameRunner for pause handling and replay mode
rg -n "pause\|toggle_pause\|gameRecord\|isReplay" src/client/ClientGameRunner.ts -B 2 -A 2

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check the full context of how intents are sent from client
cat -n src/client/ClientGameRunner.ts | head -100

Repository: openfrontio/OpenFrontIO

Length of output: 3839


🏁 Script executed:

# Verify: does PauseExecution even execute during replay? Check if replayed turns include pause intents
rg -n "archiv\|replay.*intent\|intent.*replay" --type ts -C 3 | head -60

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check if pause intents can exist in archived turns
rg -n "TogglePauseIntent" --type ts -B 3 -A 3

Repository: openfrontio/OpenFrontIO

Length of output: 1570


🏁 Script executed:

# Check if singleplayer has special handling for pause during replay
rg -n "gameType.*Singleplayer\|Singleplayer.*replay" --type ts -B 3 -A 3

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Verify: are singleplayer replays even possible? How is replay initiated?
rg -n "gameRecord\|replayTurns" src/client/LocalServer.ts -B 5 -A 5 | head -60

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check the Turn schema to see if pause intents should be in archived turns
rg -n "type Turn\|TurnSchema" src/core/Schemas.ts -A 10

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check how replay is initiated and what gameRecord contains
rg -n "lobbyConfig.gameRecord" --type ts -B 3 -A 3 | head -80

Repository: openfrontio/OpenFrontIO

Length of output: 2901


🏁 Script executed:

# Verify the complete flow: does toggle_pause work during singleplayer replay?
cat -n src/client/LocalServer.ts | sed -n '30,50p'

Repository: openfrontio/OpenFrontIO

Length of output: 793


🏁 Script executed:

# Look for any UI code that might disable pause button during replay
find . -type f -name "*.ts" | xargs grep -l "paused\|PauseOverlay" 2>/dev/null | head -10

Repository: openfrontio/OpenFrontIO

Length of output: 379


🏁 Script executed:

# Check if PauseOverlay or pause UI is disabled during replay
cat src/client/graphics/layers/HeadsUpMessage.ts | grep -n "pause\|Pause" -i -C 3

Repository: openfrontio/OpenFrontIO

Length of output: 946


🏁 Script executed:

# Search for pause button/control in Graphics or UI layers
find . -type f -name "*.ts" | xargs grep -l "pause.*button\|button.*pause\|disable.*pause" 2>/dev/null | head -5

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check Transport to see if it blocks pause intent sending during replay
rg -n "SendPause\|toggle_pause" src/client/Transport.ts -B 5 -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Verify the complete picture: How does replay speed work? Can user interact during replay?
rg -n "replaySpeed\|isReplay" src/client/LocalServer.ts -B 2 -A 2

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check if there's any code that prevents sending pause intent during replay from UI
rg -n "SendIntent\|sendIntent" src/client/Transport.ts -B 5 -A 5 | head -40

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Final check: is there any mention of pause being blocked during replay in comments or code?
rg -n "replay.*pause\|pause.*replay" src/client/ --type ts -i -C 2

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check if singleplayer replays use the same PauseExecution logic
rg -n "Singleplayer" src/core/execution/PauseExecution.ts -B 5 -A 5

Repository: openfrontio/OpenFrontIO

Length of output: 315


🏁 Script executed:

# Verify: does LocalServer.isReplay have different behavior than checking lobbyConfig.gameRecord?
cat -n src/client/LocalServer.ts | sed -n '47,90p'

Repository: openfrontio/OpenFrontIO

Length of output: 1660


🏁 Script executed:

# Final verification: can pause work during singleplayer replay?
rg -n "isReplay.*paused\|paused.*isReplay" src/client/LocalServer.ts -B 3 -A 3

Repository: openfrontio/OpenFrontIO

Length of output: 49


Guard toggle_pause against replay or document the behavior.

The toggle_pause handler doesn't check for replay mode (lines 110-122), so if sent during replay, the intent is pushed to this.intents but then discarded when endTurn() replaces intents with archived replay intents (line 187). In singleplayer replay, pause should work since the replayer is the player, but the discarded intent means PauseExecution never processes it.

Either:

  • Guard toggle_pause against replay if pause should be blocked during replay, OR
  • Preserve pause intents during replay if pause should work, OR
  • Add a comment explaining why pause intents are silently discarded
🤖 Prompt for AI Agents
In src/client/LocalServer.ts around lines 110 to 123, the toggle_pause handler
currently pushes pause intents even during replay which causes them to be
overwritten/ignored later; update the handler to first check replay mode and
bail out (or explicitly document the behavior) — i.e., if this.replaying (or
equivalent flag) is true, do not push the pause intent or call endTurn(),
instead return early (optionally log/debug message), otherwise proceed with the
existing push/endTurn/pause logic so pause only takes effect outside replay.

@YoussfeCantCode
Copy link
Contributor Author

could we have GameRunner ignore pause intents on replays?

Makes sense! I filtered in ClientGameRunner before sending turns to the worker since it already has access to lobbyConfig.gameRecord and avoids modifying worker messages. Thoughts?

Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@evanpelle evanpelle merged commit 1c52d20 into openfrontio:main Dec 26, 2025
7 checks passed
@github-project-automation github-project-automation bot moved this from Development to Complete in OpenFront Release Management Dec 26, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 27, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

Match pause function for private lobbies

4 participants