fix: 가오픈 명령과 GAME WALL 시간 경계 회귀 수정

This commit is contained in:
2026-09-05 00:55:33 +00:00
parent 1d53e6f226
commit 3437cd99f4
25 changed files with 458 additions and 71 deletions
@@ -33,6 +33,11 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
async drain(): Promise<TurnDaemonCommand[]> {
const local = this.localQueue.splice(0, this.localQueue.length);
// 종료 직후 실행하지 않을 명령을 PROCESSING으로 선점하면 새 daemon은
// lease 만료까지 기다려야 한다. 종료 batch에서는 DB 명령을 가져오지 않는다.
if (local.some((command) => command.type === 'shutdown')) {
return local;
}
const remote = await this.claimPending();
return local.concat(remote);
}
@@ -116,7 +121,13 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
orderBy: { id: 'asc' },
select: { clockPhase: true, clockRevision: true, deadlineGeneration: true, clockTick: true },
});
const gameplayAllowed = !world || world.clockPhase === 'RUNNING' || world.clockPhase === 'MANUAL';
// 가오픈도 장수 생성·삭제·거병·예약 등 사용자 명령은 처리한다.
// 자동 턴의 RUNNING/MANUAL gate는 TurnDaemonLifecycle이 별도로 지킨다.
const gameplayAllowed =
!world ||
world.clockPhase === 'PREOPEN' ||
world.clockPhase === 'RUNNING' ||
world.clockPhase === 'MANUAL';
const suspendedTournamentBetCommand = world?.clockPhase === 'SUSPENDED';
const currentRevision = world?.clockRevision ?? null;
const maintenanceSuspended =
@@ -158,6 +158,20 @@ export class TurnDaemonLifecycle {
continue;
}
const nowMs = this.clock.nowMs();
const wallNow = new Date(nowMs);
let gameClock = await this.stateStore.loadGameClock?.(wallNow);
if (gameClock?.phase === 'PREOPEN' && this.stateStore.promotePreopenAtOpening) {
await this.stateStore.promotePreopenAtOpening(wallNow);
gameClock = await this.stateStore.loadGameClock?.(wallNow);
}
if (gameClock?.phase && gameClock.phase !== 'RUNNING' && gameClock.phase !== 'MANUAL') {
this.status.nextTurnTime = undefined;
await this.clock.sleepMs(500);
continue;
}
// 수동 실행 요청도 가오픈·정지·재조정의 턴 실행 gate를 통과해야 한다.
// 사용자 명령 처리는 루프 시작에서 계속하되 시간 진행은 여기서 분리한다.
if (this.pendingRun) {
await this.runOnce(this.pendingRun);
this.pendingRun = null;
@@ -169,23 +183,6 @@ export class TurnDaemonLifecycle {
await this.clock.sleepMs(200);
continue;
}
const nowMs = this.clock.nowMs();
const wallNow = new Date(nowMs);
let gameClock = await this.stateStore.loadGameClock?.(wallNow);
if (gameClock?.phase === 'PREOPEN' && this.stateStore.promotePreopenAtOpening) {
await this.stateStore.promotePreopenAtOpening(wallNow);
gameClock = await this.stateStore.loadGameClock?.(wallNow);
}
if (
gameClock?.phase &&
gameClock.phase !== 'RUNNING' &&
gameClock.phase !== 'MANUAL'
) {
this.status.nextTurnTime = undefined;
await this.clock.sleepMs(500);
continue;
}
if (gameClock?.mode === 'manual') {
// Ref observes all generals due before one monthly boundary in
// a single snapshot. Manual mode advances directly to that