From f6df2ccb18c361c995c7743f094e38e87fdf8258 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:53:31 +0100 Subject: [PATCH] Implement tile state change handling in Game and Worker components - Added an optional `onTileStateChanged` hook in the Game interface - Refactored GameImpl to utilize the new hook for reporting tile state changes instead of emitting GameUpdateType.Tile updates. - Updated Worker.worker.ts to integrate the new tile state change handling --- src/client/graphics/layers/TerritoryLayer.ts | 4 +-- src/core/game/Game.ts | 6 ++++ src/core/game/GameImpl.ts | 31 ++++++++++++-------- src/core/worker/Worker.worker.ts | 12 ++------ 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index d54791fff..e6aebfa9c 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -108,8 +108,8 @@ export class TerritoryLayer implements Layer { this.syncPaletteMaybe(now); // Renderer tick and dirty-tile marking are driven in the worker from - // simulation-derived tile updates (tileUpdateSink). The main thread only - // drives render frames + view transforms. + // simulation-derived tile mutations (onTileStateChanged). The main thread + // only drives render frames + view transforms. FrameProfiler.end("TerritoryLayer:tick", tickProfile); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index f291e1121..1e0069742 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -733,6 +733,12 @@ export interface Game extends GameMap { callback: (neighbor: TileRef) => void, ): void; + /** + * Optional hook for tile state changes. When set, tile mutations should call + * this instead of emitting GameUpdateType.Tile updates. + */ + onTileStateChanged?: (tile: TileRef) => void; + // Player Management player(id: PlayerID): Player; players(): Player[]; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 4cca8f0e7..01c745744 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -84,6 +84,7 @@ export class GameImpl implements Game { private updates: GameUpdates = createGameUpdatesMap(); private unitGrid: UnitGrid; + public onTileStateChanged?: (tile: TileRef) => void; private playerTeams: Team[]; private botTeam: Team = ColoredTeams.Bot; @@ -234,6 +235,17 @@ export class GameImpl implements Game { (this.updates[update.type] as GameUpdate[]).push(update); } + private reportTileStateChanged(tile: TileRef): void { + if (this.onTileStateChanged) { + this.onTileStateChanged(tile); + return; + } + this.addUpdate({ + type: GameUpdateType.Tile, + update: this.toTileUpdate(tile), + }); + } + nextUnitID(): number { const old = this._nextUnitID; this._nextUnitID++; @@ -248,10 +260,7 @@ export class GameImpl implements Game { return; } this._map.setFallout(tile, value); - this.addUpdate({ - type: GameUpdateType.Tile, - update: this.toTileUpdate(tile), - }); + this.reportTileStateChanged(tile); } units(...types: UnitType[]): Unit[] { @@ -594,10 +603,7 @@ export class GameImpl implements Game { owner._lastTileChange = this._ticks; this.updateBorders(tile); this._map.setFallout(tile, false); - this.addUpdate({ - type: GameUpdateType.Tile, - update: this.toTileUpdate(tile), - }); + this.reportTileStateChanged(tile); } relinquish(tile: TileRef) { @@ -615,10 +621,7 @@ export class GameImpl implements Game { this._map.setOwnerID(tile, 0); this.updateBorders(tile); - this.addUpdate({ - type: GameUpdateType.Tile, - update: this.toTileUpdate(tile), - }); + this.reportTileStateChanged(tile); } private updateBorders(tile: TileRef) { @@ -951,7 +954,8 @@ export class GameImpl implements Game { return this._map.hasOwner(ref); } setOwnerID(ref: TileRef, playerId: number): void { - return this._map.setOwnerID(ref, playerId); + this._map.setOwnerID(ref, playerId); + this.reportTileStateChanged(ref); } hasFallout(ref: TileRef): boolean { return this._map.hasFallout(ref); @@ -961,6 +965,7 @@ export class GameImpl implements Game { } setDefended(ref: TileRef, value: boolean): void { this._map.setDefended(ref, value); + this.reportTileStateChanged(ref); } isBorder(ref: TileRef): boolean { return this._map.isBorder(ref); diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 67d90712b..e958392e8 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -4,7 +4,6 @@ import { PastelTheme } from "../configuration/PastelTheme"; import { PastelThemeDark } from "../configuration/PastelThemeDark"; import { FetchGameMapLoader } from "../game/FetchGameMapLoader"; import { PlayerID } from "../game/Game"; -import { TileRef } from "../game/GameMap"; import { AllianceExpiredUpdate, AllianceRequestReplyUpdate, @@ -575,9 +574,9 @@ ctx.addEventListener("message", async (e: MessageEvent) => { // Capacity is bounded; on overflow we fall back to markAllDirty(). dirtyTiles = new DirtyTileQueue(numTiles, Math.max(4096, numTiles)); dirtyTilesOverflow = false; - renderTileState = new Uint16Array(gr.game.tileStateView()); + renderTileState = gr.game.tileStateView(); - gr.tileUpdateSink = (packedUpdate) => { + gr.game.onTileStateChanged = (tile) => { if (!dirtyTiles) { return; } @@ -585,11 +584,6 @@ ctx.addEventListener("message", async (e: MessageEvent) => { return; } - const tile = Number(packedUpdate >> 16n) as TileRef; - const state = Number(packedUpdate & 0xffffn); - if (renderTileState) { - renderTileState[tile] = state; - } const mark = (t: any) => { if (!dirtyTiles!.mark(t)) { dirtyTilesOverflow = true; @@ -798,7 +792,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { ? new WorkerCanvas2DRenderer() : new WorkerTerritoryRenderer(); - renderTileState ??= new Uint16Array(gr.game.tileStateView()); + renderTileState ??= gr.game.tileStateView(); await renderer.init( message.offscreenCanvas, gr,