feat: gameengine(wip)
This commit is contained in:
@@ -1,16 +1,139 @@
|
|||||||
|
import { Signal, delay, must, must_err } from "@sammo/util";
|
||||||
import Denque from "denque";
|
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 {
|
export class GameEngine {
|
||||||
|
|
||||||
private static instance: GameEngine | null = null;
|
private static instance: GameEngine | null = null;
|
||||||
|
|
||||||
private npcActionQueue = new Denque<unknown>();
|
private actionSeq = 0;
|
||||||
private apiActionQueue = new Denque<unknown>();
|
|
||||||
|
private queueWaiters = new Map<number, [resolve: (result: unknown) => void, reject: (reason: unknown) => void]>();
|
||||||
|
|
||||||
|
private signal = new Signal(30000);
|
||||||
|
|
||||||
|
private actionQueue = {
|
||||||
|
server: new Denque<ActionQueueItem>(),
|
||||||
|
npc: new Denque<ActionQueueItem>(),
|
||||||
|
api: new Denque<ActionQueueItem>(),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
private rsc: ResourceController
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
this.rsc = new ResourceController();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.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<void> | null = null;
|
||||||
|
|
||||||
public static getInstance() {
|
public static getInstance() {
|
||||||
if (!GameEngine.instance) {
|
if (!GameEngine.instance) {
|
||||||
GameEngine.instance = new GameEngine();
|
GameEngine.instance = new GameEngine();
|
||||||
@@ -18,13 +141,72 @@ export class GameEngine {
|
|||||||
return GameEngine.instance;
|
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() {
|
public async stop() {
|
||||||
|
this.continueRun = false;
|
||||||
|
if (this.gameLoopPromise) {
|
||||||
|
await this.signal.abort();
|
||||||
|
await this.gameLoopPromise;
|
||||||
|
this.gameLoopPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('GameEngine stopped');
|
console.log('GameEngine stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async pushAction(queneName: string, queue: Denque<ActionQueueItem>, action: ActionQueueItem): Promise<ActionQueueItemResult> {
|
||||||
|
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<ActionQueueItemResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pushServerAction(action: ActionQueueItem): Promise<ActionQueueItemResult> {
|
||||||
|
return this.pushAction('Server', this.actionQueue.server, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pushNPCAction(action: ActionQueueItem): Promise<ActionQueueItemResult> {
|
||||||
|
return this.pushAction('NPC', this.actionQueue.npc, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pushAPIAction(action: ActionQueueItem): Promise<ActionQueueItemResult> {
|
||||||
|
return this.pushAction('API', this.actionQueue.api, action);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user