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
@@ -60,6 +60,7 @@ import {
import { createGeneralFromJoin, JoinCreateGeneralError } from './joinCreateGeneralService.js';
import { NpcPossessionError, possessNpcGeneral } from './npcPossessionService.js';
import { buildPrestartDeleteAfter, formatPrestartDeleteAfter, readPrestartDeleteAfter } from './prestartDeletion.js';
import { respondToActionableMessage } from './actionableMessageResponse.js';
let itemRegistryPromise: Promise<Map<string, ItemModule>> | null = null;
@@ -136,6 +137,8 @@ interface CommandHandlerContext {
auctionBidder?: AuctionBidder;
tournamentRewardFinalizer?: TournamentRewardFinalizer;
getImmediateGeneralActionExecutor?: () => Promise<ImmediateGeneralActionExecutor>;
reservedTurns?: InMemoryReservedTurnStore;
loadArchivedNationMaxId?: (serverId: string) => Promise<number>;
}
const requireCommandDatabase = (ctx: CommandHandlerContext): DatabaseClient => {
@@ -1604,6 +1607,35 @@ async function handleInstantRetreat(
};
}
async function handleMessageRespond(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'messageRespond' }>
): Promise<TurnDaemonCommandResult> {
if (!ctx.getImmediateGeneralActionExecutor) {
throw new Error('Immediate general action runtime is not configured.');
}
const result = await respondToActionableMessage({
db: requireCommandDatabase(ctx) as GamePrisma.TransactionClient,
world: ctx.world,
reservedTurns: ctx.reservedTurns,
executor: await ctx.getImmediateGeneralActionExecutor(),
requestId: command.requestId,
userId: command.userId,
generalId: command.generalId,
messageId: command.messageId,
response: command.response,
loadArchivedNationMaxId: ctx.loadArchivedNationMaxId,
});
return {
type: 'messageRespond',
ok: result.ok,
generalId: command.generalId,
messageId: command.messageId,
...(result.action ? { action: result.action } : {}),
reason: result.reason,
};
}
async function handleVacation(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'vacation' }>
@@ -2532,6 +2564,7 @@ export const createTurnDaemonCommandHandler = (options: {
auctionFinalizer?: AuctionFinalizer;
auctionBidder?: AuctionBidder;
tournamentRewardFinalizer?: TournamentRewardFinalizer;
loadArchivedNationMaxId?: (serverId: string) => Promise<number>;
}): TurnDaemonCommandHandler => {
let immediateGeneralActionExecutor: Promise<ImmediateGeneralActionExecutor> | null = null;
const ctx: CommandHandlerContext = {
@@ -2539,6 +2572,8 @@ export const createTurnDaemonCommandHandler = (options: {
auctionFinalizer: options.auctionFinalizer,
auctionBidder: options.auctionBidder,
tournamentRewardFinalizer: options.tournamentRewardFinalizer,
reservedTurns: options.reservedTurns,
loadArchivedNationMaxId: options.loadArchivedNationMaxId,
getImmediateGeneralActionExecutor: () => {
immediateGeneralActionExecutor ??= createImmediateGeneralActionExecutor({
world: options.world,
@@ -2588,6 +2623,8 @@ export const createTurnDaemonCommandHandler = (options: {
handleBuildNationCandidate(ctx, command as Extract<TurnDaemonCommand, { type: 'buildNationCandidate' }>),
instantRetreat: (command) =>
handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
messageRespond: (command) =>
handleMessageRespond(ctx, command as Extract<TurnDaemonCommand, { type: 'messageRespond' }>),
vacation: (command) => handleVacation(ctx, command as Extract<TurnDaemonCommand, { type: 'vacation' }>),
setMySetting: (command) =>
handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),