From 31184c1cd4c03c94af6bd3e05ffb3c59e2555abc Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sat, 9 Mar 2024 07:41:06 +0000 Subject: [PATCH] feat: gameengine(wip) --- @sammo/server/src/GameEngine.ts | 192 +++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 5 deletions(-) diff --git a/@sammo/server/src/GameEngine.ts b/@sammo/server/src/GameEngine.ts index f545e6f..4b80f6f 100644 --- a/@sammo/server/src/GameEngine.ts +++ b/@sammo/server/src/GameEngine.ts @@ -1,16 +1,139 @@ +import { Signal, delay, must, must_err } from "@sammo/util"; import Denque from "denque"; +import { ResourceController } from "./ResourceController.js"; +import { entriesWithType } from "@sammo/util/converter"; + +export interface ActionQueueItem { + waiterSeq: number; +} + +export interface ActionQueueItemResult { + success: boolean; + reason?: string; + message?: string; +} export class GameEngine { private static instance: GameEngine | null = null; - private npcActionQueue = new Denque(); - private apiActionQueue = new Denque(); + private actionSeq = 0; + + private queueWaiters = new Map void, reject: (reason: unknown) => void]>(); + + private signal = new Signal(30000); + + private actionQueue = { + server: new Denque(), + npc: new Denque(), + api: new Denque(), + } as const; + + private rsc: ResourceController private constructor() { - + this.rsc = new ResourceController(); } + 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.upcomingGeneral(); + if (!upcomingGeneral) { + break; + } + const upcomingTurn = upcomingGeneral.raw.turntime; + + const waitGeneralTurnMs = upcomingTurn.getTime() - now.getTime(); + if (waitGeneralTurnMs > 0) { + generalWaitTimer = setTimeout(() => { + this.signal.notify(); + }, waitGeneralTurnMs); + } + else { + this.signal.notify(); + } + } while (0); + + //이벤트를 대기 + const waitResult = 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.upcomingGeneral(); + if (!upcomingGeneral) { + break; + } + + if (upcomingGeneral.raw.turntime > now) { + break; + } + + //TODO: 턴 수행해야지! + + upcomingGeneral.updateTurntime(); + processedGeneralCount++; + } + + if (processedGeneralCount > 0) { + await prevSaveWaiter; + prevSaveWaiter = this.rsc.saveAll(); + } + + //서버 이벤트 처리 + for (const [invoker, queue] of entriesWithType(this.actionQueue)) { + let processEventCount = 0; + + 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) { + ticketRejected(errorReason); + } + else { + ticketResolve({ success: true }); + } + + processEventCount++; + } + + if (processEventCount > 0) { + await prevSaveWaiter; + prevSaveWaiter = this.rsc.saveAll(); + } + } + } + } + + + private gameLoopPromise: Promise | null = null; + public static getInstance() { if (!GameEngine.instance) { GameEngine.instance = new GameEngine(); @@ -18,13 +141,72 @@ export class GameEngine { return GameEngine.instance; } - public async start() { + public start(): boolean { + if (this.gameLoopPromise) { + return false; + } - console.log('GameEngine started'); + 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(queneName: string, queue: Denque, action: ActionQueueItem): Promise { + 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(action); + }); + + await delay(0); + this.signal.notify(); + + if (!done) { + throw new Error(`GameEngine: push${queneName}Action failed`); + } + + return waiter as Promise; + } + + public async pushServerAction(action: ActionQueueItem): Promise { + return this.pushAction('Server', this.actionQueue.server, action); + } + + public async pushNPCAction(action: ActionQueueItem): Promise { + return this.pushAction('NPC', this.actionQueue.npc, action); + } + + public async pushAPIAction(action: ActionQueueItem): Promise { + return this.pushAction('API', this.actionQueue.api, action); + } + } \ No newline at end of file