mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:40:43 +00:00
sab
This commit is contained in:
+337
-27
@@ -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,18 @@ 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,
|
||||
TILE_RING_HEADER_OVERFLOW,
|
||||
} from "../core/worker/SharedTileRing";
|
||||
import { WorkerClient } from "../core/worker/WorkerClient";
|
||||
import {
|
||||
AutoUpgradeEvent,
|
||||
BacklogStatusEvent,
|
||||
DoBoatAttackEvent,
|
||||
DoGroundAttackEvent,
|
||||
InputHandler,
|
||||
@@ -171,9 +180,44 @@ async function createClientGame(
|
||||
mapLoader,
|
||||
);
|
||||
}
|
||||
|
||||
let sharedTileRingBuffers: SharedTileRingBuffers | undefined;
|
||||
let sharedTileRingViews: SharedTileRingViews | null = null;
|
||||
let sharedDirtyBuffer: SharedArrayBuffer | undefined;
|
||||
let sharedDirtyFlags: Uint8Array | 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;
|
||||
const sharedStateBuffer =
|
||||
canUseSharedBuffers && gameMap.sharedStateBuffer
|
||||
? gameMap.sharedStateBuffer
|
||||
: undefined;
|
||||
const usesSharedTileState = !!sharedStateBuffer;
|
||||
|
||||
if (canUseSharedBuffers) {
|
||||
const numTiles = gameMap.gameMap.width() * gameMap.gameMap.height();
|
||||
// Ring capacity scales with world size: at most one entry per tile.
|
||||
const TILE_RING_CAPACITY = numTiles;
|
||||
sharedTileRingBuffers = createSharedTileRingBuffers(
|
||||
TILE_RING_CAPACITY,
|
||||
numTiles,
|
||||
);
|
||||
sharedTileRingViews = createSharedTileRingViews(sharedTileRingBuffers);
|
||||
sharedDirtyBuffer = sharedTileRingBuffers.dirty;
|
||||
sharedDirtyFlags = sharedTileRingViews.dirtyFlags;
|
||||
}
|
||||
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameStartInfo,
|
||||
lobbyConfig.clientID,
|
||||
sharedTileRingBuffers,
|
||||
sharedStateBuffer,
|
||||
sharedDirtyBuffer,
|
||||
);
|
||||
await worker.initialize();
|
||||
const gameView = new GameView(
|
||||
@@ -183,6 +227,7 @@ async function createClientGame(
|
||||
lobbyConfig.clientID,
|
||||
lobbyConfig.gameStartInfo.gameID,
|
||||
lobbyConfig.gameStartInfo.players,
|
||||
usesSharedTileState,
|
||||
);
|
||||
|
||||
const canvas = createCanvas();
|
||||
@@ -200,6 +245,8 @@ async function createClientGame(
|
||||
transport,
|
||||
worker,
|
||||
gameView,
|
||||
sharedTileRingViews,
|
||||
sharedDirtyFlags,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,6 +265,22 @@ 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;
|
||||
private dirtyFlags: Uint8Array | null;
|
||||
|
||||
constructor(
|
||||
private lobby: LobbyConfig,
|
||||
private eventBus: EventBus,
|
||||
@@ -226,8 +289,12 @@ export class ClientGameRunner {
|
||||
private transport: Transport,
|
||||
private worker: WorkerClient,
|
||||
private gameView: GameView,
|
||||
tileRingViews: SharedTileRingViews | null,
|
||||
dirtyFlags: Uint8Array | null,
|
||||
) {
|
||||
this.lastMessageTime = Date.now();
|
||||
this.tileRingViews = tileRingViews;
|
||||
this.dirtyFlags = dirtyFlags;
|
||||
}
|
||||
|
||||
private saveGame(update: WinUpdate) {
|
||||
@@ -302,33 +369,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!");
|
||||
@@ -373,6 +416,10 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
for (const turn of message.turns) {
|
||||
this.serverTurnHighWater = Math.max(
|
||||
this.serverTurnHighWater,
|
||||
turn.turnNumber,
|
||||
);
|
||||
if (turn.turnNumber < this.turnsSeen) {
|
||||
continue;
|
||||
}
|
||||
@@ -425,6 +472,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}`,
|
||||
@@ -455,6 +507,264 @@ 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 { gameUpdate: combinedGu, tileMetrics } =
|
||||
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,
|
||||
tileMetrics.count,
|
||||
tileMetrics.utilization,
|
||||
tileMetrics.overflow,
|
||||
tileMetrics.drainTime,
|
||||
),
|
||||
);
|
||||
|
||||
// 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[]): {
|
||||
gameUpdate: GameUpdateViewData | null;
|
||||
tileMetrics: {
|
||||
count: number;
|
||||
utilization: number;
|
||||
overflow: number;
|
||||
drainTime: number;
|
||||
};
|
||||
} {
|
||||
if (batch.length === 0) {
|
||||
return {
|
||||
gameUpdate: null,
|
||||
tileMetrics: {
|
||||
count: 0,
|
||||
utilization: 0,
|
||||
overflow: 0,
|
||||
drainTime: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let tileMetrics = {
|
||||
count: 0,
|
||||
utilization: 0,
|
||||
overflow: 0,
|
||||
drainTime: 0,
|
||||
};
|
||||
|
||||
if (this.tileRingViews) {
|
||||
const MAX_TILE_UPDATES_PER_RENDER = 100000;
|
||||
const tileRefs: TileRef[] = [];
|
||||
const drainStart = performance.now();
|
||||
drainTileUpdates(
|
||||
this.tileRingViews,
|
||||
MAX_TILE_UPDATES_PER_RENDER,
|
||||
tileRefs,
|
||||
);
|
||||
const drainTime = performance.now() - drainStart;
|
||||
|
||||
// Deduplicate tile refs for this render slice
|
||||
const uniqueTiles = new Set<TileRef>();
|
||||
for (const ref of tileRefs) {
|
||||
uniqueTiles.add(ref);
|
||||
}
|
||||
|
||||
// Calculate ring buffer utilization and overflow using dynamic capacity
|
||||
const TILE_RING_CAPACITY = this.tileRingViews.capacity;
|
||||
const utilization = (uniqueTiles.size / TILE_RING_CAPACITY) * 100;
|
||||
const overflow = Atomics.load(
|
||||
this.tileRingViews.header,
|
||||
TILE_RING_HEADER_OVERFLOW,
|
||||
);
|
||||
|
||||
tileMetrics = {
|
||||
count: uniqueTiles.size,
|
||||
utilization,
|
||||
overflow,
|
||||
drainTime,
|
||||
};
|
||||
|
||||
for (const ref of uniqueTiles) {
|
||||
if (this.dirtyFlags) {
|
||||
Atomics.store(this.dirtyFlags, ref, 0);
|
||||
}
|
||||
combinedPackedTileUpdates.push(BigInt(ref));
|
||||
}
|
||||
} else {
|
||||
// Non-SAB mode: count tile updates from batch
|
||||
let totalTileUpdates = 0;
|
||||
for (const gu of batch) {
|
||||
totalTileUpdates += gu.packedTileUpdates.length;
|
||||
}
|
||||
tileMetrics.count = totalTileUpdates;
|
||||
}
|
||||
|
||||
return {
|
||||
gameUpdate: {
|
||||
tick: last.tick,
|
||||
updates: combinedUpdates,
|
||||
packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates),
|
||||
playerNameViewData: last.playerNameViewData,
|
||||
tickExecutionDuration: last.tickExecutionDuration,
|
||||
},
|
||||
tileMetrics,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -129,6 +129,26 @@ 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,
|
||||
// Tile update metrics
|
||||
public readonly tileUpdatesCount?: number,
|
||||
public readonly ringBufferUtilization?: number,
|
||||
public readonly ringBufferOverflows?: number,
|
||||
public readonly ringDrainTime?: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BacklogStatusEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly backlogTurns: number,
|
||||
public readonly backlogGrowing: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -229,7 +229,18 @@ 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,
|
||||
event.tileUpdatesCount,
|
||||
event.ringBufferUtilization,
|
||||
event.ringBufferOverflows,
|
||||
event.ringDrainTime,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,6 +323,14 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
this.layerStats.clear();
|
||||
this.layerBreakdown = [];
|
||||
|
||||
// reset tile metrics
|
||||
this.tileUpdatesPerRender = 0;
|
||||
this.tileUpdatesPeak = 0;
|
||||
this.ringBufferUtilization = 0;
|
||||
this.ringBufferOverflows = 0;
|
||||
this.ringDrainTime = 0;
|
||||
this.totalTilesUpdated = 0;
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
@@ -418,7 +437,48 @@ 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;
|
||||
|
||||
@state()
|
||||
private tileUpdatesPerRender: number = 0;
|
||||
|
||||
@state()
|
||||
private tileUpdatesPeak: number = 0;
|
||||
|
||||
@state()
|
||||
private ringBufferUtilization: number = 0;
|
||||
|
||||
@state()
|
||||
private ringBufferOverflows: number = 0;
|
||||
|
||||
@state()
|
||||
private ringDrainTime: number = 0;
|
||||
|
||||
@state()
|
||||
private totalTilesUpdated: number = 0;
|
||||
|
||||
updateTickMetrics(
|
||||
tickExecutionDuration?: number,
|
||||
tickDelay?: number,
|
||||
backlogTurns?: number,
|
||||
ticksPerRender?: number,
|
||||
workerTicksPerSecond?: number,
|
||||
renderTicksPerSecond?: number,
|
||||
tileUpdatesCount?: number,
|
||||
ringBufferUtilization?: number,
|
||||
ringBufferOverflows?: number,
|
||||
ringDrainTime?: number,
|
||||
) {
|
||||
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
|
||||
|
||||
// Update tick execution duration stats
|
||||
@@ -455,6 +515,42 @@ 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;
|
||||
}
|
||||
|
||||
if (tileUpdatesCount !== undefined) {
|
||||
this.tileUpdatesPerRender = tileUpdatesCount;
|
||||
this.tileUpdatesPeak = Math.max(this.tileUpdatesPeak, tileUpdatesCount);
|
||||
this.totalTilesUpdated += tileUpdatesCount;
|
||||
}
|
||||
|
||||
if (ringBufferUtilization !== undefined) {
|
||||
this.ringBufferUtilization =
|
||||
Math.round(ringBufferUtilization * 100) / 100;
|
||||
}
|
||||
|
||||
if (ringBufferOverflows !== undefined && ringBufferOverflows !== 0) {
|
||||
// Remember that an overflow has occurred at least once this run.
|
||||
this.ringBufferOverflows = 1;
|
||||
}
|
||||
|
||||
if (ringDrainTime !== undefined) {
|
||||
this.ringDrainTime = Math.round(ringDrainTime * 100) / 100;
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -485,6 +581,14 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
executionSamples: [...this.tickExecutionTimes],
|
||||
delaySamples: [...this.tickDelayTimes],
|
||||
},
|
||||
tiles: {
|
||||
updatesPerRender: this.tileUpdatesPerRender,
|
||||
peakUpdates: this.tileUpdatesPeak,
|
||||
ringBufferUtilization: this.ringBufferUtilization,
|
||||
ringBufferOverflows: this.ringBufferOverflows,
|
||||
ringDrainTimeMs: this.ringDrainTime,
|
||||
totalTilesUpdated: this.totalTilesUpdated,
|
||||
},
|
||||
layers: this.layerBreakdown.map((layer) => ({ ...layer })),
|
||||
};
|
||||
}
|
||||
@@ -600,6 +704,37 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
|
||||
(max: <span>${this.tickDelayMax}ms</span>)
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Worker ticks/s:
|
||||
<span>${this.workerTicksPerSecond.toFixed(1)}</span>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Render ticks/s:
|
||||
<span>${this.renderTicksPerSecond.toFixed(1)}</span>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Ticks per render:
|
||||
<span>${this.ticksPerRender}</span>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Backlog turns:
|
||||
<span>${this.backlogTurns}</span>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Tile updates/render:
|
||||
<span>${this.tileUpdatesPerRender}</span>
|
||||
(peak: <span>${this.tileUpdatesPeak}</span>)
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Ring buffer:
|
||||
<span>${this.ringBufferUtilization}%</span>
|
||||
(${this.totalTilesUpdated} total, ${this.ringBufferOverflows}
|
||||
overflows)
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Ring drain time:
|
||||
<span>${this.ringDrainTime.toFixed(2)}ms</span>
|
||||
</div>
|
||||
${this.layerBreakdown.length
|
||||
? html`<div class="layers-section">
|
||||
<div class="performance-line">
|
||||
|
||||
+24
-3
@@ -37,12 +37,15 @@ export async function createGameRunner(
|
||||
clientID: ClientID,
|
||||
mapLoader: GameMapLoader,
|
||||
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
tileUpdateSink?: (tile: TileRef) => void,
|
||||
sharedStateBuffer?: SharedArrayBuffer,
|
||||
): Promise<GameRunner> {
|
||||
const config = await getConfig(gameStart.config, null);
|
||||
const gameMap = await loadGameMap(
|
||||
gameStart.config.gameMap,
|
||||
gameStart.config.gameMapSize,
|
||||
mapLoader,
|
||||
sharedStateBuffer,
|
||||
);
|
||||
const random = new PseudoRandom(simpleHash(gameStart.gameID));
|
||||
|
||||
@@ -85,6 +88,7 @@ export async function createGameRunner(
|
||||
game,
|
||||
new Executor(game, gameStart.gameID, clientID),
|
||||
callBack,
|
||||
tileUpdateSink,
|
||||
);
|
||||
gr.init();
|
||||
return gr;
|
||||
@@ -101,6 +105,7 @@ export class GameRunner {
|
||||
public game: Game,
|
||||
private execManager: Executor,
|
||||
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
private tileUpdateSink?: (tile: TileRef) => void,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -175,13 +180,25 @@ 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) {
|
||||
const tileRef = Number(u.update >> 16n) as TileRef;
|
||||
this.tileUpdateSink(tileRef);
|
||||
}
|
||||
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,
|
||||
@@ -272,4 +289,8 @@ export class GameRunner {
|
||||
}
|
||||
return player.bestTransportShipSpawn(targetTile);
|
||||
}
|
||||
|
||||
public hasPendingTurns(): boolean {
|
||||
return this.currTurn < this.turns.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export class GameMapImpl implements GameMap {
|
||||
height: number,
|
||||
terrainData: Uint8Array,
|
||||
private numLandTiles_: number,
|
||||
stateBuffer?: ArrayBufferLike,
|
||||
) {
|
||||
if (terrainData.length !== width * height) {
|
||||
throw new Error(
|
||||
@@ -89,7 +90,17 @@ export class GameMapImpl implements GameMap {
|
||||
this.width_ = width;
|
||||
this.height_ = height;
|
||||
this.terrain = terrainData;
|
||||
this.state = new Uint16Array(width * height);
|
||||
if (stateBuffer !== undefined) {
|
||||
const state = new Uint16Array(stateBuffer);
|
||||
if (state.length !== width * height) {
|
||||
throw new Error(
|
||||
`State buffer length ${state.length} doesn't match dimensions ${width}x${height}`,
|
||||
);
|
||||
}
|
||||
this.state = state;
|
||||
} else {
|
||||
this.state = new Uint16Array(width * height);
|
||||
}
|
||||
// Precompute the LUTs
|
||||
let ref = 0;
|
||||
this.refToX = new Array(width * height);
|
||||
|
||||
@@ -482,6 +482,7 @@ export class GameView implements GameMap {
|
||||
private _myClientID: ClientID,
|
||||
private _gameID: GameID,
|
||||
private humans: Player[],
|
||||
private usesSharedTileState: boolean = false,
|
||||
) {
|
||||
this._map = this._mapData.gameMap;
|
||||
this.lastUpdate = null;
|
||||
@@ -512,9 +513,16 @@ export class GameView implements GameMap {
|
||||
this.lastUpdate = gu;
|
||||
|
||||
this.updatedTiles = [];
|
||||
this.lastUpdate.packedTileUpdates.forEach((tu) => {
|
||||
this.updatedTiles.push(this.updateTile(tu));
|
||||
});
|
||||
if (this.usesSharedTileState) {
|
||||
this.lastUpdate.packedTileUpdates.forEach((tu) => {
|
||||
const tileRef = Number(tu);
|
||||
this.updatedTiles.push(tileRef);
|
||||
});
|
||||
} else {
|
||||
this.lastUpdate.packedTileUpdates.forEach((tu) => {
|
||||
this.updatedTiles.push(this.updateTile(tu));
|
||||
});
|
||||
}
|
||||
|
||||
if (gu.updates === null) {
|
||||
throw new Error("lastUpdate.updates not initialized");
|
||||
|
||||
@@ -6,6 +6,8 @@ export type TerrainMapData = {
|
||||
nations: Nation[];
|
||||
gameMap: GameMap;
|
||||
miniGameMap: GameMap;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
sharedDirtyBuffer?: SharedArrayBuffer;
|
||||
};
|
||||
|
||||
const loadedMaps = new Map<GameMapType, TerrainMapData>();
|
||||
@@ -35,15 +37,42 @@ export async function loadTerrainMap(
|
||||
map: GameMapType,
|
||||
mapSize: GameMapSize,
|
||||
terrainMapFileLoader: GameMapLoader,
|
||||
sharedStateBuffer?: SharedArrayBuffer,
|
||||
): Promise<TerrainMapData> {
|
||||
const cached = loadedMaps.get(map);
|
||||
if (cached !== undefined) return cached;
|
||||
const useCache = sharedStateBuffer === undefined;
|
||||
const canUseSharedBuffers =
|
||||
typeof SharedArrayBuffer !== "undefined" &&
|
||||
typeof Atomics !== "undefined" &&
|
||||
typeof (globalThis as any).crossOriginIsolated === "boolean" &&
|
||||
(globalThis as any).crossOriginIsolated === true;
|
||||
|
||||
// Don't use cache if we can create SharedArrayBuffer but none was provided
|
||||
const shouldUseCache = useCache && !canUseSharedBuffers;
|
||||
|
||||
if (shouldUseCache) {
|
||||
const cached = loadedMaps.get(map);
|
||||
if (cached !== undefined) return cached;
|
||||
}
|
||||
const mapFiles = terrainMapFileLoader.getMapData(map);
|
||||
const manifest = await mapFiles.manifest();
|
||||
|
||||
const stateBuffer =
|
||||
sharedStateBuffer ??
|
||||
(canUseSharedBuffers
|
||||
? new SharedArrayBuffer(
|
||||
manifest.map.width *
|
||||
manifest.map.height *
|
||||
Uint16Array.BYTES_PER_ELEMENT,
|
||||
)
|
||||
: undefined);
|
||||
|
||||
const gameMap =
|
||||
mapSize === GameMapSize.Normal
|
||||
? await genTerrainFromBin(manifest.map, await mapFiles.mapBin())
|
||||
? await genTerrainFromBin(
|
||||
manifest.map,
|
||||
await mapFiles.mapBin(),
|
||||
stateBuffer,
|
||||
)
|
||||
: await genTerrainFromBin(manifest.map4x, await mapFiles.map4xBin());
|
||||
|
||||
const miniMap =
|
||||
@@ -63,18 +92,28 @@ export async function loadTerrainMap(
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
const result: TerrainMapData = {
|
||||
nations: manifest.nations,
|
||||
gameMap: gameMap,
|
||||
miniGameMap: miniMap,
|
||||
sharedStateBuffer:
|
||||
typeof SharedArrayBuffer !== "undefined" &&
|
||||
stateBuffer instanceof SharedArrayBuffer
|
||||
? stateBuffer
|
||||
: undefined,
|
||||
sharedDirtyBuffer: undefined, // populated by consumer when needed
|
||||
};
|
||||
loadedMaps.set(map, result);
|
||||
// Only cache the result when caching is actually used (non-SAB path)
|
||||
if (shouldUseCache) {
|
||||
loadedMaps.set(map, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function genTerrainFromBin(
|
||||
mapData: MapMetadata,
|
||||
data: Uint8Array,
|
||||
stateBuffer?: ArrayBufferLike,
|
||||
): Promise<GameMap> {
|
||||
if (data.length !== mapData.width * mapData.height) {
|
||||
throw new Error(
|
||||
@@ -87,5 +126,6 @@ export async function genTerrainFromBin(
|
||||
mapData.height,
|
||||
data,
|
||||
mapData.num_land_tiles,
|
||||
stateBuffer,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { TileRef } from "../game/GameMap";
|
||||
|
||||
export interface SharedTileRingBuffers {
|
||||
header: SharedArrayBuffer;
|
||||
data: SharedArrayBuffer;
|
||||
dirty: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
export interface SharedTileRingViews {
|
||||
header: Int32Array;
|
||||
buffer: Uint32Array;
|
||||
dirtyFlags: Uint8Array;
|
||||
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,
|
||||
numTiles: number,
|
||||
): SharedTileRingBuffers {
|
||||
const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const data = new SharedArrayBuffer(capacity * Uint32Array.BYTES_PER_ELEMENT);
|
||||
const dirty = new SharedArrayBuffer(numTiles * Uint8Array.BYTES_PER_ELEMENT);
|
||||
return { header, data, dirty };
|
||||
}
|
||||
|
||||
export function createSharedTileRingViews(
|
||||
buffers: SharedTileRingBuffers,
|
||||
): SharedTileRingViews {
|
||||
const header = new Int32Array(buffers.header);
|
||||
const buffer = new Uint32Array(buffers.data);
|
||||
const dirtyFlags = new Uint8Array(buffers.dirty);
|
||||
return {
|
||||
header,
|
||||
buffer,
|
||||
dirtyFlags,
|
||||
capacity: buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function pushTileUpdate(
|
||||
views: SharedTileRingViews,
|
||||
value: TileRef,
|
||||
): 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: TileRef[],
|
||||
): 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);
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import version from "../../../resources/version.txt";
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import {
|
||||
createSharedTileRingViews,
|
||||
pushTileUpdate,
|
||||
SharedTileRingViews,
|
||||
} from "./SharedTileRing";
|
||||
import {
|
||||
AttackAveragePositionResultMessage,
|
||||
InitializedMessage,
|
||||
@@ -16,6 +22,9 @@ import {
|
||||
const ctx: Worker = self as any;
|
||||
let gameRunner: Promise<GameRunner> | null = null;
|
||||
const mapLoader = new FetchGameMapLoader(`/maps`, version);
|
||||
let isProcessingTurns = false;
|
||||
let sharedTileRing: SharedTileRingViews | null = null;
|
||||
let dirtyFlags: Uint8Array | null = null;
|
||||
|
||||
function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) {
|
||||
// skip if ErrorUpdate
|
||||
@@ -32,25 +41,68 @@ 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<MainThreadMessage>) => {
|
||||
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,
|
||||
dirty: message.sharedDirtyBuffer!,
|
||||
});
|
||||
dirtyFlags = sharedTileRing.dirtyFlags;
|
||||
} else {
|
||||
sharedTileRing = null;
|
||||
dirtyFlags = null;
|
||||
}
|
||||
|
||||
gameRunner = createGameRunner(
|
||||
message.gameStartInfo,
|
||||
message.clientID,
|
||||
mapLoader,
|
||||
gameUpdate,
|
||||
sharedTileRing && dirtyFlags
|
||||
? (tile: TileRef) => {
|
||||
if (Atomics.compareExchange(dirtyFlags!, tile, 0, 1) === 0) {
|
||||
pushTileUpdate(sharedTileRing!, tile);
|
||||
}
|
||||
}
|
||||
: sharedTileRing
|
||||
? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile)
|
||||
: undefined,
|
||||
message.sharedStateBuffer,
|
||||
).then((gr) => {
|
||||
sendMessage({
|
||||
type: "initialized",
|
||||
id: message.id,
|
||||
} as InitializedMessage);
|
||||
processPendingTurns();
|
||||
return gr;
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -67,6 +119,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
try {
|
||||
const gr = await gameRunner;
|
||||
await gr.addTurn(message.turn);
|
||||
processPendingTurns();
|
||||
} catch (error) {
|
||||
console.error("Failed to process turn:", error);
|
||||
throw error;
|
||||
|
||||
@@ -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,9 @@ export class WorkerClient {
|
||||
constructor(
|
||||
private gameStartInfo: GameStartInfo,
|
||||
private clientID: ClientID,
|
||||
private sharedTileRingBuffers?: SharedTileRingBuffers,
|
||||
private sharedStateBuffer?: SharedArrayBuffer,
|
||||
private sharedDirtyBuffer?: SharedArrayBuffer,
|
||||
) {
|
||||
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
@@ -70,6 +74,10 @@ export class WorkerClient {
|
||||
id: messageId,
|
||||
gameStartInfo: this.gameStartInfo,
|
||||
clientID: this.clientID,
|
||||
sharedTileRingHeader: this.sharedTileRingBuffers?.header,
|
||||
sharedTileRingData: this.sharedTileRingBuffers?.data,
|
||||
sharedStateBuffer: this.sharedStateBuffer,
|
||||
sharedDirtyBuffer: this.sharedDirtyBuffer,
|
||||
});
|
||||
|
||||
// Add timeout for initialization
|
||||
@@ -100,12 +108,6 @@ export class WorkerClient {
|
||||
});
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.worker.postMessage({
|
||||
type: "heartbeat",
|
||||
});
|
||||
}
|
||||
|
||||
playerProfile(playerID: number): Promise<PlayerProfile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
|
||||
@@ -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,15 @@ 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;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
sharedDirtyBuffer?: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
export interface TurnMessage extends BaseWorkerMessage {
|
||||
@@ -114,7 +113,6 @@ export interface TransportShipSpawnResultMessage extends BaseWorkerMessage {
|
||||
|
||||
// Union types for type safety
|
||||
export type MainThreadMessage =
|
||||
| HeartbeatMessage
|
||||
| InitMessage
|
||||
| TurnMessage
|
||||
| PlayerActionsMessage
|
||||
|
||||
Reference in New Issue
Block a user