fix: Ref 게임 로직과 시나리오 풀 호환을 보정

월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
This commit is contained in:
2026-08-23 16:26:14 +00:00
parent bf6b7be7b0
commit 85591c68ad
114 changed files with 13327 additions and 901 deletions
+40 -17
View File
@@ -16,11 +16,7 @@ import {
import { readInheritancePoint, resolveInheritConstants } from '../../services/inheritance.js';
import { loadAuthoritativeAccountIcon } from '../../services/accountIconSync.js';
import { loadCurrentGameTime } from '../../services/gameClock.js';
import {
getSelectionPoolStatus,
reserveSelectionPool,
resolveSelectionMaxGeneral,
} from '@sammo-ts/game-engine/turn/selectPoolService.js';
import { getSelectionPoolStatus, resolveSelectionMaxGeneral } from '@sammo-ts/game-engine/turn/selectPoolService.js';
import {
ConflictingTurnDaemonCommandError,
RejectedNpcPossessionCommandError,
@@ -54,6 +50,31 @@ const resolveSelectionCommandResult = (
return { ok: true, generalId: result.generalId };
};
const resolveSelectionReservationCommandResult = (
result: Awaited<ReturnType<GameApiContext['turnDaemon']['requestCommand']>> | null
) => {
if (!result) {
throw new TRPCError({
code: 'TIMEOUT',
message:
'장수 선택 목록 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.',
});
}
if (result.type !== 'selectPoolReserve') {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: '턴 데몬이 올바르지 않은 장수 선택 목록 결과를 반환했습니다.',
});
}
if (!result.ok) {
throw new TRPCError({ code: result.code, message: result.reason });
}
return result.reservation;
};
const resolveSelectionReservationRequestId = (contextRequestId: string | undefined, userId: string) =>
contextRequestId ? `select-pool:${userId}:${contextRequestId}:reserve` : undefined;
const resolveSelectionRequestId = (
contextRequestId: string | undefined,
userId: string,
@@ -366,26 +387,22 @@ export const joinRouter = router({
},
};
}),
getSelectionPool: authedProcedure.mutation(async ({ ctx }) => {
getSelectionPool: engineAuthedProcedure.mutation(async ({ ctx }) => {
const userId = ctx.auth?.user.id;
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
const worldState = await ctx.db.worldState.findFirst();
if (!worldState) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'World state is not initialized.',
});
}
const gameTime = await loadCurrentGameTime(ctx.db);
return reserveSelectionPool({
db: ctx.db,
worldState,
const commandRequestId = resolveSelectionReservationRequestId(ctx.requestId, userId);
const result = await ctx.turnDaemon.requestCommand({
type: 'selectPoolReserve',
...(commandRequestId ? { requestId: commandRequestId } : {}),
userId,
now: gameTime.now,
seedOwnerIdentity: ctx.auth?.user.legacyMemberNo ?? userId,
acceptedGameAt: gameTime.now.toISOString(),
...(gameTime.tick === null ? {} : { acceptedGameTick: gameTime.tick }),
});
return resolveSelectionReservationCommandResult(result);
}),
selectPoolGeneral: engineAuthedProcedure
.input(
@@ -413,6 +430,7 @@ export const joinRouter = router({
});
}
const commandRequestId = resolveSelectionRequestId(ctx.requestId, userId, input.clientRequestId, 'create');
const gameTime = await loadCurrentGameTime(ctx.db);
const result = await ctx.turnDaemon.requestCommand({
type: 'selectPoolCreate',
...(commandRequestId ? { requestId: commandRequestId } : {}),
@@ -421,6 +439,8 @@ export const joinRouter = router({
uniqueName: input.uniqueName,
personality: input.personality,
seedOwnerIdentity: auth.user.legacyMemberNo ?? userId,
acceptedGameAt: gameTime.now.toISOString(),
...(gameTime.tick === null ? {} : { acceptedGameTick: gameTime.tick }),
...(selectedIcon
? {
ownerPicture: selectedIcon.picture,
@@ -450,12 +470,15 @@ export const joinRouter = router({
input.clientRequestId,
'reselect'
);
const gameTime = await loadCurrentGameTime(ctx.db);
const result = await ctx.turnDaemon.requestCommand({
type: 'selectPoolReselect',
...(commandRequestId ? { requestId: commandRequestId } : {}),
userId,
ownerDisplayName: auth.user.displayName,
uniqueName: input.uniqueName,
acceptedGameAt: gameTime.now.toISOString(),
...(gameTime.tick === null ? {} : { acceptedGameTick: gameTime.tick }),
});
return resolveSelectionCommandResult(result, 'selectPoolReselect');
}),