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 { GameEngineMsg } from "./GameEngineDefs.js"; export interface ActionRequest { } export interface ActionRequestContainer { waiterSeq: number; action: ActionRequest; } export type ActionSuccess = { success: true; message?: string; } export type ActionFailure = { success: false; reason: string; } export type ActionResult = ActionSuccess | ActionFailure; type QueueType = 'server' | 'api'; //type QueueType = 'server' | 'npc' | 'api'; 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 async gameLoop(): Promise { let generalWaitTimer: ReturnType | null = null; let prevSaveWaiter: Promise | 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; } 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 prevSaveWaiter; const lastExecuted = this.rsc.gameEnv().raw.lastExecuted; prevSaveWaiter = this.rsc.saveAll().then(()=>{ this.postStatus({ type: 'update', lastExecuted: lastExecuted.toISOString() }); }); } //서버 이벤트 처리 for (const [invoker, queue] of entriesWithType(this.actionQueue)) { let processEventCount = 0; let responseQueue: (() => void)[] = []; while (!queue.isEmpty()) { const action = must(queue.shift()); //TODO: action 처리 const errorReason: string | undefined = undefined; const [ticketResolve, ticketRejected] = must_err( this.queueWaiters.get(action.waiterSeq), Error, `GameEngine: waiterSeq(${action.waiterSeq}) not found` ); this.queueWaiters.delete(action.waiterSeq); if (errorReason) { responseQueue.push(() => { ticketRejected(errorReason); }); } else { responseQueue.push(() => { ticketResolve({ success: true }); }); } processEventCount++; } if (processEventCount > 0) { await prevSaveWaiter; prevSaveWaiter = null; await this.rsc.saveAll(); for (const response of responseQueue) { response(); } } } } } 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) { await this.signal.abort(); await this.gameLoopPromise; this.gameLoopPromise = null; } console.log('GameEngine stopped'); } 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); } }