import { Signal, delay, must, must_err } from "@sammo/util"; import Denque from "denque"; import { ResourceController } from "./ResourceController.js"; import { entriesWithType } from "@sammo/util/converter"; import db from "./connectDB.js"; import type { Mongoose } from "mongoose"; import type { ActionPack, GameEngineMsg } from "./GameEngineDefs.js"; import { ActionRequestList, type QueueType } from "@sammo/game_logic"; export interface ActionRequest { name: keyof ActionPack; args: Parameters[1]; } export interface ActionRequestContainer { waiterSeq: number; action: ActionRequest; } export type ActionSuccess = { success: true; value: ReturnType; } export type ActionFailure = { success: false; reason: string; } export type ActionResult = ActionSuccess | ActionFailure; export class GameEngine { private static instance: GameEngine | null = null; private actionSeq = 0; private queueWaiters = new Map void, reject: (reason: ActionFailure) => void]>(); private signal = new Signal(30000); private actionQueue = { server: new Denque(), //npc: new Denque(), api: new Denque(), } as const satisfies Record>; private rsc: ResourceController private constructor( db: Mongoose, private postStatus: (msg: GameEngineMsg) => void, ) { this.rsc = new ResourceController(db); } private continueRun = true; private prevSaveWaiter: Promise | null = null; private async processGeneralTurn() { const now = new Date(); let processedGeneralCount = 0; while (true) { //장수턴 부터 처리 const upcomingGeneral = this.rsc.popGeneralTurntimeQueue(); if (!upcomingGeneral) { break; } const generalTurntime = upcomingGeneral.raw.turntime; if (generalTurntime > now) { break; } //TODO: 턴 수행해야지! upcomingGeneral.updateTurntime(); this.rsc.insertGeneralTurntimeQueue(upcomingGeneral, true); this.rsc.gameEnv().update((env) => { env.lastExecuted = generalTurntime; }) processedGeneralCount++; } if (processedGeneralCount > 0) { await this.prevSaveWaiter; const lastExecuted = this.rsc.gameEnv().raw.lastExecuted; this.prevSaveWaiter = this.rsc.saveAll().then(() => { this.postStatus({ type: 'update', lastExecuted: lastExecuted.toISOString() }); }); } } private async processActions() { //서버 이벤트 처리 for (const [invoker, queue] of entriesWithType(this.actionQueue)) { let processEventCount = 0; let responseQueue: (() => void)[] = []; while (!queue.isEmpty()) { const action = must(queue.shift()); const actionResult = this.processAction(invoker, action.action); const [ticketResolve, ticketRejected] = must_err( this.queueWaiters.get(action.waiterSeq), Error, `GameEngine: waiterSeq(${action.waiterSeq}) not found` ); this.queueWaiters.delete(action.waiterSeq); if(actionResult.success){ responseQueue.push(() => { ticketResolve(actionResult); }); } else{ responseQueue.push(() => { ticketRejected(actionResult); }); } processEventCount++; } if (processEventCount > 0) { await this.prevSaveWaiter; this.prevSaveWaiter = null; await this.rsc.saveAll(); for (const response of responseQueue) { response(); } } } } private async gameLoop(): Promise { let generalWaitTimer: ReturnType | null = null; while (this.continueRun) { //가장 빠른 장수 do { const now = new Date(); const upcomingGeneral = this.rsc.peekGeneralTurntimeQueue(); if (!upcomingGeneral) { break; } const upcomingTurn = upcomingGeneral.raw.turntime; const waitGeneralTurnMs = upcomingTurn.getTime() - now.getTime(); if (waitGeneralTurnMs > 0) { generalWaitTimer = setTimeout(() => { this.signal.notify(); generalWaitTimer = null; }, waitGeneralTurnMs); } else { this.signal.notify(); } } while (0); for (const queue of Object.values(this.actionQueue)) { if (!queue.isEmpty()) { this.signal.notify(); break; } } //이벤트 대기 await this.signal.wait(); if (generalWaitTimer) { clearTimeout(generalWaitTimer); } if (!this.continueRun) { break; } await this.processGeneralTurn(); await this.processActions(); } } private gameLoopPromise: Promise | null = null; 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, updateHandler); return GameEngine.instance; } public static getInstance() { if (!GameEngine.instance) { throw new Error('GameEngine is not initialized'); } return GameEngine.instance; } public start(): boolean { if (this.gameLoopPromise) { return false; } for (const queue of Object.values(this.actionQueue)) { queue.clear(); } this.continueRun = true; this.gameLoopPromise = this.gameLoop(); console.info('GameEngine started'); return true; } public async stop() { this.continueRun = false; if (this.gameLoopPromise) { this.signal.abort(); await this.gameLoopPromise; this.gameLoopPromise = null; } console.log('GameEngine stopped'); } private processAction(invoker: QueueType, action: ActionRequest): ActionResult { const actionName = action.name; const actionArgs = action.args; try{ const result = ActionRequestList[actionName](invoker, actionArgs as any, this.rsc); return { success: true, value: result }; } catch(e){ const msg = e instanceof Error ? e.message : e; return { success: false, reason: String(msg), }; } } private async pushAction(queueName: QueueType, action: ActionRequest): Promise { const queue = this.actionQueue[queueName]; if (this.actionSeq >= Number.MAX_SAFE_INTEGER) { this.actionSeq = 0; } const seq = this.actionSeq++; if (this.queueWaiters.has(seq)) { throw new Error(`GameEngine: seq(${seq}) is already used`); } let done = false; const waiter = new Promise((resolve, reject) => { this.queueWaiters.set(seq, [resolve, reject]); done = true; queue.push({ waiterSeq: seq, action }); }); await delay(0); this.signal.notify(); if (!done) { throw new Error(`GameEngine: pushAction(${queueName}) failed`); } return waiter as Promise; } public async pushServerAction(action: ActionRequest): Promise { return this.pushAction('server', action); } /* public async pushNPCAction(action: ActionRequest): Promise { return this.pushAction('npc', action); } */ public async pushAPIAction(action: ActionRequest): Promise { return this.pushAction('api', action); } }