fix: 게임 시계 완료 감사 경계 보강

This commit is contained in:
2026-09-03 11:14:01 +00:00
parent c10a71fbcc
commit 24a63d338c
13 changed files with 208 additions and 24 deletions
@@ -300,6 +300,7 @@ const respondToRaiseInvader = async (options: {
authority: options.clockOperationAuthority,
});
world.applyClockReconciliation(alignment);
const alignedState = world.getState();
const args = asRecord(payload.option).args;
if (!Array.isArray(args) || args.length !== 4 || args.some((value) => typeof value !== 'number')) {
return { ok: false, action: 'raiseInvader', reason: '이민족 소환 인자가 올바르지 않습니다.' };
@@ -315,14 +316,14 @@ const respondToRaiseInvader = async (options: {
await handler(
args,
{
year: state.currentYear,
month: state.currentMonth,
startyear: asNumber(state.meta.startYear, state.currentYear),
year: alignedState.currentYear,
month: alignedState.currentMonth,
startyear: asNumber(alignedState.meta.startYear, alignedState.currentYear),
currentEventID: 0,
// Ref uses the frozen game_env.turntime while unification is paused.
// `now` is the realtime game projection and can be hours ahead after
// a long response wait, delaying every newly summoned invader turn.
turnTime: state.lastTurnTime,
// The exact reconciliation snapshot is the authority even when the
// turn rate stays unchanged. Using the pre-reconciliation cursor here
// would create immediately overdue invader turns after a long wait.
turnTime: alignedState.lastTurnTime,
},
event
);
+20 -5
View File
@@ -1193,12 +1193,17 @@ export const createDatabaseTurnHooks = async (
clock_phase: string;
clock_revision: bigint;
deadline_generation: bigint;
clock_initialized: boolean;
opening_reached: boolean;
}>
>(GamePrisma.sql`
SELECT clock_phase,
clock_revision,
deadline_generation,
clock_base_time IS NOT NULL
AND clock_tick IS NOT NULL
AND clock_wall_anchor IS NOT NULL
AND last_turn_tick IS NOT NULL AS clock_initialized,
clock_wall_anchor <= CURRENT_TIMESTAMP AS opening_reached
FROM world_state
WHERE id = ${state.id}
@@ -1211,33 +1216,43 @@ export const createDatabaseTurnHooks = async (
const expectedPhase = state.clockPhase ?? (state.clockMode === 'realtime' ? 'RUNNING' : 'MANUAL');
const expectedRevision = BigInt(state.clockRevision ?? 1);
const expectedGeneration = BigInt(state.deadlineGeneration ?? 1);
// Match worldLoader's dual-read boundary: a legacy row is not an
// authoritative RUNNING clock merely because the newly-added phase
// column has its database default. Its first fenced flush installs
// the complete MANUAL snapshot atomically.
const durablePhase = durableClock.clock_initialized ? durableClock.clock_phase : 'MANUAL';
const stateMeta = asRecord(state.meta);
const unificationSuspensionId =
typeof stateMeta.unificationClockSuspensionId === 'string'
? stateMeta.unificationClockSuspensionId
: null;
const openingPhaseTransition =
durableClock.clock_phase === 'PREOPEN' && expectedPhase === 'RUNNING' && durableClock.opening_reached;
durableClock.clock_initialized &&
durablePhase === 'PREOPEN' &&
expectedPhase === 'RUNNING' &&
durableClock.opening_reached;
const unificationSuspensionTransition =
durableClock.clock_phase === 'RUNNING' &&
durableClock.clock_initialized &&
durablePhase === 'RUNNING' &&
expectedPhase === 'SUSPENDED' &&
Number(stateMeta.isunited ?? stateMeta.isUnited ?? 0) === 2 &&
Boolean(unificationSuspensionId);
const completionPhaseTransition =
durableClock.clock_phase === 'RUNNING' &&
durableClock.clock_initialized &&
durablePhase === 'RUNNING' &&
expectedPhase === 'COMPLETED' &&
Number(stateMeta.isunited ?? stateMeta.isUnited ?? 0) >= 2;
if (
(!openingPhaseTransition &&
!unificationSuspensionTransition &&
!completionPhaseTransition &&
durableClock.clock_phase !== expectedPhase) ||
durablePhase !== expectedPhase) ||
durableClock.clock_revision !== expectedRevision ||
durableClock.deadline_generation !== expectedGeneration
) {
throw new Error(
`Game clock fence changed before flush: expected ${expectedPhase}@${expectedRevision}/${expectedGeneration}, ` +
`found ${durableClock.clock_phase}@${durableClock.clock_revision}/${durableClock.deadline_generation}.`
`found ${durablePhase}@${durableClock.clock_revision}/${durableClock.deadline_generation}.`
);
}
const unificationCutWallAt = unificationSuspensionTransition ? await readClockDatabaseWall(prisma) : null;