Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,10 @@
"choose_spawn": "Choose a starting location",
"random_spawn": "Random spawn is enabled. Selecting starting location for you..."
},
"pause": {
"singleplayer_game_paused": "Game paused",
"multiplayer_game_paused": "Game paused by Lobby Creator"
},
"territory_patterns": {
"title": "Skins",
"colors": "Colors",
Expand Down
13 changes: 12 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ export class ClientGameRunner {
this.saveGame(gu.updates[GameUpdateType.Win][0]);
}
});

const worker = this.worker;
const keepWorkerAlive = () => {
if (this.isActive) {
Expand Down Expand Up @@ -432,7 +433,17 @@ export class ClientGameRunner {
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
);
} else {
this.worker.sendTurn(message.turn);
this.worker.sendTurn(
// Filter out pause intents in replays
this.gameView.config().isReplay()
? {
...message.turn,
intents: message.turn.intents.filter(
(i) => i.type !== "toggle_pause",
),
}
: message.turn,
);
this.turnsSeen++;
}
}
Expand Down
26 changes: 15 additions & 11 deletions src/client/LocalServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,6 @@ export class LocalServer {
} satisfies ServerStartGameMessage);
}

pause() {
this.paused = true;
}

resume() {
this.paused = false;
}

onMessage(clientMsg: ClientMessage) {
if (clientMsg.type === "rejoin") {
this.clientMessage({
Expand All @@ -115,13 +107,25 @@ export class LocalServer {
} satisfies ServerStartGameMessage);
}
if (clientMsg.type === "intent") {
if (this.lobbyConfig.gameRecord) {
// If we are replaying a game, we don't want to process intents
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;
}
Comment on lines +110 to 123
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.

if (this.paused) {
// Don't process non-pause intents during replays or while paused
if (this.lobbyConfig.gameRecord || this.paused) {
return;
}

this.intents.push(clientMsg.intent);
}
if (clientMsg.type === "hash") {
Expand Down
21 changes: 9 additions & 12 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { getPlayToken } from "./Auth";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalServer } from "./LocalServer";

export class PauseGameEvent implements GameEvent {
export class PauseGameIntentEvent implements GameEvent {
constructor(public readonly paused: boolean) {}
}

Expand Down Expand Up @@ -186,6 +186,7 @@ export class Transport {

private pingInterval: number | null = null;
public readonly isLocal: boolean;

constructor(
private lobbyConfig: LobbyConfig,
private eventBus: EventBus,
Expand Down Expand Up @@ -237,7 +238,7 @@ export class Transport {
);
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));

this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
this.eventBus.on(PauseGameIntentEvent, (e) => this.onPauseGameIntent(e));
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e));
this.eventBus.on(SendHashEvent, (e) => this.onSendHashEvent(e));
this.eventBus.on(CancelAttackIntentEvent, (e) =>
Expand Down Expand Up @@ -575,16 +576,12 @@ export class Transport {
});
}

private onPauseGameEvent(event: PauseGameEvent) {
if (!this.isLocal) {
console.log(`cannot pause multiplayer games`);
return;
}
if (event.paused) {
this.localServer.pause();
} else {
this.localServer.resume();
}
private onPauseGameIntent(event: PauseGameIntentEvent) {
this.sendIntent({
type: "toggle_pause",
clientID: this.lobbyConfig.clientID,
paused: event.paused,
});
}

private onSendWinnerEvent(event: SendWinnerEvent) {
Expand Down
63 changes: 41 additions & 22 deletions src/client/graphics/layers/GameRightSidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { PauseGameEvent } from "../../Transport";
import { PauseGameIntentEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
Expand All @@ -37,6 +37,7 @@ export class GameRightSidebar extends LitElement implements Layer {
private timer: number = 0;

private hasWinner = false;
private isLobbyCreator = false;

createRenderRoot() {
return this;
Expand All @@ -48,6 +49,7 @@ export class GameRightSidebar extends LitElement implements Layer {
this.game.config().isReplay();
this._isVisible = true;
this.game.inSpawnPhase();

this.requestUpdate();
}

Expand All @@ -57,6 +59,13 @@ export class GameRightSidebar extends LitElement implements Layer {
if (updates) {
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
}

// Check if the player is the lobby creator
if (!this.isLobbyCreator && this.game.myPlayer()?.isLobbyCreator()) {
this.isLobbyCreator = true;
this.requestUpdate();
}

const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
if (maxTimerValue !== undefined) {
if (this.game.inSpawnPhase()) {
Expand Down Expand Up @@ -96,7 +105,7 @@ export class GameRightSidebar extends LitElement implements Layer {

private onPauseButtonClick() {
this.isPaused = !this.isPaused;
this.eventBus.emit(new PauseGameEvent(this.isPaused));
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
}

private onExitButtonClick() {
Expand Down Expand Up @@ -153,25 +162,35 @@ export class GameRightSidebar extends LitElement implements Layer {
}

maybeRenderReplayButtons() {
if (this._isSinglePlayer || this.game?.config()?.isReplay()) {
return html` <div class="cursor-pointer" @click=${this.toggleReplayPanel}>
<img
src=${FastForwardIconSolid}
alt="replay"
width="20"
height="20"
/>
</div>
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
<img
src=${this.isPaused ? playIcon : pauseIcon}
alt="play/pause"
width="20"
height="20"
/>
</div>`;
} else {
return html``;
}
const isReplayOrSingleplayer =
this._isSinglePlayer || this.game?.config()?.isReplay();
const showPauseButton = isReplayOrSingleplayer || this.isLobbyCreator;

return html`
${isReplayOrSingleplayer
? html`
<div class="cursor-pointer" @click=${this.toggleReplayPanel}>
<img
src=${FastForwardIconSolid}
alt="replay"
width="20"
height="20"
/>
</div>
`
: ""}
${showPauseButton
? html`
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
<img
src=${this.isPaused ? playIcon : pauseIcon}
alt="play/pause"
width="20"
height="20"
/>
</div>
`
: ""}
`;
}
}
38 changes: 30 additions & 8 deletions src/client/graphics/layers/HeadsUpMessage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
Expand All @@ -11,6 +13,9 @@ export class HeadsUpMessage extends LitElement implements Layer {
@state()
private isVisible = false;

@state()
private isPaused = false;

createRenderRoot() {
return this;
}
Expand All @@ -21,28 +26,45 @@ export class HeadsUpMessage extends LitElement implements Layer {
}

tick() {
if (!this.game.inSpawnPhase()) {
this.isVisible = false;
this.requestUpdate();
const updates = this.game.updatesSinceLastTick();
if (updates && updates[GameUpdateType.GamePaused].length > 0) {
const pauseUpdate = updates[GameUpdateType.GamePaused][0];
this.isPaused = pauseUpdate.paused;
}

this.isVisible = this.game.inSpawnPhase() || this.isPaused;
this.requestUpdate();
}

private getMessage(): string {
if (this.isPaused) {
if (this.game.config().gameConfig().gameType === GameType.Singleplayer) {
return translateText("pause.singleplayer_game_paused");
} else {
return translateText("pause.multiplayer_game_paused");
}
}
return this.game.config().isRandomSpawn()
? translateText("heads_up_message.random_spawn")
: translateText("heads_up_message.choose_spawn");
}

render() {
if (!this.isVisible) {
return html``;
}

const message = this.getMessage();

return html`
<div
class="flex items-center relative
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
${this.game.config().isRandomSpawn()
? translateText("heads_up_message.random_spawn")
: translateText("heads_up_message.choose_spawn")}
${message}
</div>
`;
}
Expand Down
4 changes: 2 additions & 2 deletions src/client/graphics/layers/SettingsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import musicIcon from "../../../../resources/images/music.svg";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { PauseGameEvent } from "../../Transport";
import { PauseGameIntentEvent } from "../../Transport";
import { translateText } from "../../Utils";
import SoundManager from "../../sound/SoundManager";
import { Layer } from "./Layer";
Expand Down Expand Up @@ -108,7 +108,7 @@ export class SettingsModal extends LitElement implements Layer {

private pauseGame(pause: boolean) {
if (this.shouldPause && !this.wasPausedWhenOpened)
this.eventBus.emit(new PauseGameEvent(pause));
this.eventBus.emit(new PauseGameIntentEvent(pause));
}

private onTerrainButtonClick() {
Expand Down
1 change: 1 addition & 0 deletions src/core/GameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function createGameRunner(
PlayerType.Human,
p.clientID,
random.nextID(),
p.isLobbyCreator ?? false,
);
});

Expand Down
12 changes: 11 additions & 1 deletion src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export type Intent =
| EmbargoAllIntent
| UpgradeStructureIntent
| DeleteUnitIntent
| KickPlayerIntent;
| KickPlayerIntent
| TogglePauseIntent;

export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
Expand Down Expand Up @@ -79,6 +80,7 @@ export type AllianceExtensionIntent = z.infer<
>;
export type DeleteUnitIntent = z.infer<typeof DeleteUnitIntentSchema>;
export type KickPlayerIntent = z.infer<typeof KickPlayerIntentSchema>;
export type TogglePauseIntent = z.infer<typeof TogglePauseIntentSchema>;

export type Turn = z.infer<typeof TurnSchema>;
export type GameConfig = z.infer<typeof GameConfigSchema>;
Expand All @@ -91,6 +93,7 @@ export type ClientMessage =
| ClientRejoinMessage
| ClientLogMessage
| ClientHashMessage;

export type ServerMessage =
| ServerTurnMessage
| ServerStartGameMessage
Expand Down Expand Up @@ -354,6 +357,11 @@ export const KickPlayerIntentSchema = BaseIntentSchema.extend({
target: ID,
});

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

const IntentSchema = z.discriminatedUnion("type", [
AttackIntentSchema,
CancelAttackIntentSchema,
Expand All @@ -377,6 +385,7 @@ const IntentSchema = z.discriminatedUnion("type", [
AllianceExtensionIntentSchema,
DeleteUnitIntentSchema,
KickPlayerIntentSchema,
TogglePauseIntentSchema,
]);

//
Expand Down Expand Up @@ -430,6 +439,7 @@ export const PlayerSchema = z.object({
clientID: ID,
username: UsernameSchema,
cosmetics: PlayerCosmeticsSchema.optional(),
isLobbyCreator: z.boolean().optional(),
});

export const GameStartInfoSchema = z.object({
Expand Down
Loading
Loading