diff --git a/app/game-engine/src/scenario/scenarioSeeder.ts b/app/game-engine/src/scenario/scenarioSeeder.ts index 87d4f02..5c81868 100644 --- a/app/game-engine/src/scenario/scenarioSeeder.ts +++ b/app/game-engine/src/scenario/scenarioSeeder.ts @@ -34,6 +34,7 @@ export interface ScenarioInstallOptions { joinMode?: 'full' | 'onlyRandom'; autorunUser?: ScenarioAutorunOptions | null; preopenAt?: Date | null; + season?: number; } export interface ScenarioSeedOptions { @@ -254,6 +255,10 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom lastTurnTime: formatDateTime(now), }; + if (typeof install?.season === 'number' && Number.isFinite(install.season)) { + worldMeta.season = Math.floor(install.season); + } + const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]; if (typeof integrationSeed === 'string' && integrationSeed.trim().length > 0) { worldMeta.hiddenSeed = integrationSeed.trim(); diff --git a/app/gateway-api/src/adminRouter.ts b/app/gateway-api/src/adminRouter.ts index a86fb2d..86b9bd1 100644 --- a/app/gateway-api/src/adminRouter.ts +++ b/app/gateway-api/src/adminRouter.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { TRPCError } from '@trpc/server'; import { z } from 'zod'; -import { resolvePostgresConfigFromEnv } from '@sammo-ts/infra'; +import { createGamePostgresConnector, resolvePostgresConfigFromEnv } from '@sammo-ts/infra'; import { procedure, router } from './trpc.js'; import { listScenarioPreviews, resolveGitCommitSha } from './scenario/scenarioCatalog.js'; @@ -329,6 +329,20 @@ const readMetaObject = (value: unknown): Record => { return value as Record; }; +const readMetaNumber = (meta: Record, key: string): number | null => { + const raw = meta[key]; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return Math.floor(raw); + } + if (typeof raw === 'string') { + const parsed = Number(raw); + if (Number.isFinite(parsed)) { + return Math.floor(parsed); + } + } + return null; +}; + const applyMetaPatch = ( meta: Record, patch: Record @@ -850,6 +864,22 @@ export const adminRouter = router({ schema: updatedProfile.profile, }).url; const resourcesRoot = path.resolve(process.cwd(), 'resources'); + const profileMeta = readMetaObject(updatedProfile.meta); + const nextSeasonIdx = readMetaNumber(profileMeta, 'nextSeasonIdx'); + let baseSeason: number | null = null; + const connector = createGamePostgresConnector({ url: databaseUrl }); + try { + await connector.connect(); + const row = await connector.prisma.worldState.findFirst({ + select: { meta: true }, + }); + if (row) { + baseSeason = readMetaNumber(readMetaObject(row.meta), 'season'); + } + } finally { + await connector.disconnect(); + } + const season = nextSeasonIdx ?? baseSeason ?? 1; await seedProfileDatabase({ databaseUrl, @@ -866,6 +896,7 @@ export const adminRouter = router({ showImgLevel: input.install.showImgLevel, tournamentTrig: input.install.tournamentTrig, joinMode: input.install.joinMode, + season, autorunUser: input.install.autorunUser ? { limitMinutes: input.install.autorunUser.limitMinutes, diff --git a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts index 1e763ad..ecc5d86 100644 --- a/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts +++ b/app/gateway-api/src/orchestrator/gatewayOrchestrator.ts @@ -113,6 +113,20 @@ interface GatewayAdminActionResult { const normalizeMeta = (value: unknown): Record => (isRecord(value) ? value : {}); +const readMetaNumber = (meta: Record, key: string): number | null => { + const raw = meta[key]; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return Math.floor(raw); + } + if (typeof raw === 'string') { + const parsed = Number(raw); + if (Number.isFinite(parsed)) { + return Math.floor(parsed); + } + } + return null; +}; + const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => { if (typeof value === 'string') { return value as GatewayAdminActionStatus; @@ -615,6 +629,10 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { if (!seedInfo.scenarioId) { return { status: 'FAILED', detail: 'scenarioId is missing' }; } + const profileMeta = normalizeMeta(profile.meta); + const nextSeasonIdx = readMetaNumber(profileMeta, 'nextSeasonIdx'); + const baseSeason = readMetaNumber(normalizeMeta(seedInfo.meta), 'season'); + const season = nextSeasonIdx ?? baseSeason ?? 1; const seedTime = openAt ?? (action.scheduledAt && action.action === 'RESET_SCHEDULED' ? new Date(action.scheduledAt) : this.now()); @@ -650,7 +668,10 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { scenarioId: seedInfo.scenarioId, tickSeconds: seedInfo.tickSeconds, now: seedTime, - installOptions: installOptions ?? undefined, + installOptions: { + ...(installOptions ?? {}), + season, + }, scenarioOptions: { scenarioRoot: path.join(resourceRoot, 'scenario') }, mapOptions: { mapRoot: path.join(resourceRoot, 'map') }, unitSetOptions: { unitSetRoot: path.join(resourceRoot, 'unitset') }, @@ -685,18 +706,19 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { private async resolveResetSeedInfo( profile: GatewayProfileRecord, overrides?: { scenarioId?: number | null; tickSeconds?: number } - ): Promise<{ databaseUrl: string; scenarioId: number | null; tickSeconds?: number }> { + ): Promise<{ databaseUrl: string; scenarioId: number | null; tickSeconds?: number; meta: Record } > { const databaseUrl = resolvePostgresConfigFromEnv({ env: this.processConfig.baseEnv ?? process.env, schema: profile.profile, }).url; let scenarioId = overrides?.scenarioId ?? parseScenarioId(profile.scenario); let tickSeconds: number | undefined = overrides?.tickSeconds; + let meta: Record = {}; const connector = createGamePostgresConnector({ url: databaseUrl }); await connector.connect(); try { const row = await connector.prisma.worldState.findFirst({ - select: { scenarioCode: true, tickSeconds: true }, + select: { scenarioCode: true, tickSeconds: true, meta: true }, }); if (row) { if (scenarioId === null) { @@ -708,11 +730,12 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle { if (tickSeconds === undefined && typeof row.tickSeconds === 'number' && Number.isFinite(row.tickSeconds)) { tickSeconds = row.tickSeconds; } + meta = normalizeMeta(row.meta); } } finally { await connector.disconnect(); } - return { databaseUrl, scenarioId, tickSeconds }; + return { databaseUrl, scenarioId, tickSeconds, meta }; } private async runBuildCommands(