mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:30:43 +00:00
build worker messaging system
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Executor } from "../core/execution/ExecutionManager";
|
||||
import { Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, UnitEvent, Tile, PlayerType, GameMap, Difficulty, GameType } from "../core/game/Game";
|
||||
import { Cell, MutableGame, PlayerID, MutablePlayer, TileEvent, Player, Game, UnitEvent, Tile, PlayerType, GameMap, Difficulty, GameType } from "../core/game/Game";
|
||||
import { createGame } from "../core/game/GameImpl";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
|
||||
@@ -74,10 +74,10 @@ export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: Gam
|
||||
const config = getConfig(gameConfig)
|
||||
|
||||
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
|
||||
const gameView = new GameView(config, terrainMap.map)
|
||||
|
||||
const worker = new WorkerClient(lobbyConfig.gameID, gameConfig)
|
||||
await worker.initialize()
|
||||
const gameView = new GameView(worker, config, terrainMap.map)
|
||||
|
||||
|
||||
consolex.log('going to init path finder')
|
||||
consolex.log('inited path finder')
|
||||
@@ -118,7 +118,6 @@ export class ClientGameRunner {
|
||||
public start() {
|
||||
consolex.log('starting client game')
|
||||
this.isActive = true
|
||||
this.eventBus.on(PlayerEvent, (e) => this.playerEvent(e))
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e))
|
||||
|
||||
this.renderer.initialize()
|
||||
@@ -168,13 +167,6 @@ export class ClientGameRunner {
|
||||
this.transport.leaveGame()
|
||||
}
|
||||
|
||||
private playerEvent(event: PlayerEvent) {
|
||||
if (event.player.clientID() == this.clientID) {
|
||||
consolex.log('setting name')
|
||||
this.myPlayer = event.player
|
||||
}
|
||||
}
|
||||
|
||||
private inputEvent(event: MouseUpEvent) {
|
||||
if (!this.isActive) {
|
||||
return
|
||||
@@ -193,7 +185,10 @@ export class ClientGameRunner {
|
||||
return
|
||||
}
|
||||
if (this.myPlayer == null) {
|
||||
return
|
||||
this.myPlayer = this.gameView.playerByClientID(this.clientID)
|
||||
if (this.myPlayer == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const owner = tile.owner()
|
||||
|
||||
@@ -29,7 +29,7 @@ export class GameRunner {
|
||||
private playerToName = new Map<PlayerID, NameViewData>()
|
||||
|
||||
constructor(
|
||||
private game: MutableGame,
|
||||
public game: MutableGame,
|
||||
private eventBus: EventBus,
|
||||
private execManager: Executor,
|
||||
private callBack: (gu: GameUpdateViewData) => void
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Config } from "./configuration/Config";
|
||||
import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage, Execution, ExecutionView, Game, Gold, MutableTile, Nation, PlayerID, PlayerInfo, PlayerType, Relation, TerrainMap, TerrainTile, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "./game/Game";
|
||||
import { ClientID } from "./Schemas";
|
||||
import { TerraNulliusImpl } from './game/TerraNulliusImpl';
|
||||
import { WorkerClient } from './worker/WorkerClient';
|
||||
|
||||
export interface ViewSerializable<T> {
|
||||
toViewData(): T;
|
||||
@@ -254,7 +255,7 @@ export class GameView {
|
||||
private tiles: TileView[][] = []
|
||||
private smallIDToID = new Map<number, PlayerID>()
|
||||
|
||||
constructor(private _config: Config, private _terrainMap: TerrainMap) {
|
||||
constructor(private worker: WorkerClient, private _config: Config, private _terrainMap: TerrainMap) {
|
||||
// Initialize the 2D array
|
||||
this.tiles = Array(_terrainMap.width()).fill(null).map(() => Array(_terrainMap.height()).fill(null));
|
||||
|
||||
|
||||
@@ -361,10 +361,6 @@ export class TileEvent implements GameEvent {
|
||||
constructor(public readonly tile: Tile, public readonly borderOnlyChange: boolean = false) { }
|
||||
}
|
||||
|
||||
export class PlayerEvent implements GameEvent {
|
||||
constructor(public readonly player: Player) { }
|
||||
}
|
||||
|
||||
export class UnitEvent implements GameEvent {
|
||||
constructor(public readonly unit: Unit, public oldTile: Tile) { }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { info } from "console";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { EventBus } from "../EventBus";
|
||||
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus, MutableTile } from "./Game";
|
||||
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus, MutableTile } from "./Game";
|
||||
import { TerrainMapImpl } from "./TerrainMapLoader";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
@@ -250,7 +250,6 @@ export class GameImpl implements MutableGame {
|
||||
let player = new PlayerImpl(this, this.nextID, playerInfo, manpower)
|
||||
this.nextID++
|
||||
this._players.set(playerInfo.id, player)
|
||||
this.eventBus.emit(new PlayerEvent(player))
|
||||
return player
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,94 @@
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { GameUpdateViewData } from "../GameView";
|
||||
import {
|
||||
MainThreadMessage,
|
||||
WorkerMessage,
|
||||
InitializedMessage,
|
||||
SharesBorderResultMessage
|
||||
} from './WorkerMessages';
|
||||
|
||||
let gameRunner: Promise<GameRunner> = null
|
||||
|
||||
const ctx: Worker = self as any;
|
||||
let gameRunner: Promise<GameRunner> | null = null;
|
||||
|
||||
function gameUpdate(gu: GameUpdateViewData) {
|
||||
self.postMessage({
|
||||
sendMessage({
|
||||
type: "game_update",
|
||||
gameUpdate: gu
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
self.onmessage = (e) => {
|
||||
switch (e.data.type) {
|
||||
function sendMessage(message: WorkerMessage) {
|
||||
ctx.postMessage(message);
|
||||
}
|
||||
|
||||
ctx.addEventListener('message', async (e: MessageEvent<MainThreadMessage>) => {
|
||||
const message = e.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'init':
|
||||
gameRunner = createGameRunner(e.data.gameID, e.data.gameConfig, gameUpdate).then(gr => {
|
||||
self.postMessage({
|
||||
type: 'initialized'
|
||||
try {
|
||||
gameRunner = createGameRunner(
|
||||
message.gameID,
|
||||
message.gameConfig,
|
||||
gameUpdate
|
||||
).then(gr => {
|
||||
sendMessage({
|
||||
type: 'initialized',
|
||||
id: message.id
|
||||
} as InitializedMessage);
|
||||
return gr;
|
||||
});
|
||||
return gr;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize game runner:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'turn':
|
||||
gameRunner.then(gr => gr.addTurn(e.data.turn))
|
||||
if (!gameRunner) {
|
||||
throw new Error('Game runner not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const gr = await gameRunner;
|
||||
await gr.addTurn(message.turn);
|
||||
} catch (error) {
|
||||
console.error('Failed to process turn:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'shares_border':
|
||||
if (!gameRunner) {
|
||||
throw new Error('Game runner not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const game = (await gameRunner).game
|
||||
const result = game.player(message.player1)
|
||||
.sharesBorderWith(game.player(message.player2))
|
||||
|
||||
sendMessage({
|
||||
type: 'shares_border_result',
|
||||
id: message.id,
|
||||
result
|
||||
} as SharesBorderResultMessage);
|
||||
} catch (error) {
|
||||
console.error('Failed to check borders:', error);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown message :', message);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Error handling
|
||||
ctx.addEventListener('error', (error) => {
|
||||
console.error('Worker error:', error);
|
||||
});
|
||||
|
||||
ctx.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection in worker:', event);
|
||||
});
|
||||
@@ -1,40 +1,69 @@
|
||||
import { consolex } from "../Consolex";
|
||||
import { Cell, Game, GameMap, TerrainTile, TerrainType, Tile } from "../game/Game";
|
||||
import { PlayerID } from "../game/Game";
|
||||
import { GameUpdateViewData } from "../GameView";
|
||||
import { AStar, PathFindResultType } from "../pathfinding/AStar";
|
||||
import { MiniAStar } from "../pathfinding/MiniAStar";
|
||||
import { GameConfig, GameID, Turn } from "../Schemas";
|
||||
import { generateID } from "../Util";
|
||||
|
||||
import { WorkerMessage } from "./WorkerMessages";
|
||||
|
||||
export class WorkerClient {
|
||||
private worker: Worker;
|
||||
private isInitialized = false;
|
||||
private messageHandlers: Map<string, (message: WorkerMessage) => void>;
|
||||
private gameUpdateCallback?: (update: GameUpdateViewData) => void;
|
||||
|
||||
constructor(private gameID: GameID, private gameConfig: GameConfig) {
|
||||
// Create a new worker using webpack worker-loader
|
||||
// The import.meta.url ensures webpack can properly bundle the worker
|
||||
this.worker = new Worker(new URL('./Worker.worker.ts', import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
|
||||
// Set up global message handler
|
||||
this.worker.addEventListener('message', this.handleWorkerMessage.bind(this));
|
||||
}
|
||||
|
||||
private handleWorkerMessage(event: MessageEvent<WorkerMessage>) {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'game_update':
|
||||
if (this.gameUpdateCallback && message.gameUpdate) {
|
||||
this.gameUpdateCallback(message.gameUpdate);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'initialized':
|
||||
case 'shares_border_result':
|
||||
if (message.id && this.messageHandlers.has(message.id)) {
|
||||
const handler = this.messageHandlers.get(message.id)!;
|
||||
handler(message);
|
||||
this.messageHandlers.delete(message.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = generateID()
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === 'initialized') {
|
||||
this.isInitialized = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'init',
|
||||
id: messageId,
|
||||
gameID: this.gameID,
|
||||
gameConfig: this.gameConfig
|
||||
});
|
||||
|
||||
const handler = (e: MessageEvent) => {
|
||||
if (e.data.type === 'initialized') {
|
||||
this.isInitialized = true;
|
||||
this.worker.removeEventListener('message', handler)
|
||||
resolve();
|
||||
return
|
||||
// Add timeout for initialization
|
||||
setTimeout(() => {
|
||||
if (!this.isInitialized) {
|
||||
this.messageHandlers.delete(messageId);
|
||||
reject(new Error('Worker initialization timeout'));
|
||||
}
|
||||
};
|
||||
|
||||
this.worker.addEventListener('message', handler);
|
||||
}, 5000); // 5 second timeout
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,23 +71,48 @@ export class WorkerClient {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Failed to initialize pathfinder');
|
||||
}
|
||||
const handler = (e: MessageEvent) => {
|
||||
if (e.data.type == "game_update") {
|
||||
gameUpdate(e.data.gameUpdate)
|
||||
}
|
||||
}
|
||||
this.worker.addEventListener('message', handler);
|
||||
this.gameUpdateCallback = gameUpdate;
|
||||
}
|
||||
|
||||
sendTurn(turn: Turn) {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Worker not initialized');
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "turn",
|
||||
turn: turn
|
||||
})
|
||||
type: 'turn',
|
||||
turn
|
||||
});
|
||||
}
|
||||
|
||||
sharesBorderWith(p1: PlayerID, p2: PlayerID): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error('Worker not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = generateID()
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (message.type === 'shares_border_result' && message.result !== undefined) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'shares_border',
|
||||
id: messageId,
|
||||
player1: p1,
|
||||
player2: p2
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
cleanup() {
|
||||
this.worker.terminate();
|
||||
this.messageHandlers.clear();
|
||||
this.gameUpdateCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { GameUpdateViewData } from "../GameView";
|
||||
import { GameConfig, GameID, Turn } from "../Schemas";
|
||||
import { PlayerID } from "../game/Game";
|
||||
|
||||
export type WorkerMessageType =
|
||||
| 'init'
|
||||
| 'initialized'
|
||||
| 'turn'
|
||||
| 'game_update'
|
||||
| 'shares_border'
|
||||
| 'shares_border_result';
|
||||
|
||||
// Base interface for all messages
|
||||
interface BaseWorkerMessage {
|
||||
type: WorkerMessageType;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
// Messages from main thread to worker
|
||||
export interface InitMessage extends BaseWorkerMessage {
|
||||
type: 'init';
|
||||
gameID: GameID;
|
||||
gameConfig: GameConfig;
|
||||
}
|
||||
|
||||
export interface TurnMessage extends BaseWorkerMessage {
|
||||
type: 'turn';
|
||||
turn: Turn;
|
||||
}
|
||||
|
||||
export interface SharesBorderMessage extends BaseWorkerMessage {
|
||||
type: 'shares_border';
|
||||
player1: PlayerID;
|
||||
player2: PlayerID;
|
||||
}
|
||||
|
||||
// Messages from worker to main thread
|
||||
export interface InitializedMessage extends BaseWorkerMessage {
|
||||
type: 'initialized';
|
||||
}
|
||||
|
||||
export interface GameUpdateMessage extends BaseWorkerMessage {
|
||||
type: 'game_update';
|
||||
gameUpdate: GameUpdateViewData;
|
||||
}
|
||||
|
||||
export interface SharesBorderResultMessage extends BaseWorkerMessage {
|
||||
type: 'shares_border_result';
|
||||
result: boolean;
|
||||
}
|
||||
|
||||
// Union types for type safety
|
||||
export type MainThreadMessage = InitMessage | TurnMessage | SharesBorderMessage;
|
||||
export type WorkerMessage = InitializedMessage | GameUpdateMessage | SharesBorderResultMessage;
|
||||
Reference in New Issue
Block a user