feat: 응답 가능한 서신과 턴 시간 호환 이관

등용장과 이민족 선택 응답을 turn daemon transaction으로 연결하고 Ref의 통일 이후 상태 전이와 수신자별 메시지 저장 규칙을 보존한다.\n\n유산 턴 시간 변경을 nextTurnTimeBase 기반 결정적 계산으로 바로잡고 API, 엔진, Chromium 회귀를 추가한다.
This commit is contained in:
2026-08-19 18:03:56 +00:00
parent 9390195d5f
commit 81279e76b5
26 changed files with 1405 additions and 224 deletions
+53 -1
View File
@@ -30,6 +30,7 @@ import {
import {
asRecord,
ChangeJournal,
GAME_TICKS_PER_TURN,
type CommittedReadModelInvalidation,
type ReadModelDomain,
type RealtimeReadModelChanges,
@@ -1037,6 +1038,7 @@ export const createDatabaseTurnHooks = async (
const transactionOptions = { timeout: options?.transactionTimeoutMs ?? 30_000 };
const readModelBaseline = createRealtimeReadModelBaseline(world);
let worldReadModelBaseline = createWorldReadModelSignature(world);
let persistedTickSeconds = world.getState().tickSeconds;
const committedReceipts = new Map<bigint, CommittedReadModelChangeReceipt>();
const enqueueCommittedReceipt = (
@@ -1159,6 +1161,54 @@ export const createDatabaseTurnHooks = async (
data: worldStateUpdate,
});
if (
state.tickSeconds !== persistedTickSeconds &&
commandCompletion?.result.type !== 'updateRuntimeSettings'
) {
const ticksPerSecond = BigInt(GAME_TICKS_PER_TURN / state.tickSeconds);
const baseTime = state.clockBaseTime ?? state.lastTurnTime;
await prisma.$executeRaw(GamePrisma.sql`
UPDATE auction
SET close_at = CAST(${baseTime} AS timestamp)
+ (close_tick / ${ticksPerSecond}) * INTERVAL '1 second'
+ (((close_tick % ${ticksPerSecond}) * 1000) / ${ticksPerSecond}) * INTERVAL '1 millisecond',
updated_at = NOW()
WHERE close_tick IS NOT NULL
`);
await prisma.$executeRaw(GamePrisma.sql`
UPDATE message
SET time = CASE
WHEN time_tick IS NULL THEN time
ELSE CAST(${baseTime} AS timestamp)
+ (time_tick / ${ticksPerSecond}) * INTERVAL '1 second'
+ (((time_tick % ${ticksPerSecond}) * 1000) / ${ticksPerSecond}) * INTERVAL '1 millisecond'
END,
valid_until = CASE
WHEN valid_until_tick IS NULL THEN valid_until
ELSE CAST(${baseTime} AS timestamp)
+ (valid_until_tick / ${ticksPerSecond}) * INTERVAL '1 second'
+ (((valid_until_tick % ${ticksPerSecond}) * 1000) / ${ticksPerSecond}) * INTERVAL '1 millisecond'
END
WHERE time_tick IS NOT NULL OR valid_until_tick IS NOT NULL
`);
await prisma.$executeRaw(GamePrisma.sql`
UPDATE vote_poll
SET start_at = CASE
WHEN start_tick IS NULL THEN start_at
ELSE CAST(${baseTime} AS timestamp)
+ (start_tick / ${ticksPerSecond}) * INTERVAL '1 second'
+ (((start_tick % ${ticksPerSecond}) * 1000) / ${ticksPerSecond}) * INTERVAL '1 millisecond'
END,
end_at = CASE
WHEN end_tick IS NULL THEN end_at
ELSE CAST(${baseTime} AS timestamp)
+ (end_tick / ${ticksPerSecond}) * INTERVAL '1 second'
+ (((end_tick % ${ticksPerSecond}) * 1000) / ${ticksPerSecond}) * INTERVAL '1 millisecond'
END
WHERE start_tick IS NOT NULL OR end_tick IS NOT NULL
`);
}
for (const betting of pendingNationBettingOpens) {
await persistNationBettingOpen(prisma, betting);
}
@@ -1530,7 +1580,8 @@ export const createDatabaseTurnHooks = async (
return id;
},
},
message
message,
{ sendDestOnly: message.sendDestOnly }
);
}
if (options?.reservedTurns && persistedReservedTurnChanges) {
@@ -1598,6 +1649,7 @@ export const createDatabaseTurnHooks = async (
}
applyRealtimeReadModelBaseline(readModelBaseline, changes);
worldReadModelBaseline = persisted.worldReadModelSignature;
persistedTickSeconds = state.tickSeconds;
},
readModelChanges: persisted.readModelChanges,
journalWrite: persisted.journalWrite,