feat: 실행 중 게임 옵션 변경을 지원

Gateway 관리 화면에서 현재 기수의 장수 생성 제한, 유저 자동턴, 턴 간격을 내구성 action으로 변경한다.

턴 간격 변경은 논리 tick과 현재 게임 시각을 보존하며 DB와 Redis의 tick 기반 시각을 재투영하고 기존 로그 timestamp는 유지한다.
This commit is contained in:
2026-08-17 16:05:07 +00:00
parent 50e7d894e4
commit ca6823d409
26 changed files with 1680 additions and 115 deletions
+16 -4
View File
@@ -89,6 +89,7 @@ import { buildCommandEnv } from './reservedTurnCommands.js';
import { DatabaseTurnDaemonLease, TurnDaemonLeaseUnavailableError } from '../lifecycle/databaseTurnDaemonLease.js';
import { EngineStateManager } from './engineStateManager.js';
import { applyRuntimeClockShift } from './runtimeClockShift.js';
import { applyRuntimeGameSettings } from './runtimeGameSettings.js';
export interface TurnDaemonRuntimeOptions {
profile: string;
@@ -442,7 +443,7 @@ const createMonthlyCalendarRuntime = async (options: {
profileName: options.profileName,
getWorld: options.getWorld,
getRedisClient: options.getRedisClient,
getWorldConfig: () => options.snapshot.worldConfig ?? null,
getWorldConfig: () => options.getWorld()?.getWorldConfig() ?? options.snapshot.worldConfig ?? null,
getNationPowerRollCount: () => cache.nationPowerRollCount,
getTournamentRollConsumed: () => cache.tournamentRollConsumed,
now: () => options.getWorld()?.getGameNow(new Date(options.clock.nowMs())) ?? new Date(options.clock.nowMs()),
@@ -451,7 +452,7 @@ const createMonthlyCalendarRuntime = async (options: {
profileName: options.profileName,
getWorld: options.getWorld,
getRedisClient: options.getRedisClient,
getWorldConfig: () => options.snapshot.worldConfig ?? null,
getWorldConfig: () => options.getWorld()?.getWorldConfig() ?? options.snapshot.worldConfig ?? null,
getNationPowerRollCount: () => cache.nationPowerRollCount,
onTournamentRollConsumed: (consumed) => {
cache.tournamentRollConsumed = consumed;
@@ -599,6 +600,17 @@ const createStartedAdminActionConsumer = async (options: {
redis: options.redisConnector?.client,
});
}
if (action.action === 'UPDATE_RUNTIME_SETTINGS') {
if (!options.commandConnector) {
return { status: 'FAILED', detail: '게임 command database 연결이 없습니다.' };
}
return applyRuntimeGameSettings({
action,
profileName,
db: options.commandConnector.prisma,
redis: options.redisConnector?.client,
});
}
switch (action.action) {
case 'RESUME':
options.controlQueue.enqueue({ type: 'resume', reason });
@@ -766,7 +778,6 @@ const createTurnDaemonRuntimeWithLease = async (
const stateStore = new InMemoryTurnStateStore(world);
let fastForwardPreparedMonth = '';
const processor = new InMemoryTurnProcessor(world, {
tickMinutes,
beforeExecuteGeneral: reservedTurnStoreHandle
? async (general) => {
if (options.exclusiveFastForward) {
@@ -959,7 +970,8 @@ const createTurnDaemonRuntimeWithLease = async (
{
clock,
controlQueue: resolvedControlQueue,
getNextTickTime: (lastTurnTime) => getNextTickTime(lastTurnTime, tickMinutes),
getNextTickTime: (lastTurnTime) =>
getNextTickTime(lastTurnTime, Math.max(1, Math.round(world.getState().tickSeconds / 60))),
stateStore,
processor,
hooks,