Files
core_ng/@sammo/server/src/GameEngine.ts
T
2024-03-09 16:15:43 +00:00

275 lines
8.1 KiB
TypeScript

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<number, [resolve: (result: ActionSuccess) => void, reject: (reason: ActionFailure) => void]>();
private signal = new Signal(30000);
private actionQueue = {
server: new Denque<ActionRequestContainer>(),
//npc: new Denque<ActionRequestContainer>(),
api: new Denque<ActionRequestContainer>(),
} as const satisfies Record<QueueType, Denque<ActionRequestContainer>>;
private rsc: ResourceController
private constructor(
db: Mongoose,
private postStatus: (msg: GameEngineMsg) => void,
) {
this.rsc = new ResourceController(db);
}
private continueRun = true;
private async gameLoop(): Promise<void> {
let generalWaitTimer: ReturnType<typeof setTimeout> | null = null;
let prevSaveWaiter: Promise<void> | 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<void> | null = null;
public static async initInstance(updateHandler: (msg: GameEngineMsg)=>void): Promise<GameEngine> {
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<ActionResult> {
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<ActionResult>;
}
public async pushServerAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('server', action);
}
/*
public async pushNPCAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('npc', action);
}
*/
public async pushAPIAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('api', action);
}
}