305 lines
8.7 KiB
TypeScript
305 lines
8.7 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 { ActionPack, GameEngineMsg } from "./GameEngineDefs.js";
|
|
import { ActionRequestList, type QueueType } from "@sammo/game_logic";
|
|
export interface ActionRequest {
|
|
name: keyof ActionPack;
|
|
args: Parameters<ActionPack[keyof ActionPack]>[1];
|
|
}
|
|
|
|
export interface ActionRequestContainer {
|
|
waiterSeq: number;
|
|
action: ActionRequest;
|
|
}
|
|
|
|
export type ActionSuccess = {
|
|
success: true;
|
|
value: ReturnType<ActionPack[keyof ActionPack]>;
|
|
}
|
|
|
|
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<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 prevSaveWaiter: Promise<void> | 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<void> {
|
|
let generalWaitTimer: ReturnType<typeof setTimeout> | 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<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) {
|
|
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<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);
|
|
}
|
|
|
|
} |