feat: port scenario 903 select-pool flow
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
import { isEventDomesticTraitKey } from '@sammo-ts/logic';
|
||||
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const DEFAULT_GENERAL_POOL_ROOT = path.resolve(resolveWorkspaceRoot(), 'resources', 'general-pool');
|
||||
const SUPPORTED_POOL = 'SPoolUnderU30';
|
||||
const EXPECTED_COLUMNS = [
|
||||
'generalName',
|
||||
'leadership',
|
||||
'strength',
|
||||
'intel',
|
||||
'specialDomestic',
|
||||
'dex',
|
||||
'imgsvr',
|
||||
'picture',
|
||||
] as const;
|
||||
|
||||
export interface GeneralPoolSeedEntry {
|
||||
uniqueName: string;
|
||||
info: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GeneralPoolLoaderOptions {
|
||||
generalPoolRoot?: string;
|
||||
}
|
||||
|
||||
const readPoolResource = async (filePath: string): Promise<unknown> =>
|
||||
JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
|
||||
|
||||
const normalizePoolRow = (row: unknown, index: number): GeneralPoolSeedEntry => {
|
||||
if (!Array.isArray(row) || row.length !== EXPECTED_COLUMNS.length) {
|
||||
throw new Error(`General pool row ${index} does not match the expected ${EXPECTED_COLUMNS.length} columns.`);
|
||||
}
|
||||
const info = Object.fromEntries(EXPECTED_COLUMNS.map((column, columnIndex) => [column, row[columnIndex]]));
|
||||
const uniqueName = info.generalName;
|
||||
if (typeof uniqueName !== 'string' || uniqueName.length === 0) {
|
||||
throw new Error(`General pool row ${index} has no generalName.`);
|
||||
}
|
||||
if (uniqueName.length > 20) {
|
||||
throw new Error(`General pool row ${index} has a generalName longer than the select_pool key.`);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(info.leadership) ||
|
||||
!Number.isInteger(info.strength) ||
|
||||
!Number.isInteger(info.intel) ||
|
||||
typeof info.specialDomestic !== 'string' ||
|
||||
!isEventDomesticTraitKey(info.specialDomestic) ||
|
||||
!Array.isArray(info.dex) ||
|
||||
info.dex.length !== 5 ||
|
||||
info.dex.some((value) => typeof value !== 'number' || !Number.isInteger(value) || value < 0) ||
|
||||
info.dex.reduce((sum, value) => sum + Number(value), 0) <= 0 ||
|
||||
(info.imgsvr !== 0 && info.imgsvr !== 1) ||
|
||||
typeof info.picture !== 'string'
|
||||
) {
|
||||
throw new Error(`General pool row ${index} contains invalid candidate data.`);
|
||||
}
|
||||
return {
|
||||
uniqueName,
|
||||
info: {
|
||||
...info,
|
||||
uniqueName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const loadGeneralPoolEntries = async (
|
||||
poolName: string,
|
||||
options?: GeneralPoolLoaderOptions
|
||||
): Promise<GeneralPoolSeedEntry[]> => {
|
||||
if (poolName !== SUPPORTED_POOL) {
|
||||
throw new Error(`Unsupported general pool: ${poolName}.`);
|
||||
}
|
||||
const root = path.resolve(options?.generalPoolRoot ?? DEFAULT_GENERAL_POOL_ROOT);
|
||||
const raw = await readPoolResource(path.resolve(root, `${poolName}.json`));
|
||||
if (!isRecord(raw) || !Array.isArray(raw.columns) || !Array.isArray(raw.data)) {
|
||||
throw new Error(`General pool ${poolName} is not a valid resource.`);
|
||||
}
|
||||
if (
|
||||
raw.columns.length !== EXPECTED_COLUMNS.length ||
|
||||
raw.columns.some((column, index) => column !== EXPECTED_COLUMNS[index])
|
||||
) {
|
||||
throw new Error(`General pool ${poolName} has an unexpected column contract.`);
|
||||
}
|
||||
const entries = raw.data.map(normalizePoolRow);
|
||||
if (new Set(entries.map((entry) => entry.uniqueName)).size !== entries.length) {
|
||||
throw new Error(`General pool ${poolName} contains duplicate unique names.`);
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineEventCreateManyInput } from '@sammo-ts/infra';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import {
|
||||
@@ -14,6 +16,8 @@ import type { ScenarioLoaderOptions } from './scenarioLoader.js';
|
||||
import { loadScenarioDefinitionById } from './scenarioLoader.js';
|
||||
import type { UnitSetLoaderOptions } from './unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
||||
import type { GeneralPoolLoaderOptions } from './generalPoolLoader.js';
|
||||
import { loadGeneralPoolEntries } from './generalPoolLoader.js';
|
||||
import { applyInitialChangeCityEvents } from '../turn/monthlyChangeCityAction.js';
|
||||
|
||||
const DEFAULT_TICK_SECONDS = 120 * 60;
|
||||
@@ -51,6 +55,7 @@ export interface ScenarioSeedOptions {
|
||||
scenarioOptions?: ScenarioLoaderOptions;
|
||||
mapOptions?: MapLoaderOptions;
|
||||
unitSetOptions?: UnitSetLoaderOptions;
|
||||
generalPoolOptions?: GeneralPoolLoaderOptions;
|
||||
resetTables?: boolean;
|
||||
now?: Date;
|
||||
tickSeconds?: number;
|
||||
@@ -209,6 +214,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
const scenarioDefinition = includeExtendedGeneral ? scenario : { ...scenario, generalsEx: [] };
|
||||
const map = await loadMapDefinitionByName(scenario.config.environment.mapName, options.mapOptions);
|
||||
const unitSet = await loadUnitSetDefinitionByName(scenario.config.environment.unitSet, options.unitSetOptions);
|
||||
const targetGeneralPool =
|
||||
typeof scenario.config.map.targetGeneralPool === 'string' ? scenario.config.map.targetGeneralPool : null;
|
||||
const generalPoolEntries = targetGeneralPool
|
||||
? await loadGeneralPoolEntries(targetGeneralPool, options.generalPoolOptions)
|
||||
: [];
|
||||
|
||||
const { seed, warnings } = buildScenarioBootstrap({
|
||||
scenario: scenarioDefinition,
|
||||
@@ -271,10 +281,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
worldMeta.serverId = install.serverId.trim();
|
||||
}
|
||||
|
||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV];
|
||||
if (typeof integrationSeed === 'string' && integrationSeed.trim().length > 0) {
|
||||
worldMeta.hiddenSeed = integrationSeed.trim();
|
||||
}
|
||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
||||
worldMeta.hiddenSeed =
|
||||
integrationSeed && integrationSeed.length > 0
|
||||
? integrationSeed
|
||||
: randomBytes(16).toString('hex');
|
||||
|
||||
if (install?.preopenAt) {
|
||||
worldMeta.preopenAt = formatDateTime(install.preopenAt);
|
||||
@@ -286,6 +297,8 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
options: install.autorunUser.options,
|
||||
};
|
||||
}
|
||||
const archivedWorldMeta = { ...worldMeta };
|
||||
delete archivedWorldMeta.hiddenSeed;
|
||||
|
||||
await connector.connect();
|
||||
try {
|
||||
@@ -297,6 +310,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
if (eventTableReady) {
|
||||
await prisma.event.deleteMany();
|
||||
}
|
||||
await prisma.selectPoolEntry.deleteMany();
|
||||
await prisma.generalTurn.deleteMany();
|
||||
await prisma.generalTurnRevision.deleteMany();
|
||||
await prisma.rankData.deleteMany();
|
||||
await prisma.generalAccessLog.deleteMany();
|
||||
await prisma.diplomacy.deleteMany();
|
||||
await prisma.general.deleteMany();
|
||||
await prisma.troop.deleteMany();
|
||||
@@ -316,6 +334,15 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
},
|
||||
});
|
||||
|
||||
if (generalPoolEntries.length > 0) {
|
||||
await prisma.selectPoolEntry.createMany({
|
||||
data: generalPoolEntries.map((entry) => ({
|
||||
uniqueName: entry.uniqueName,
|
||||
info: asJson(entry.info),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof worldMeta.serverId === 'string' && worldMeta.serverId) {
|
||||
await prisma.gameHistory.upsert({
|
||||
where: { serverId: worldMeta.serverId },
|
||||
@@ -332,7 +359,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
||||
env: asJson({
|
||||
config: scenarioConfig,
|
||||
meta: worldMeta,
|
||||
meta: archivedWorldMeta,
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
@@ -347,7 +374,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
||||
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
||||
env: asJson({
|
||||
config: scenarioConfig,
|
||||
meta: worldMeta,
|
||||
meta: archivedWorldMeta,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user