fix(game): preserve scenario NPC progression
This commit is contained in:
@@ -205,6 +205,9 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
const generalPoolEntries = targetGeneralPool
|
||||
? await loadGeneralPoolEntries(targetGeneralPool, options.generalPoolOptions)
|
||||
: [];
|
||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
||||
const hiddenSeed =
|
||||
integrationSeed && integrationSeed.length > 0 ? integrationSeed : randomBytes(16).toString('hex');
|
||||
|
||||
const { seed, warnings } = buildScenarioBootstrap({
|
||||
scenario: scenarioDefinition,
|
||||
@@ -212,6 +215,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
unitSet,
|
||||
options: {
|
||||
includeNeutralNationInSeed: options.includeNeutralNationInSeed ?? true,
|
||||
hiddenSeed,
|
||||
},
|
||||
});
|
||||
seed.cities = applyInitialChangeCityEvents(seed.cities, seed.initialEvents);
|
||||
@@ -273,9 +277,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
worldMeta.installCommitSha = install.installCommitSha.trim();
|
||||
}
|
||||
|
||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
||||
worldMeta.hiddenSeed =
|
||||
integrationSeed && integrationSeed.length > 0 ? integrationSeed : randomBytes(16).toString('hex');
|
||||
worldMeta.hiddenSeed = hiddenSeed;
|
||||
|
||||
if (install?.preopenAt) {
|
||||
worldMeta.preopenAt = formatDateTime(install.preopenAt);
|
||||
|
||||
@@ -553,6 +553,7 @@ export const createDatabaseTurnHooks = async (
|
||||
profileName?: string;
|
||||
reservedTurns?: InMemoryReservedTurnStore;
|
||||
turnDaemonLease?: DatabaseTurnDaemonLease;
|
||||
transactionTimeoutMs?: number;
|
||||
}
|
||||
): Promise<DatabaseTurnHooks> => {
|
||||
// 턴 처리 결과를 DB에 반영하는 훅을 만든다.
|
||||
@@ -999,7 +1000,10 @@ export const createDatabaseTurnHooks = async (
|
||||
if (transaction) {
|
||||
await persist(transaction);
|
||||
} else {
|
||||
await prisma.$transaction(persist);
|
||||
await prisma.$transaction(
|
||||
persist,
|
||||
options?.transactionTimeoutMs ? { timeout: options.transactionTimeoutMs } : undefined
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -1020,11 +1024,14 @@ export const createDatabaseTurnHooks = async (
|
||||
acknowledge();
|
||||
},
|
||||
executeCommand: async (requestId, execute) => {
|
||||
const committed = await prisma.$transaction(async (transaction) => {
|
||||
const result = await execute({ db: transaction });
|
||||
const acknowledge = await persistChanges(transaction, { requestId, result });
|
||||
return { result, acknowledge };
|
||||
});
|
||||
const committed = await prisma.$transaction(
|
||||
async (transaction) => {
|
||||
const result = await execute({ db: transaction });
|
||||
const acknowledge = await persistChanges(transaction, { requestId, result });
|
||||
return { result, acknowledge };
|
||||
},
|
||||
options?.transactionTimeoutMs ? { timeout: options.transactionTimeoutMs } : undefined
|
||||
);
|
||||
committed.acknowledge();
|
||||
return committed.result;
|
||||
},
|
||||
|
||||
@@ -102,6 +102,13 @@ export interface TurnDaemonRuntimeOptions {
|
||||
leaseDurationMs?: number;
|
||||
leaseOwnerId?: string;
|
||||
enableLeaseHeartbeat?: boolean;
|
||||
/**
|
||||
* Isolated, single-process fixture acceleration only. Reserved turns are
|
||||
* loaded once and no concurrent API writer may touch this database.
|
||||
*/
|
||||
exclusiveFastForward?: boolean;
|
||||
databaseTransactionTimeoutMs?: number;
|
||||
onActionResolved?: NonNullable<Parameters<typeof createReservedTurnHandler>[0]['onActionResolved']>;
|
||||
}
|
||||
|
||||
export interface TurnDaemonRuntime {
|
||||
@@ -179,6 +186,9 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
databaseFlushEnabled: boolean,
|
||||
turnDaemonLease: DatabaseTurnDaemonLease | null
|
||||
): Promise<TurnDaemonRuntime> => {
|
||||
if (options.exclusiveFastForward && options.profileName) {
|
||||
throw new Error('exclusiveFastForward cannot be used with a gateway-managed profile.');
|
||||
}
|
||||
// DB에서 월드를 읽고 턴 데몬을 구동할 런타임을 만든다.
|
||||
const { state, snapshot } = await loadTurnWorldFromDatabase({
|
||||
databaseUrl: options.databaseUrl,
|
||||
@@ -499,6 +509,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
commandProfile,
|
||||
commandEnv: monthlyCommandEnv,
|
||||
getAdditionalOccupiedUniqueItemKeys: () => occupiedAuctionUniqueItemKeys,
|
||||
onActionResolved: options.onActionResolved,
|
||||
})),
|
||||
calendarHandler: calendarHandler ?? undefined,
|
||||
autoAdvanceDiplomacyMonth: false,
|
||||
@@ -538,10 +549,20 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
});
|
||||
|
||||
const stateStore = new InMemoryTurnStateStore(world);
|
||||
let fastForwardPreparedMonth = '';
|
||||
const processor = new InMemoryTurnProcessor(world, {
|
||||
tickMinutes,
|
||||
beforeExecuteGeneral: reservedTurnStoreHandle
|
||||
? async (general) => {
|
||||
if (options.exclusiveFastForward) {
|
||||
const state = world.getState();
|
||||
const monthKey = `${state.currentYear}-${state.currentMonth}`;
|
||||
if (fastForwardPreparedMonth !== monthKey) {
|
||||
await refreshOccupiedAuctionUniqueItemKeys();
|
||||
fastForwardPreparedMonth = monthKey;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const promises: Promise<unknown>[] = [];
|
||||
promises.push(
|
||||
reservedTurnStoreHandle.store.prepareTurnsForExecution(
|
||||
@@ -597,6 +618,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
profileName: options.profileName ?? options.profile,
|
||||
reservedTurns: reservedTurnStoreHandle?.store,
|
||||
turnDaemonLease: turnDaemonLease ?? undefined,
|
||||
transactionTimeoutMs: options.databaseTransactionTimeoutMs,
|
||||
});
|
||||
auctionBidder = await createAuctionBidder({
|
||||
databaseUrl: options.databaseUrl,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { loadTurnCommandProfile } from '../src/turn/turnCommandProfile.js';
|
||||
|
||||
const GENERAL_AI_ACTIONS = [
|
||||
'che_군량매매',
|
||||
'che_귀환',
|
||||
'che_랜덤임관',
|
||||
'che_모병',
|
||||
'che_물자조달',
|
||||
'che_선양',
|
||||
'che_소집해제',
|
||||
'che_이동',
|
||||
'che_정착장려',
|
||||
'che_해산',
|
||||
'che_헌납',
|
||||
] as const;
|
||||
|
||||
const NATION_AI_ACTIONS = ['che_몰수', 'che_발령', 'che_선전포고', 'che_천도', 'che_포상'] as const;
|
||||
|
||||
describe('default turn command profile AI coverage', () => {
|
||||
it('loads every action selected directly by the general and nation AI', async () => {
|
||||
const profile = await loadTurnCommandProfile();
|
||||
|
||||
expect(profile.general).toEqual(expect.arrayContaining([...GENERAL_AI_ACTIONS]));
|
||||
expect(profile.nation).toEqual(expect.arrayContaining([...NATION_AI_ACTIONS]));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user