diff --git a/@sammo/server/src/GameEngine.ts b/@sammo/server/src/GameEngine.ts index 25e1787..e1f3588 100644 --- a/@sammo/server/src/GameEngine.ts +++ b/@sammo/server/src/GameEngine.ts @@ -4,6 +4,7 @@ import { ResourceController } from "./ResourceController.js"; import { entriesWithType } from "@sammo/util/converter"; import db from "./connectDB.js"; import type { Mongoose } from "mongoose"; +import type { GameEngineMsg } from "./GameEngineDefs.js"; export interface ActionRequest { } @@ -45,7 +46,10 @@ export class GameEngine { private rsc: ResourceController - private constructor(db: Mongoose) { + private constructor( + db: Mongoose, + private postStatus: (msg: GameEngineMsg) => void, + ) { this.rsc = new ResourceController(db); } @@ -122,7 +126,13 @@ export class GameEngine { if (processedGeneralCount > 0) { await prevSaveWaiter; - prevSaveWaiter = this.rsc.saveAll(); + const lastExecuted = this.rsc.gameEnv().raw.lastExecuted; + prevSaveWaiter = this.rsc.saveAll().then(()=>{ + this.postStatus({ + type: 'update', + lastExecuted: lastExecuted.toISOString() + }); + }); } //서버 이벤트 처리 @@ -172,12 +182,12 @@ export class GameEngine { private gameLoopPromise: Promise | null = null; - public static async initInstance(): Promise { + public static async initInstance(updateHandler: (msg: GameEngineMsg)=>void): Promise { if (GameEngine.instance) { throw new Error('GameEngine is already initialized'); } - GameEngine.instance = new GameEngine(await db); + GameEngine.instance = new GameEngine(await db, updateHandler); return GameEngine.instance; } diff --git a/@sammo/server/src/GameEngineController.ts b/@sammo/server/src/GameEngineController.ts new file mode 100644 index 0000000..dcb93ba --- /dev/null +++ b/@sammo/server/src/GameEngineController.ts @@ -0,0 +1,103 @@ +import { randomUUID } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { MessageChannel, Worker } from "node:worker_threads"; +import * as nodePath from "node:path"; +import { delay, must } from "@sammo/util"; +import type { GameEngineMsg, GameEngineRPCDefs } from "./GameEngineDefs.js"; +import { RPCClient } from "@sammo/server_util"; + + +const __filename = fileURLToPath(new URL(import.meta.url)); +const __dirname = nodePath.dirname(__filename); +const workerPath = nodePath.resolve(__dirname, 'GameEngineWorker'); + +class GameEngineController { + private constructor() { } + + private static _instance: GameEngineController; + static get instance(): GameEngineController { + if (!GameEngineController._instance) { + GameEngineController._instance = new GameEngineController(); + } + return GameEngineController._instance; + } + + private engineGUID?: string = undefined; + private _lastExecuted?: Date = undefined; + private stopWaiter: Promise = Promise.resolve(); + private stopWaiterResolve: () => void = () => { }; + private rpcClient?: RPCClient = undefined; + + get isRunning(): boolean { + return this.engineGUID !== undefined; + } + + get lastExecuted(): Date | undefined { + return this._lastExecuted; + } + + async start(): Promise { + if (this.engineGUID) { + throw new Error('GameEngine is already running'); + } + + const guid = randomUUID(); + + const { port1: rpcServer, port2: rpcClient } = new MessageChannel(); + + const worker = new Worker(workerPath, { + workerData: { + guid + } + }); + this.stopWaiter = new Promise((resolve) => { + this.stopWaiterResolve = resolve; + }); + + worker.postMessage({ + type: 'init', + port: rpcServer + }, [rpcServer]); + + await delay(0); + + this.engineGUID = guid; + + worker.once('exit', () => { + this.engineGUID = undefined; + this._lastExecuted = undefined; + this.rpcClient = undefined; + this.stopWaiterResolve(); + }); + + worker.on('message', (msg: GameEngineMsg) => { + if (msg.type === 'update') { + this._lastExecuted = new Date(msg.lastExecuted); + } + }); + + this.rpcClient = new RPCClient(rpcClient, 'GameEngineController') + } + + async stop(): Promise { + if (!this.isRunning) { + return false; + } + await must(this.rpcClient).callFunction('stop', undefined); + await this.stopWaiter; + return true; + } + + async restart(): Promise { + await this.stop(); + await this.start(); + } + + async call(name: T, arg: Parameters[0]): Promise> { + return must(this.rpcClient).callFunction(name, arg); + } +} + +export function getGameEngineController(): GameEngineController { + return GameEngineController.instance; +} \ No newline at end of file diff --git a/@sammo/server/src/GameEngineDefs.ts b/@sammo/server/src/GameEngineDefs.ts new file mode 100644 index 0000000..fc36fea --- /dev/null +++ b/@sammo/server/src/GameEngineDefs.ts @@ -0,0 +1,18 @@ +import type { RPCLists } from "@sammo/server_util"; +import type { ActionRequest, ActionResult } from "./GameEngine.js"; + + + +export type GameEngineRPCDefs = { + stop: ()=>Promise, + pushAPIAction: (action: ActionRequest)=>Promise, + pushServerAction: (action: ActionRequest)=>Promise, +}; + + +type WorkerUpdate = { + type: 'update', + lastExecuted: string, +} + +export type GameEngineMsg = WorkerUpdate; \ No newline at end of file diff --git a/@sammo/server/src/GameEngineWorker.ts b/@sammo/server/src/GameEngineWorker.ts new file mode 100644 index 0000000..e69de29