diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 575977f9a..eb1088c66 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -12,7 +12,7 @@ import { import { createPartialGameRecord, replacer } from "../core/Util"; import { ServerConfig } from "../core/configuration/Config"; import { getConfig } from "../core/configuration/ConfigLoader"; -import { PlayerActions, UnitType } from "../core/game/Game"; +import { GameUpdates, PlayerActions, UnitType } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { GameMapLoader } from "../core/game/GameMapLoader"; import { @@ -25,9 +25,17 @@ import { import { GameView, PlayerView } from "../core/game/GameView"; import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader"; import { UserSettings } from "../core/game/UserSettings"; +import { + createSharedTileRingBuffers, + createSharedTileRingViews, + drainTileUpdates, + SharedTileRingBuffers, + SharedTileRingViews, +} from "../core/worker/SharedTileRing"; import { WorkerClient } from "../core/worker/WorkerClient"; import { AutoUpgradeEvent, + BacklogStatusEvent, DoBoatAttackEvent, DoGroundAttackEvent, InputHandler, @@ -161,9 +169,30 @@ async function createClientGame( mapLoader, ); } + + let sharedTileRingBuffers: SharedTileRingBuffers | undefined; + let sharedTileRingViews: SharedTileRingViews | null = null; + const isIsolated = + typeof (globalThis as any).crossOriginIsolated === "boolean" + ? (globalThis as any).crossOriginIsolated === true + : false; + const canUseSharedBuffers = + typeof SharedArrayBuffer !== "undefined" && + typeof Atomics !== "undefined" && + isIsolated; + + if (canUseSharedBuffers) { + // Capacity is number of tile updates that can be queued. + // This is a compromise between memory usage and backlog tolerance. + const TILE_RING_CAPACITY = 262144; + sharedTileRingBuffers = createSharedTileRingBuffers(TILE_RING_CAPACITY); + sharedTileRingViews = createSharedTileRingViews(sharedTileRingBuffers); + } + const worker = new WorkerClient( lobbyConfig.gameStartInfo, lobbyConfig.clientID, + sharedTileRingBuffers, ); await worker.initialize(); const gameView = new GameView( @@ -190,6 +219,7 @@ async function createClientGame( transport, worker, gameView, + sharedTileRingViews, ); } @@ -208,6 +238,21 @@ export class ClientGameRunner { private lastTickReceiveTime: number = 0; private currentTickDelay: number | undefined = undefined; + // Track how far behind the client simulation is compared to the server. + private serverTurnHighWater: number = 0; + private lastProcessedTick: number = 0; + private backlogTurns: number = 0; + private backlogGrowing: boolean = false; + private lastRenderedTick: number = 0; + private workerTicksSinceSample: number = 0; + private renderTicksSinceSample: number = 0; + private metricsSampleStart: number = 0; + + private pendingUpdates: GameUpdateViewData[] = []; + private pendingStart = 0; + private isProcessingUpdates = false; + private tileRingViews: SharedTileRingViews | null; + constructor( private lobby: LobbyConfig, private eventBus: EventBus, @@ -216,8 +261,10 @@ export class ClientGameRunner { private transport: Transport, private worker: WorkerClient, private gameView: GameView, + tileRingViews: SharedTileRingViews | null, ) { this.lastMessageTime = Date.now(); + this.tileRingViews = tileRingViews; } private saveGame(update: WinUpdate) { @@ -292,33 +339,9 @@ export class ClientGameRunner { this.stop(); return; } - this.transport.turnComplete(); - gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { - this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); - }); - this.gameView.update(gu); - this.renderer.tick(); - - // Emit tick metrics event for performance overlay - this.eventBus.emit( - new TickMetricsEvent(gu.tickExecutionDuration, this.currentTickDelay), - ); - - // Reset tick delay for next measurement - this.currentTickDelay = undefined; - - if (gu.updates[GameUpdateType.Win].length > 0) { - this.saveGame(gu.updates[GameUpdateType.Win][0]); - } + this.pendingUpdates.push(gu); + this.processPendingUpdates(); }); - const worker = this.worker; - const keepWorkerAlive = () => { - if (this.isActive) { - worker.sendHeartbeat(); - requestAnimationFrame(keepWorkerAlive); - } - }; - requestAnimationFrame(keepWorkerAlive); const onconnect = () => { console.log("Connected to game server!"); @@ -363,6 +386,10 @@ export class ClientGameRunner { } for (const turn of message.turns) { + this.serverTurnHighWater = Math.max( + this.serverTurnHighWater, + turn.turnNumber, + ); if (turn.turnNumber < this.turnsSeen) { continue; } @@ -415,6 +442,11 @@ export class ClientGameRunner { } this.lastTickReceiveTime = now; + this.serverTurnHighWater = Math.max( + this.serverTurnHighWater, + message.turn.turnNumber, + ); + if (this.turnsSeen !== message.turn.turnNumber) { console.error( `got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`, @@ -445,6 +477,203 @@ export class ClientGameRunner { } } + private processPendingUpdates() { + const pendingCount = this.pendingUpdates.length - this.pendingStart; + if (this.isProcessingUpdates || pendingCount <= 0) { + return; + } + + this.isProcessingUpdates = true; + const processFrame = () => { + const BASE_SLICE_BUDGET_MS = 8; // keep UI responsive while catching up + const MAX_SLICE_BUDGET_MS = 1000; // allow longer slices when backlog is large + const BACKLOG_FREE_TURNS = 10; // scaling starts at this many turns + const BACKLOG_MAX_TURNS = 500; // MAX_SLICE_BUDGET_MS is reached at this many turns + const MAX_TICKS_PER_SLICE = 1000; + + const backlogOverhead = Math.max( + 0, + this.backlogTurns - BACKLOG_FREE_TURNS, + ); + const backlogScale = Math.min( + 1, + backlogOverhead / (BACKLOG_MAX_TURNS - BACKLOG_FREE_TURNS), + ); + const sliceBudgetMs = + BASE_SLICE_BUDGET_MS + + backlogScale * (MAX_SLICE_BUDGET_MS - BASE_SLICE_BUDGET_MS); + + const frameStart = performance.now(); + const batch: GameUpdateViewData[] = []; + let lastTickDuration: number | undefined; + let lastTick: number | undefined; + + let processedCount = 0; + + // Consume updates until we hit the time budget or per-slice cap. + while (this.pendingStart < this.pendingUpdates.length) { + const gu = this.pendingUpdates[this.pendingStart++]; + processedCount++; + this.workerTicksSinceSample++; + batch.push(gu); + + this.transport.turnComplete(); + gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { + this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); + }); + this.updateBacklogMetrics(gu.tick); + + if (gu.updates[GameUpdateType.Win].length > 0) { + this.saveGame(gu.updates[GameUpdateType.Win][0]); + } + + if (gu.tickExecutionDuration !== undefined) { + lastTickDuration = gu.tickExecutionDuration; + } + lastTick = gu.tick; + + const elapsed = performance.now() - frameStart; + if (processedCount >= MAX_TICKS_PER_SLICE || elapsed >= sliceBudgetMs) { + break; + } + } + + // Compact the queue if we've advanced far into it. + if ( + this.pendingStart > 0 && + (this.pendingStart > 1024 || + this.pendingStart >= this.pendingUpdates.length / 2) + ) { + this.pendingUpdates = this.pendingUpdates.slice(this.pendingStart); + this.pendingStart = 0; + } + + // Only update view and render when ALL processing is complete + if ( + this.pendingStart >= this.pendingUpdates.length && + batch.length > 0 && + lastTick !== undefined + ) { + const combinedGu = this.mergeGameUpdates(batch); + if (combinedGu) { + this.gameView.update(combinedGu); + } + + const ticksPerRender = + this.lastRenderedTick === 0 + ? lastTick + : lastTick - this.lastRenderedTick; + this.lastRenderedTick = lastTick; + + this.renderTicksSinceSample++; + + let workerTicksPerSecond: number | undefined; + let renderTicksPerSecond: number | undefined; + const now = performance.now(); + if (this.metricsSampleStart === 0) { + this.metricsSampleStart = now; + } else { + const elapsedSeconds = (now - this.metricsSampleStart) / 1000; + if (elapsedSeconds > 0) { + workerTicksPerSecond = this.workerTicksSinceSample / elapsedSeconds; + renderTicksPerSecond = this.renderTicksSinceSample / elapsedSeconds; + } + this.metricsSampleStart = now; + this.workerTicksSinceSample = 0; + this.renderTicksSinceSample = 0; + } + + this.renderer.tick(); + this.eventBus.emit( + new TickMetricsEvent( + lastTickDuration, + this.currentTickDelay, + this.backlogTurns, + ticksPerRender, + workerTicksPerSecond, + renderTicksPerSecond, + ), + ); + + // Reset tick delay for next measurement + this.currentTickDelay = undefined; + } + + if (this.pendingStart < this.pendingUpdates.length) { + requestAnimationFrame(processFrame); + } else { + this.isProcessingUpdates = false; + } + }; + + requestAnimationFrame(processFrame); + } + + private mergeGameUpdates( + batch: GameUpdateViewData[], + ): GameUpdateViewData | null { + if (batch.length === 0) { + return null; + } + + const last = batch[batch.length - 1]; + const combinedUpdates: GameUpdates = {} as GameUpdates; + + // Initialize combinedUpdates with empty arrays for each existing key + for (const key in last.updates) { + const type = Number(key) as GameUpdateType; + combinedUpdates[type] = []; + } + + const combinedPackedTileUpdates: bigint[] = []; + + for (const gu of batch) { + for (const key in gu.updates) { + const type = Number(key) as GameUpdateType; + // We don't care about the specific update subtype here; just treat it + // as an array we can concatenate. + const updatesForType = gu.updates[type] as unknown as any[]; + (combinedUpdates[type] as unknown as any[]).push(...updatesForType); + } + } + + if (this.tileRingViews) { + const MAX_TILE_UPDATES_PER_RENDER = 100000; + drainTileUpdates( + this.tileRingViews, + MAX_TILE_UPDATES_PER_RENDER, + combinedPackedTileUpdates, + ); + } else { + for (const gu of batch) { + gu.packedTileUpdates.forEach((tu) => { + combinedPackedTileUpdates.push(tu); + }); + } + } + + return { + tick: last.tick, + updates: combinedUpdates, + packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates), + playerNameViewData: last.playerNameViewData, + tickExecutionDuration: last.tickExecutionDuration, + }; + } + + private updateBacklogMetrics(processedTick: number) { + this.lastProcessedTick = processedTick; + const previousBacklog = this.backlogTurns; + this.backlogTurns = Math.max( + 0, + this.serverTurnHighWater - this.lastProcessedTick, + ); + this.backlogGrowing = this.backlogTurns > previousBacklog; + this.eventBus.emit( + new BacklogStatusEvent(this.backlogTurns, this.backlogGrowing), + ); + } + private inputEvent(event: MouseUpEvent) { if (!this.isActive || this.renderer.uiState.ghostStructure !== null) { return; diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 26d8f6c27..dbae066ee 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -129,6 +129,21 @@ export class TickMetricsEvent implements GameEvent { constructor( public readonly tickExecutionDuration?: number, public readonly tickDelay?: number, + // Number of turns the client is behind the server (if known) + public readonly backlogTurns?: number, + // Number of simulation ticks applied since last render + public readonly ticksPerRender?: number, + // Approximate worker simulation ticks per second + public readonly workerTicksPerSecond?: number, + // Approximate render tick() calls per second + public readonly renderTicksPerSecond?: number, + ) {} +} + +export class BacklogStatusEvent implements GameEvent { + constructor( + public readonly backlogTurns: number, + public readonly backlogGrowing: boolean, ) {} } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 1410cdbbd..c99a46014 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -2,7 +2,10 @@ import { EventBus } from "../../core/EventBus"; import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { GameStartingModal } from "../GameStartingModal"; -import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; +import { + BacklogStatusEvent, + RefreshGraphicsEvent as RedrawGraphicsEvent, +} from "../InputHandler"; import { FrameProfiler } from "./FrameProfiler"; import { TransformHandler } from "./TransformHandler"; import { UIState } from "./UIState"; @@ -292,6 +295,9 @@ export function createRenderer( export class GameRenderer { private context: CanvasRenderingContext2D; + private backlogTurns: number = 0; + private backlogGrowing: boolean = false; + private lastRenderTime: number = 0; constructor( private game: GameView, @@ -309,6 +315,10 @@ export class GameRenderer { initialize() { this.eventBus.on(RedrawGraphicsEvent, () => this.redraw()); + this.eventBus.on(BacklogStatusEvent, (event: BacklogStatusEvent) => { + this.backlogTurns = event.backlogTurns; + this.backlogGrowing = event.backlogGrowing; + }); this.layers.forEach((l) => l.init?.()); document.body.appendChild(this.canvas); @@ -344,6 +354,28 @@ export class GameRenderer { } renderGame() { + const now = performance.now(); + + if (this.backlogTurns > 0) { + const BASE_FPS = 60; + const MIN_FPS = 10; + const BACKLOG_MAX_TURNS = 50; + + const scale = Math.min(1, this.backlogTurns / BACKLOG_MAX_TURNS); + const targetFps = BASE_FPS - scale * (BASE_FPS - MIN_FPS); + const minFrameInterval = 1000 / targetFps; + + if (this.lastRenderTime !== 0) { + const sinceLast = now - this.lastRenderTime; + if (sinceLast < minFrameInterval) { + requestAnimationFrame(() => this.renderGame()); + return; + } + } + } + + this.lastRenderTime = now; + FrameProfiler.clear(); const start = performance.now(); // Set background diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts index fb744d4a0..64499024f 100644 --- a/src/client/graphics/layers/PerformanceOverlay.ts +++ b/src/client/graphics/layers/PerformanceOverlay.ts @@ -229,7 +229,14 @@ export class PerformanceOverlay extends LitElement implements Layer { this.setVisible(this.userSettings.performanceOverlay()); }); this.eventBus.on(TickMetricsEvent, (event: TickMetricsEvent) => { - this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay); + this.updateTickMetrics( + event.tickExecutionDuration, + event.tickDelay, + event.backlogTurns, + event.ticksPerRender, + event.workerTicksPerSecond, + event.renderTicksPerSecond, + ); }); } @@ -418,7 +425,26 @@ export class PerformanceOverlay extends LitElement implements Layer { this.layerBreakdown = breakdown; } - updateTickMetrics(tickExecutionDuration?: number, tickDelay?: number) { + @state() + private backlogTurns: number = 0; + + @state() + private ticksPerRender: number = 0; + + @state() + private workerTicksPerSecond: number = 0; + + @state() + private renderTicksPerSecond: number = 0; + + updateTickMetrics( + tickExecutionDuration?: number, + tickDelay?: number, + backlogTurns?: number, + ticksPerRender?: number, + workerTicksPerSecond?: number, + renderTicksPerSecond?: number, + ) { if (!this.isVisible || !this.userSettings.performanceOverlay()) return; // Update tick execution duration stats @@ -455,6 +481,22 @@ export class PerformanceOverlay extends LitElement implements Layer { } } + if (backlogTurns !== undefined) { + this.backlogTurns = backlogTurns; + } + + if (ticksPerRender !== undefined) { + this.ticksPerRender = ticksPerRender; + } + + if (workerTicksPerSecond !== undefined) { + this.workerTicksPerSecond = workerTicksPerSecond; + } + + if (renderTicksPerSecond !== undefined) { + this.renderTicksPerSecond = renderTicksPerSecond; + } + this.requestUpdate(); } @@ -600,6 +642,22 @@ export class PerformanceOverlay extends LitElement implements Layer { ${this.tickDelayAvg.toFixed(2)}ms (max: ${this.tickDelayMax}ms) +
+ Worker ticks/s: + ${this.workerTicksPerSecond.toFixed(1)} +
+
+ Render ticks/s: + ${this.renderTicksPerSecond.toFixed(1)} +
+
+ Ticks per render: + ${this.ticksPerRender} +
+
+ Backlog turns: + ${this.backlogTurns} +
${this.layerBreakdown.length ? html`
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 04a76c7cf..e74a00dfc 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -37,6 +37,7 @@ export async function createGameRunner( clientID: ClientID, mapLoader: GameMapLoader, callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, + tileUpdateSink?: (update: bigint) => void, ): Promise { const config = await getConfig(gameStart.config, null); const gameMap = await loadGameMap( @@ -86,6 +87,7 @@ export async function createGameRunner( game, new Executor(game, gameStart.gameID, clientID), callBack, + tileUpdateSink, ); gr.init(); return gr; @@ -102,6 +104,7 @@ export class GameRunner { public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, + private tileUpdateSink?: (update: bigint) => void, ) {} init() { @@ -176,13 +179,24 @@ export class GameRunner { }); } - // Many tiles are updated to pack it into an array - const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update); + // Many tiles are updated; either publish them via a shared sink or pack + // them into the view data. + let packedTileUpdates: BigUint64Array; + const tileUpdates = updates[GameUpdateType.Tile]; + if (this.tileUpdateSink !== undefined) { + for (const u of tileUpdates) { + this.tileUpdateSink(u.update); + } + packedTileUpdates = new BigUint64Array(); + } else { + const raw = tileUpdates.map((u) => u.update); + packedTileUpdates = new BigUint64Array(raw); + } updates[GameUpdateType.Tile] = []; this.callBack({ tick: this.game.ticks(), - packedTileUpdates: new BigUint64Array(packedTileUpdates), + packedTileUpdates, updates: updates, playerNameViewData: this.playerViewData, tickExecutionDuration: tickExecutionDuration, @@ -273,4 +287,8 @@ export class GameRunner { } return player.bestTransportShipSpawn(targetTile); } + + public hasPendingTurns(): boolean { + return this.currTurn < this.turns.length; + } } diff --git a/src/core/worker/SharedTileRing.ts b/src/core/worker/SharedTileRing.ts new file mode 100644 index 000000000..0d8d0c331 --- /dev/null +++ b/src/core/worker/SharedTileRing.ts @@ -0,0 +1,79 @@ +export interface SharedTileRingBuffers { + header: SharedArrayBuffer; + data: SharedArrayBuffer; +} + +export interface SharedTileRingViews { + header: Int32Array; + buffer: BigUint64Array; + capacity: number; +} + +// Header indices +export const TILE_RING_HEADER_WRITE_INDEX = 0; +export const TILE_RING_HEADER_READ_INDEX = 1; +export const TILE_RING_HEADER_OVERFLOW = 2; + +export function createSharedTileRingBuffers( + capacity: number, +): SharedTileRingBuffers { + const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT); + const data = new SharedArrayBuffer( + capacity * BigUint64Array.BYTES_PER_ELEMENT, + ); + return { header, data }; +} + +export function createSharedTileRingViews( + buffers: SharedTileRingBuffers, +): SharedTileRingViews { + const header = new Int32Array(buffers.header); + const buffer = new BigUint64Array(buffers.data); + return { + header, + buffer, + capacity: buffer.length, + }; +} + +export function pushTileUpdate( + views: SharedTileRingViews, + value: bigint, +): void { + const { header, buffer, capacity } = views; + + const write = Atomics.load(header, TILE_RING_HEADER_WRITE_INDEX); + const read = Atomics.load(header, TILE_RING_HEADER_READ_INDEX); + const nextWrite = (write + 1) % capacity; + + // If the buffer is full, advance read (drop oldest) and mark overflow. + if (nextWrite === read) { + Atomics.store(header, TILE_RING_HEADER_OVERFLOW, 1); + const nextRead = (read + 1) % capacity; + Atomics.store(header, TILE_RING_HEADER_READ_INDEX, nextRead); + } + + buffer[write] = value; + Atomics.store(header, TILE_RING_HEADER_WRITE_INDEX, nextWrite); +} + +export function drainTileUpdates( + views: SharedTileRingViews, + maxItems: number, + out: bigint[], +): void { + const { header, buffer, capacity } = views; + + let read = Atomics.load(header, TILE_RING_HEADER_READ_INDEX); + const write = Atomics.load(header, TILE_RING_HEADER_WRITE_INDEX); + + let count = 0; + + while (read !== write && count < maxItems) { + out.push(buffer[read]); + read = (read + 1) % capacity; + count++; + } + + Atomics.store(header, TILE_RING_HEADER_READ_INDEX, read); +} diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 1014968fb..3c1164849 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -2,6 +2,11 @@ import version from "../../../resources/version.txt"; import { createGameRunner, GameRunner } from "../GameRunner"; import { FetchGameMapLoader } from "../game/FetchGameMapLoader"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; +import { + createSharedTileRingViews, + pushTileUpdate, + SharedTileRingViews, +} from "./SharedTileRing"; import { AttackAveragePositionResultMessage, InitializedMessage, @@ -16,6 +21,8 @@ import { const ctx: Worker = self as any; let gameRunner: Promise | null = null; const mapLoader = new FetchGameMapLoader(`/maps`, version); +let isProcessingTurns = false; +let sharedTileRing: SharedTileRingViews | null = null; function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) { // skip if ErrorUpdate @@ -32,25 +39,58 @@ function sendMessage(message: WorkerMessage) { ctx.postMessage(message); } +async function processPendingTurns() { + if (isProcessingTurns) { + return; + } + if (!gameRunner) { + return; + } + + const gr = await gameRunner; + if (!gr || !gr.hasPendingTurns()) { + return; + } + + isProcessingTurns = true; + try { + while (gr.hasPendingTurns()) { + gr.executeNextTick(); + } + } finally { + isProcessingTurns = false; + } +} + ctx.addEventListener("message", async (e: MessageEvent) => { const message = e.data; switch (message.type) { - case "heartbeat": - (await gameRunner)?.executeNextTick(); - break; case "init": try { + if (message.sharedTileRingHeader && message.sharedTileRingData) { + sharedTileRing = createSharedTileRingViews({ + header: message.sharedTileRingHeader, + data: message.sharedTileRingData, + }); + } else { + sharedTileRing = null; + } + gameRunner = createGameRunner( message.gameStartInfo, message.clientID, mapLoader, gameUpdate, + sharedTileRing + ? (update: bigint) => pushTileUpdate(sharedTileRing!, update) + : undefined, ).then((gr) => { sendMessage({ type: "initialized", id: message.id, } as InitializedMessage); + processPendingTurns(); return gr; }); } catch (error) { @@ -67,6 +107,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { try { const gr = await gameRunner; await gr.addTurn(message.turn); + processPendingTurns(); } catch (error) { console.error("Failed to process turn:", error); throw error; diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index bde436f39..6df22a933 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -9,6 +9,7 @@ import { TileRef } from "../game/GameMap"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; import { ClientID, GameStartInfo, Turn } from "../Schemas"; import { generateID } from "../Util"; +import { SharedTileRingBuffers } from "./SharedTileRing"; import { WorkerMessage } from "./WorkerMessages"; export class WorkerClient { @@ -22,6 +23,7 @@ export class WorkerClient { constructor( private gameStartInfo: GameStartInfo, private clientID: ClientID, + private sharedTileRingBuffers?: SharedTileRingBuffers, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url)); this.messageHandlers = new Map(); @@ -70,6 +72,8 @@ export class WorkerClient { id: messageId, gameStartInfo: this.gameStartInfo, clientID: this.clientID, + sharedTileRingHeader: this.sharedTileRingBuffers?.header, + sharedTileRingData: this.sharedTileRingBuffers?.data, }); // Add timeout for initialization @@ -100,12 +104,6 @@ export class WorkerClient { }); } - sendHeartbeat() { - this.worker.postMessage({ - type: "heartbeat", - }); - } - playerProfile(playerID: number): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index a8d30e9b1..23a5ead5d 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -9,7 +9,6 @@ import { GameUpdateViewData } from "../game/GameUpdates"; import { ClientID, GameStartInfo, Turn } from "../Schemas"; export type WorkerMessageType = - | "heartbeat" | "init" | "initialized" | "turn" @@ -31,15 +30,13 @@ interface BaseWorkerMessage { id?: string; } -export interface HeartbeatMessage extends BaseWorkerMessage { - type: "heartbeat"; -} - // Messages from main thread to worker export interface InitMessage extends BaseWorkerMessage { type: "init"; gameStartInfo: GameStartInfo; clientID: ClientID; + sharedTileRingHeader?: SharedArrayBuffer; + sharedTileRingData?: SharedArrayBuffer; } export interface TurnMessage extends BaseWorkerMessage { @@ -114,7 +111,6 @@ export interface TransportShipSpawnResultMessage extends BaseWorkerMessage { // Union types for type safety export type MainThreadMessage = - | HeartbeatMessage | InitMessage | TurnMessage | PlayerActionsMessage