fix: make scenario reset generation atomic
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineEventCreateManyInput } from '@sammo-ts/infra';
|
import {
|
||||||
|
createGamePostgresConnector,
|
||||||
|
type GamePrisma,
|
||||||
|
type InputJsonValue,
|
||||||
|
type TurnEngineEventCreateManyInput,
|
||||||
|
} from '@sammo-ts/infra';
|
||||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
import {
|
import {
|
||||||
buildScenarioBootstrap,
|
buildScenarioBootstrap,
|
||||||
@@ -47,6 +52,8 @@ export interface ScenarioInstallOptions {
|
|||||||
preopenAt?: Date | null;
|
preopenAt?: Date | null;
|
||||||
season?: number;
|
season?: number;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
installOperationId?: string;
|
||||||
|
installCommitSha?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScenarioSeedOptions {
|
export interface ScenarioSeedOptions {
|
||||||
@@ -63,38 +70,17 @@ export interface ScenarioSeedOptions {
|
|||||||
includeNeutralNationInSeed?: boolean;
|
includeNeutralNationInSeed?: boolean;
|
||||||
defaultGeneralGold?: number;
|
defaultGeneralGold?: number;
|
||||||
defaultGeneralRice?: number;
|
defaultGeneralRice?: number;
|
||||||
|
onBeforeCommit?: (transaction: GamePrisma.TransactionClient, result: ScenarioSeedResult) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScenarioSeedResult {
|
export interface ScenarioSeedResult {
|
||||||
seed: WorldSeedPayload;
|
seed: WorldSeedPayload;
|
||||||
warnings: ScenarioBootstrapWarning[];
|
warnings: ScenarioBootstrapWarning[];
|
||||||
|
applied: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
||||||
|
|
||||||
type RawQueryClient = {
|
|
||||||
$queryRawUnsafe<T>(query: string): Promise<T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveSchemaName = (databaseUrl: string): string => {
|
|
||||||
try {
|
|
||||||
return new URL(databaseUrl).searchParams.get('schema') ?? 'public';
|
|
||||||
} catch {
|
|
||||||
return 'public';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEventTable = async (prisma: RawQueryClient, schema: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const result = await prisma.$queryRawUnsafe<Array<{ regclass: string | null }>>(
|
|
||||||
`SELECT to_regclass('${schema}.event')::text as regclass`
|
|
||||||
);
|
|
||||||
return Array.isArray(result) && result.length > 0 && result[0]?.regclass !== null;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (date: Date): string => {
|
const formatDateTime = (date: Date): string => {
|
||||||
const pad = (value: number): string => String(value).padStart(2, '0');
|
const pad = (value: number): string => String(value).padStart(2, '0');
|
||||||
return [
|
return [
|
||||||
@@ -280,6 +266,12 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
|||||||
if (typeof install?.serverId === 'string' && install.serverId.trim()) {
|
if (typeof install?.serverId === 'string' && install.serverId.trim()) {
|
||||||
worldMeta.serverId = install.serverId.trim();
|
worldMeta.serverId = install.serverId.trim();
|
||||||
}
|
}
|
||||||
|
if (typeof install?.installOperationId === 'string' && install.installOperationId.trim()) {
|
||||||
|
worldMeta.installOperationId = install.installOperationId.trim();
|
||||||
|
}
|
||||||
|
if (typeof install?.installCommitSha === 'string' && install.installCommitSha.trim()) {
|
||||||
|
worldMeta.installCommitSha = install.installCommitSha.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
|
||||||
worldMeta.hiddenSeed =
|
worldMeta.hiddenSeed =
|
||||||
@@ -300,278 +292,337 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
|
|||||||
|
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
try {
|
try {
|
||||||
const prisma = connector.prisma;
|
const result: ScenarioSeedResult = { seed, warnings, applied: true };
|
||||||
const schema = resolveSchemaName(options.databaseUrl);
|
const applied = await connector.prisma.$transaction(
|
||||||
const eventTableReady = await hasEventTable(prisma, schema);
|
async (prisma) => {
|
||||||
|
await prisma.$queryRawUnsafe(
|
||||||
if (options.resetTables ?? true) {
|
'SELECT pg_advisory_xact_lock(hashtextextended(current_schema(), 0))::text AS lock_result'
|
||||||
if (eventTableReady) {
|
);
|
||||||
await prisma.event.deleteMany();
|
const requestedInstallOperationId = worldMeta.installOperationId;
|
||||||
}
|
const requestedInstallCommitSha = worldMeta.installCommitSha;
|
||||||
await prisma.selectPoolEntry.deleteMany();
|
if (typeof requestedInstallOperationId === 'string') {
|
||||||
await prisma.generalTurn.deleteMany();
|
const existingWorld = await prisma.worldState.findFirst({ select: { meta: true } });
|
||||||
await prisma.generalTurnRevision.deleteMany();
|
const existingWorldMeta = asRecord(existingWorld?.meta);
|
||||||
await prisma.rankData.deleteMany();
|
if (existingWorldMeta.installOperationId === requestedInstallOperationId) {
|
||||||
await prisma.generalAccessLog.deleteMany();
|
if (existingWorldMeta.installCommitSha !== requestedInstallCommitSha) {
|
||||||
await prisma.diplomacy.deleteMany();
|
throw new Error(
|
||||||
await prisma.general.deleteMany();
|
`Install operation ${requestedInstallOperationId} belongs to a different source commit.`
|
||||||
await prisma.troop.deleteMany();
|
|
||||||
await prisma.city.deleteMany();
|
|
||||||
await prisma.nation.deleteMany();
|
|
||||||
await prisma.worldState.deleteMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.worldState.create({
|
|
||||||
data: {
|
|
||||||
scenarioCode: String(options.scenarioId),
|
|
||||||
currentYear: startState.currentYear,
|
|
||||||
currentMonth: startState.currentMonth,
|
|
||||||
tickSeconds,
|
|
||||||
config: asJson({ ...scenarioConfig, ...worldConfig }),
|
|
||||||
meta: asJson(worldMeta),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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 },
|
|
||||||
create: {
|
|
||||||
serverId: worldMeta.serverId,
|
|
||||||
date: now,
|
|
||||||
winnerNation: null,
|
|
||||||
map: scenario.config.environment.mapName ?? null,
|
|
||||||
season:
|
|
||||||
typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
|
|
||||||
? Math.floor(worldMeta.season)
|
|
||||||
: 1,
|
|
||||||
scenario: options.scenarioId,
|
|
||||||
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
|
||||||
env: asJson({
|
|
||||||
config: scenarioConfig,
|
|
||||||
meta: archivedWorldMeta,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
date: now,
|
|
||||||
winnerNation: null,
|
|
||||||
map: scenario.config.environment.mapName ?? null,
|
|
||||||
season:
|
|
||||||
typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
|
|
||||||
? Math.floor(worldMeta.season)
|
|
||||||
: 1,
|
|
||||||
scenario: options.scenarioId,
|
|
||||||
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
|
||||||
env: asJson({
|
|
||||||
config: scenarioConfig,
|
|
||||||
meta: archivedWorldMeta,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seed.nations.length > 0) {
|
|
||||||
await prisma.nation.createMany({
|
|
||||||
data: seed.nations.map((nation) => ({
|
|
||||||
id: nation.id,
|
|
||||||
name: nation.name,
|
|
||||||
color: nation.color,
|
|
||||||
capitalCityId: nation.capitalCityId ?? null,
|
|
||||||
gold: nation.gold,
|
|
||||||
rice: nation.rice,
|
|
||||||
tech: nation.tech,
|
|
||||||
level: nation.level,
|
|
||||||
typeCode: nation.typeCode,
|
|
||||||
meta: asJson({
|
|
||||||
infoText: nation.infoText,
|
|
||||||
cityIds: nation.cityIds,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seed.cities.length > 0) {
|
|
||||||
await prisma.city.createMany({
|
|
||||||
data: seed.cities.map((city) => ({
|
|
||||||
id: city.id,
|
|
||||||
name: city.name,
|
|
||||||
level: city.level,
|
|
||||||
nationId: city.nationId,
|
|
||||||
supplyState: city.supplyState,
|
|
||||||
frontState: city.frontState,
|
|
||||||
population: city.population,
|
|
||||||
populationMax: city.populationMax,
|
|
||||||
agriculture: city.agriculture,
|
|
||||||
agricultureMax: city.agricultureMax,
|
|
||||||
commerce: city.commerce,
|
|
||||||
commerceMax: city.commerceMax,
|
|
||||||
security: city.security,
|
|
||||||
securityMax: city.securityMax,
|
|
||||||
trust: city.trust,
|
|
||||||
trade: city.trade,
|
|
||||||
defence: city.defence,
|
|
||||||
defenceMax: city.defenceMax,
|
|
||||||
wall: city.wall,
|
|
||||||
wallMax: city.wallMax,
|
|
||||||
region: city.region,
|
|
||||||
conflict: asJson({}),
|
|
||||||
meta: asJson({
|
|
||||||
position: city.position,
|
|
||||||
connections: city.connections,
|
|
||||||
state: city.state,
|
|
||||||
...city.meta,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seed.generals.length > 0) {
|
|
||||||
await prisma.general.createMany({
|
|
||||||
data: seed.generals.map((general) => ({
|
|
||||||
id: general.id,
|
|
||||||
name: general.name,
|
|
||||||
nationId: general.nationId,
|
|
||||||
cityId: general.cityId,
|
|
||||||
npcState: general.npcType,
|
|
||||||
affinity: general.affinity,
|
|
||||||
bornYear: general.birthYear,
|
|
||||||
deadYear: general.deathYear,
|
|
||||||
picture: general.picture === null ? null : String(general.picture),
|
|
||||||
leadership: general.stats.leadership,
|
|
||||||
strength: general.stats.strength,
|
|
||||||
intel: general.stats.intelligence,
|
|
||||||
officerLevel: general.officerLevel,
|
|
||||||
gold: generalGold,
|
|
||||||
rice: generalRice,
|
|
||||||
crewTypeId: general.crewTypeId,
|
|
||||||
horseCode: general.horse ?? 'None',
|
|
||||||
weaponCode: general.weapon ?? 'None',
|
|
||||||
bookCode: general.book ?? 'None',
|
|
||||||
itemCode: general.item ?? 'None',
|
|
||||||
turnTime: now,
|
|
||||||
age: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
|
|
||||||
startAge: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
|
|
||||||
personalCode: general.personality ?? 'None',
|
|
||||||
specialCode: general.special ?? 'None',
|
|
||||||
special2Code: general.specialWar ?? 'None',
|
|
||||||
lastTurn: asJson({}),
|
|
||||||
meta: asJson(
|
|
||||||
(() => {
|
|
||||||
const meta = { ...general.meta } as Record<string, unknown>;
|
|
||||||
if (typeof meta.birthYear !== 'number' || !Number.isFinite(meta.birthYear)) {
|
|
||||||
meta.birthYear = general.birthYear;
|
|
||||||
}
|
|
||||||
delete meta.deathYear;
|
|
||||||
delete meta.deadYear;
|
|
||||||
const fallbackKillturn =
|
|
||||||
typeof meta.killturn === 'number' && Number.isFinite(meta.killturn) ? meta.killturn : 0;
|
|
||||||
const deathMonth =
|
|
||||||
typeof meta.deathMonth === 'number' &&
|
|
||||||
Number.isInteger(meta.deathMonth) &&
|
|
||||||
meta.deathMonth >= 1 &&
|
|
||||||
meta.deathMonth <= 12
|
|
||||||
? meta.deathMonth
|
|
||||||
: resolveScenarioGeneralDeathMonth({
|
|
||||||
scenarioTitle: String(seed.scenarioMeta?.title ?? ''),
|
|
||||||
startYear: seed.scenarioMeta?.startYear ?? null,
|
|
||||||
contextLabel: typeof meta.source === 'string' ? meta.source : 'general',
|
|
||||||
generalId: general.id,
|
|
||||||
generalName: general.name,
|
|
||||||
deathYear: general.deathYear,
|
|
||||||
});
|
|
||||||
const killturn = resolveKillturnFromDeathYear(
|
|
||||||
startState.currentYear,
|
|
||||||
startState.currentMonth,
|
|
||||||
general.deathYear,
|
|
||||||
deathMonth,
|
|
||||||
fallbackKillturn
|
|
||||||
);
|
);
|
||||||
return {
|
}
|
||||||
...meta,
|
return false;
|
||||||
killturn,
|
}
|
||||||
deathMonth,
|
}
|
||||||
npcType: general.npcType,
|
if (options.resetTables ?? true) {
|
||||||
crewTypeId: general.crewTypeId,
|
await prisma.inputEvent.deleteMany();
|
||||||
} satisfies GeneralMeta;
|
await prisma.turnDaemonLease.deleteMany();
|
||||||
})()
|
await prisma.npcSelectionToken.deleteMany();
|
||||||
),
|
await prisma.trafficPeriodGeneral.deleteMany();
|
||||||
penalty: asJson({}),
|
await prisma.trafficPeriod.deleteMany();
|
||||||
})),
|
await prisma.messageReadState.deleteMany();
|
||||||
});
|
await prisma.message.deleteMany();
|
||||||
}
|
await prisma.nationTurn.deleteMany();
|
||||||
|
await prisma.nationTurnRevision.deleteMany();
|
||||||
if (seed.troops.length > 0) {
|
await prisma.generalTurn.deleteMany();
|
||||||
await prisma.troop.createMany({
|
await prisma.generalTurnRevision.deleteMany();
|
||||||
data: seed.troops.map((troop) => ({
|
await prisma.selectPoolEntry.deleteMany();
|
||||||
troopLeaderId: troop.id,
|
await prisma.generalAccessLog.deleteMany();
|
||||||
nationId: troop.nationId,
|
await prisma.rankData.deleteMany();
|
||||||
name: troop.name,
|
await prisma.diplomacyLetter.deleteMany();
|
||||||
})),
|
await prisma.diplomacy.deleteMany();
|
||||||
});
|
await prisma.auctionBid.deleteMany();
|
||||||
}
|
await prisma.auction.deleteMany();
|
||||||
|
await prisma.nationBet.deleteMany();
|
||||||
const diplomacyMap = new Map<
|
await prisma.nationBetting.deleteMany();
|
||||||
string,
|
await prisma.boardComment.deleteMany();
|
||||||
{ srcNationId: number; destNationId: number; state: number; term: number }
|
await prisma.boardPost.deleteMany();
|
||||||
>();
|
await prisma.voteComment.deleteMany();
|
||||||
const nationIds = seed.nations.map((nation) => nation.id);
|
await prisma.vote.deleteMany();
|
||||||
for (const srcNationId of nationIds) {
|
await prisma.votePoll.deleteMany();
|
||||||
for (const destNationId of nationIds) {
|
await prisma.logEntry.deleteMany();
|
||||||
if (srcNationId === destNationId) {
|
await prisma.event.deleteMany();
|
||||||
continue;
|
await prisma.general.deleteMany();
|
||||||
|
await prisma.troop.deleteMany();
|
||||||
|
await prisma.city.deleteMany();
|
||||||
|
await prisma.nation.deleteMany();
|
||||||
|
await prisma.worldState.deleteMany();
|
||||||
}
|
}
|
||||||
diplomacyMap.set(`${srcNationId}:${destNationId}`, {
|
|
||||||
srcNationId,
|
|
||||||
destNationId,
|
|
||||||
state: 2,
|
|
||||||
term: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const row of seed.diplomacy) {
|
|
||||||
diplomacyMap.set(`${row.fromNationId}:${row.toNationId}`, {
|
|
||||||
srcNationId: row.fromNationId,
|
|
||||||
destNationId: row.toNationId,
|
|
||||||
state: row.state,
|
|
||||||
term: row.durationMonths,
|
|
||||||
});
|
|
||||||
diplomacyMap.set(`${row.toNationId}:${row.fromNationId}`, {
|
|
||||||
srcNationId: row.toNationId,
|
|
||||||
destNationId: row.fromNationId,
|
|
||||||
state: row.state,
|
|
||||||
term: row.durationMonths,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const diplomacyRows = Array.from(diplomacyMap.values());
|
|
||||||
if (diplomacyRows.length > 0) {
|
|
||||||
await prisma.diplomacy.createMany({
|
|
||||||
data: diplomacyRows.map((row) => ({
|
|
||||||
srcNationId: row.srcNationId,
|
|
||||||
destNationId: row.destNationId,
|
|
||||||
stateCode: row.state,
|
|
||||||
term: row.term,
|
|
||||||
meta: asJson({}),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventRows = buildEventRows(seed.events);
|
await prisma.worldState.create({
|
||||||
if (eventRows.length > 0 && eventTableReady) {
|
data: {
|
||||||
await prisma.event.createMany({
|
scenarioCode: String(options.scenarioId),
|
||||||
data: eventRows,
|
currentYear: startState.currentYear,
|
||||||
});
|
currentMonth: startState.currentMonth,
|
||||||
}
|
tickSeconds,
|
||||||
|
config: asJson({ ...scenarioConfig, ...worldConfig }),
|
||||||
|
meta: asJson(worldMeta),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const existingHistory = await prisma.gameHistory.findUnique({
|
||||||
|
where: { serverId: worldMeta.serverId },
|
||||||
|
select: { env: true },
|
||||||
|
});
|
||||||
|
const requestedInstallOperationId = worldMeta.installOperationId;
|
||||||
|
const requestedInstallCommitSha = worldMeta.installCommitSha;
|
||||||
|
if (existingHistory && typeof requestedInstallOperationId === 'string') {
|
||||||
|
const existingHistoryMeta = asRecord(asRecord(existingHistory.env).meta);
|
||||||
|
if (
|
||||||
|
existingHistoryMeta.installOperationId !== requestedInstallOperationId ||
|
||||||
|
existingHistoryMeta.installCommitSha !== requestedInstallCommitSha
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Game history serverId collision for install operation ${requestedInstallOperationId}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.gameHistory.upsert({
|
||||||
|
where: { serverId: worldMeta.serverId },
|
||||||
|
create: {
|
||||||
|
serverId: worldMeta.serverId,
|
||||||
|
date: now,
|
||||||
|
winnerNation: null,
|
||||||
|
map: scenario.config.environment.mapName ?? null,
|
||||||
|
season:
|
||||||
|
typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
|
||||||
|
? Math.floor(worldMeta.season)
|
||||||
|
: 1,
|
||||||
|
scenario: options.scenarioId,
|
||||||
|
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
||||||
|
env: asJson({
|
||||||
|
config: scenarioConfig,
|
||||||
|
meta: archivedWorldMeta,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
date: now,
|
||||||
|
winnerNation: null,
|
||||||
|
map: scenario.config.environment.mapName ?? null,
|
||||||
|
season:
|
||||||
|
typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
|
||||||
|
? Math.floor(worldMeta.season)
|
||||||
|
: 1,
|
||||||
|
scenario: options.scenarioId,
|
||||||
|
scenarioName: String(seed.scenarioMeta?.title ?? ''),
|
||||||
|
env: asJson({
|
||||||
|
config: scenarioConfig,
|
||||||
|
meta: archivedWorldMeta,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seed.nations.length > 0) {
|
||||||
|
await prisma.nation.createMany({
|
||||||
|
data: seed.nations.map((nation) => ({
|
||||||
|
id: nation.id,
|
||||||
|
name: nation.name,
|
||||||
|
color: nation.color,
|
||||||
|
capitalCityId: nation.capitalCityId ?? null,
|
||||||
|
gold: nation.gold,
|
||||||
|
rice: nation.rice,
|
||||||
|
tech: nation.tech,
|
||||||
|
level: nation.level,
|
||||||
|
typeCode: nation.typeCode,
|
||||||
|
meta: asJson({
|
||||||
|
infoText: nation.infoText,
|
||||||
|
cityIds: nation.cityIds,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seed.cities.length > 0) {
|
||||||
|
await prisma.city.createMany({
|
||||||
|
data: seed.cities.map((city) => ({
|
||||||
|
id: city.id,
|
||||||
|
name: city.name,
|
||||||
|
level: city.level,
|
||||||
|
nationId: city.nationId,
|
||||||
|
supplyState: city.supplyState,
|
||||||
|
frontState: city.frontState,
|
||||||
|
population: city.population,
|
||||||
|
populationMax: city.populationMax,
|
||||||
|
agriculture: city.agriculture,
|
||||||
|
agricultureMax: city.agricultureMax,
|
||||||
|
commerce: city.commerce,
|
||||||
|
commerceMax: city.commerceMax,
|
||||||
|
security: city.security,
|
||||||
|
securityMax: city.securityMax,
|
||||||
|
trust: city.trust,
|
||||||
|
trade: city.trade,
|
||||||
|
defence: city.defence,
|
||||||
|
defenceMax: city.defenceMax,
|
||||||
|
wall: city.wall,
|
||||||
|
wallMax: city.wallMax,
|
||||||
|
region: city.region,
|
||||||
|
conflict: asJson({}),
|
||||||
|
meta: asJson({
|
||||||
|
position: city.position,
|
||||||
|
connections: city.connections,
|
||||||
|
state: city.state,
|
||||||
|
...city.meta,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seed.generals.length > 0) {
|
||||||
|
await prisma.general.createMany({
|
||||||
|
data: seed.generals.map((general) => ({
|
||||||
|
id: general.id,
|
||||||
|
name: general.name,
|
||||||
|
nationId: general.nationId,
|
||||||
|
cityId: general.cityId,
|
||||||
|
npcState: general.npcType,
|
||||||
|
affinity: general.affinity,
|
||||||
|
bornYear: general.birthYear,
|
||||||
|
deadYear: general.deathYear,
|
||||||
|
picture: general.picture === null ? null : String(general.picture),
|
||||||
|
leadership: general.stats.leadership,
|
||||||
|
strength: general.stats.strength,
|
||||||
|
intel: general.stats.intelligence,
|
||||||
|
officerLevel: general.officerLevel,
|
||||||
|
gold: generalGold,
|
||||||
|
rice: generalRice,
|
||||||
|
crewTypeId: general.crewTypeId,
|
||||||
|
horseCode: general.horse ?? 'None',
|
||||||
|
weaponCode: general.weapon ?? 'None',
|
||||||
|
bookCode: general.book ?? 'None',
|
||||||
|
itemCode: general.item ?? 'None',
|
||||||
|
turnTime: now,
|
||||||
|
age: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
|
||||||
|
startAge: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
|
||||||
|
personalCode: general.personality ?? 'None',
|
||||||
|
specialCode: general.special ?? 'None',
|
||||||
|
special2Code: general.specialWar ?? 'None',
|
||||||
|
lastTurn: asJson({}),
|
||||||
|
meta: asJson(
|
||||||
|
(() => {
|
||||||
|
const meta = { ...general.meta } as Record<string, unknown>;
|
||||||
|
if (typeof meta.birthYear !== 'number' || !Number.isFinite(meta.birthYear)) {
|
||||||
|
meta.birthYear = general.birthYear;
|
||||||
|
}
|
||||||
|
delete meta.deathYear;
|
||||||
|
delete meta.deadYear;
|
||||||
|
const fallbackKillturn =
|
||||||
|
typeof meta.killturn === 'number' && Number.isFinite(meta.killturn)
|
||||||
|
? meta.killturn
|
||||||
|
: 0;
|
||||||
|
const deathMonth =
|
||||||
|
typeof meta.deathMonth === 'number' &&
|
||||||
|
Number.isInteger(meta.deathMonth) &&
|
||||||
|
meta.deathMonth >= 1 &&
|
||||||
|
meta.deathMonth <= 12
|
||||||
|
? meta.deathMonth
|
||||||
|
: resolveScenarioGeneralDeathMonth({
|
||||||
|
scenarioTitle: String(seed.scenarioMeta?.title ?? ''),
|
||||||
|
startYear: seed.scenarioMeta?.startYear ?? null,
|
||||||
|
contextLabel:
|
||||||
|
typeof meta.source === 'string' ? meta.source : 'general',
|
||||||
|
generalId: general.id,
|
||||||
|
generalName: general.name,
|
||||||
|
deathYear: general.deathYear,
|
||||||
|
});
|
||||||
|
const killturn = resolveKillturnFromDeathYear(
|
||||||
|
startState.currentYear,
|
||||||
|
startState.currentMonth,
|
||||||
|
general.deathYear,
|
||||||
|
deathMonth,
|
||||||
|
fallbackKillturn
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
killturn,
|
||||||
|
deathMonth,
|
||||||
|
npcType: general.npcType,
|
||||||
|
crewTypeId: general.crewTypeId,
|
||||||
|
} satisfies GeneralMeta;
|
||||||
|
})()
|
||||||
|
),
|
||||||
|
penalty: asJson({}),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seed.troops.length > 0) {
|
||||||
|
await prisma.troop.createMany({
|
||||||
|
data: seed.troops.map((troop) => ({
|
||||||
|
troopLeaderId: troop.id,
|
||||||
|
nationId: troop.nationId,
|
||||||
|
name: troop.name,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const diplomacyMap = new Map<
|
||||||
|
string,
|
||||||
|
{ srcNationId: number; destNationId: number; state: number; term: number }
|
||||||
|
>();
|
||||||
|
const nationIds = seed.nations.map((nation) => nation.id);
|
||||||
|
for (const srcNationId of nationIds) {
|
||||||
|
for (const destNationId of nationIds) {
|
||||||
|
if (srcNationId === destNationId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
diplomacyMap.set(`${srcNationId}:${destNationId}`, {
|
||||||
|
srcNationId,
|
||||||
|
destNationId,
|
||||||
|
state: 2,
|
||||||
|
term: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const row of seed.diplomacy) {
|
||||||
|
diplomacyMap.set(`${row.fromNationId}:${row.toNationId}`, {
|
||||||
|
srcNationId: row.fromNationId,
|
||||||
|
destNationId: row.toNationId,
|
||||||
|
state: row.state,
|
||||||
|
term: row.durationMonths,
|
||||||
|
});
|
||||||
|
diplomacyMap.set(`${row.toNationId}:${row.fromNationId}`, {
|
||||||
|
srcNationId: row.toNationId,
|
||||||
|
destNationId: row.fromNationId,
|
||||||
|
state: row.state,
|
||||||
|
term: row.durationMonths,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const diplomacyRows = Array.from(diplomacyMap.values());
|
||||||
|
if (diplomacyRows.length > 0) {
|
||||||
|
await prisma.diplomacy.createMany({
|
||||||
|
data: diplomacyRows.map((row) => ({
|
||||||
|
srcNationId: row.srcNationId,
|
||||||
|
destNationId: row.destNationId,
|
||||||
|
stateCode: row.state,
|
||||||
|
term: row.term,
|
||||||
|
meta: asJson({}),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventRows = buildEventRows(seed.events);
|
||||||
|
if (eventRows.length > 0) {
|
||||||
|
await prisma.event.createMany({
|
||||||
|
data: eventRows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await options.onBeforeCommit?.(prisma, result);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{ maxWait: 10_000, timeout: 60_000 }
|
||||||
|
);
|
||||||
|
result.applied = applied;
|
||||||
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
await connector.disconnect();
|
await connector.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { seed, warnings };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ type ScenarioSeederPrismaClient = {
|
|||||||
};
|
};
|
||||||
selectPoolEntry: {
|
selectPoolEntry: {
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
findFirst(args: {
|
findFirst(args: { orderBy: { id: 'asc' | 'desc' } }): Promise<{ uniqueName: string; info: unknown } | null>;
|
||||||
orderBy: { id: 'asc' | 'desc' };
|
|
||||||
}): Promise<{ uniqueName: string; info: unknown } | null>;
|
|
||||||
};
|
};
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
@@ -397,9 +395,7 @@ describeDb('scenario database seed', () => {
|
|||||||
hiddenSeed: 'scenario-seeder-explicit-hidden-seed',
|
hiddenSeed: 'scenario-seeder-explicit-hidden-seed',
|
||||||
});
|
});
|
||||||
const explicitHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
const explicitHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
||||||
expect((explicitHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty(
|
expect((explicitHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty('hiddenSeed');
|
||||||
'hiddenSeed'
|
|
||||||
);
|
|
||||||
|
|
||||||
delete process.env[envName];
|
delete process.env[envName];
|
||||||
await seedScenarioToDatabase({
|
await seedScenarioToDatabase({
|
||||||
@@ -412,9 +408,7 @@ describeDb('scenario database seed', () => {
|
|||||||
expect(randomSeed).toMatch(/^[0-9a-f]{32}$/);
|
expect(randomSeed).toMatch(/^[0-9a-f]{32}$/);
|
||||||
expect(randomSeed).not.toBe('scenario-seeder-explicit-hidden-seed');
|
expect(randomSeed).not.toBe('scenario-seeder-explicit-hidden-seed');
|
||||||
const randomHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
const randomHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
||||||
expect((randomHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty(
|
expect((randomHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty('hiddenSeed');
|
||||||
'hiddenSeed'
|
|
||||||
);
|
|
||||||
await prisma.gameHistory.deleteMany({ where: { serverId } });
|
await prisma.gameHistory.deleteMany({ where: { serverId } });
|
||||||
} finally {
|
} finally {
|
||||||
if (originalSeed === undefined) {
|
if (originalSeed === undefined) {
|
||||||
@@ -425,6 +419,399 @@ describeDb('scenario database seed', () => {
|
|||||||
await connector.disconnect();
|
await connector.disconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('serializes and skips the same committed install generation inside the seed transaction', async () => {
|
||||||
|
const envName = 'INTEGRATION_WORLD_SEED';
|
||||||
|
const originalSeed = process.env[envName];
|
||||||
|
const serverId = 'scenario-seeder-idempotent-generation';
|
||||||
|
const installOperationId = 'scenario-seeder-idempotent-operation';
|
||||||
|
const installCommitSha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||||
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
|
try {
|
||||||
|
process.env[envName] = 'scenario-seeder-idempotent-hidden-seed';
|
||||||
|
const results = await Promise.all([
|
||||||
|
seedScenarioToDatabase({
|
||||||
|
scenarioId: 1010,
|
||||||
|
databaseUrl,
|
||||||
|
installOptions: { serverId, installOperationId, installCommitSha },
|
||||||
|
}),
|
||||||
|
seedScenarioToDatabase({
|
||||||
|
scenarioId: 1010,
|
||||||
|
databaseUrl,
|
||||||
|
installOptions: { serverId, installOperationId, installCommitSha },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(results.map(({ applied }) => applied).sort()).toEqual([false, true]);
|
||||||
|
|
||||||
|
await connector.connect();
|
||||||
|
const worldBeforeMismatch = await connector.prisma.worldState.findFirstOrThrow();
|
||||||
|
expect(worldBeforeMismatch.meta).toMatchObject({
|
||||||
|
hiddenSeed: 'scenario-seeder-idempotent-hidden-seed',
|
||||||
|
installOperationId,
|
||||||
|
installCommitSha,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
seedScenarioToDatabase({
|
||||||
|
scenarioId: 903,
|
||||||
|
databaseUrl,
|
||||||
|
installOptions: {
|
||||||
|
serverId,
|
||||||
|
installOperationId,
|
||||||
|
installCommitSha: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow('belongs to a different source commit');
|
||||||
|
await expect(connector.prisma.worldState.findFirstOrThrow()).resolves.toEqual(worldBeforeMismatch);
|
||||||
|
await expect(connector.prisma.gameHistory.count({ where: { serverId } })).resolves.toBe(1);
|
||||||
|
} finally {
|
||||||
|
if (originalSeed === undefined) {
|
||||||
|
delete process.env[envName];
|
||||||
|
} else {
|
||||||
|
process.env[envName] = originalSeed;
|
||||||
|
}
|
||||||
|
await connector.prisma.gameHistory.deleteMany({ where: { serverId } });
|
||||||
|
await connector.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rolls back the complete season replacement when the final seed hook fails', async () => {
|
||||||
|
const envName = 'INTEGRATION_WORLD_SEED';
|
||||||
|
const originalSeed = process.env[envName];
|
||||||
|
const baselineServerId = 'scenario-seeder-rollback-baseline';
|
||||||
|
const failedServerId = 'scenario-seeder-rollback-failed';
|
||||||
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
|
try {
|
||||||
|
process.env[envName] = 'scenario-seeder-rollback-hidden-seed';
|
||||||
|
await seedScenarioToDatabase({
|
||||||
|
scenarioId: 1010,
|
||||||
|
databaseUrl,
|
||||||
|
now: new Date('2031-01-01T00:00:00.000Z'),
|
||||||
|
installOptions: {
|
||||||
|
serverId: baselineServerId,
|
||||||
|
installOperationId: 'baseline-operation',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await connector.connect();
|
||||||
|
const prisma = connector.prisma;
|
||||||
|
const readSnapshot = async () => ({
|
||||||
|
world: await prisma.worldState.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
nations: await prisma.nation.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
cities: await prisma.city.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
generals: await prisma.general.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
troops: await prisma.troop.findMany({ orderBy: { troopLeaderId: 'asc' } }),
|
||||||
|
diplomacy: await prisma.diplomacy.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
events: await prisma.event.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
history: await prisma.gameHistory.findMany({ orderBy: { id: 'asc' } }),
|
||||||
|
});
|
||||||
|
const before = await readSnapshot();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
seedScenarioToDatabase({
|
||||||
|
scenarioId: 903,
|
||||||
|
databaseUrl,
|
||||||
|
now: new Date('2032-01-01T00:00:00.000Z'),
|
||||||
|
installOptions: {
|
||||||
|
serverId: baselineServerId,
|
||||||
|
installOperationId: 'colliding-operation',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Game history serverId collision');
|
||||||
|
expect(await readSnapshot()).toEqual(before);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
seedScenarioToDatabase({
|
||||||
|
scenarioId: 903,
|
||||||
|
databaseUrl,
|
||||||
|
now: new Date('2032-02-02T00:00:00.000Z'),
|
||||||
|
installOptions: {
|
||||||
|
serverId: failedServerId,
|
||||||
|
installOperationId: 'failed-operation',
|
||||||
|
},
|
||||||
|
onBeforeCommit: async () => {
|
||||||
|
throw new Error('injected final seed failure');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow('injected final seed failure');
|
||||||
|
|
||||||
|
expect(await readSnapshot()).toEqual(before);
|
||||||
|
await expect(prisma.gameHistory.findUnique({ where: { serverId: failedServerId } })).resolves.toBeNull();
|
||||||
|
} finally {
|
||||||
|
if (originalSeed === undefined) {
|
||||||
|
delete process.env[envName];
|
||||||
|
} else {
|
||||||
|
process.env[envName] = originalSeed;
|
||||||
|
}
|
||||||
|
await connector.prisma.gameHistory.deleteMany({
|
||||||
|
where: { serverId: { in: [baselineServerId, failedServerId] } },
|
||||||
|
});
|
||||||
|
await connector.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears current-season services while preserving archive and diagnostic data', async () => {
|
||||||
|
const marker = 'scenario-reset-boundary';
|
||||||
|
const serverId = 'rst-boundary-server';
|
||||||
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
|
await seedScenarioToDatabase({ scenarioId: 1010, databaseUrl });
|
||||||
|
await connector.connect();
|
||||||
|
const prisma = connector.prisma;
|
||||||
|
try {
|
||||||
|
const world = await prisma.worldState.findFirstOrThrow();
|
||||||
|
const general = await prisma.general.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
|
const nation = await prisma.nation.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
|
const trafficPeriod = await prisma.trafficPeriod.create({
|
||||||
|
data: {
|
||||||
|
worldStateId: world.id,
|
||||||
|
year: 999,
|
||||||
|
month: 12,
|
||||||
|
startedAt: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
lastRefresh: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.trafficPeriodGeneral.create({
|
||||||
|
data: {
|
||||||
|
periodId: trafficPeriod.id,
|
||||||
|
generalId: general.id,
|
||||||
|
refresh: 1,
|
||||||
|
lastRefresh: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.inputEvent.create({
|
||||||
|
data: { requestId: marker, target: 'API', eventType: marker },
|
||||||
|
});
|
||||||
|
await prisma.turnDaemonLease.create({
|
||||||
|
data: {
|
||||||
|
profile: marker,
|
||||||
|
ownerId: marker,
|
||||||
|
leaseUntil: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.npcSelectionToken.create({
|
||||||
|
data: {
|
||||||
|
ownerUserId: marker,
|
||||||
|
validUntil: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
pickMoreFrom: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
pickResult: {},
|
||||||
|
nonce: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.messageReadState.create({ data: { generalId: general.id } });
|
||||||
|
await prisma.message.create({
|
||||||
|
data: {
|
||||||
|
mailbox: general.id,
|
||||||
|
type: marker,
|
||||||
|
src: general.id,
|
||||||
|
dest: general.id,
|
||||||
|
time: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
validUntil: new Date('2033-02-01T00:00:00.000Z'),
|
||||||
|
message: { marker },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.nationTurn.create({
|
||||||
|
data: { nationId: nation.id, officerLevel: 12, turnIdx: 0, actionCode: marker },
|
||||||
|
});
|
||||||
|
await prisma.nationTurnRevision.create({
|
||||||
|
data: { nationId: nation.id, officerLevel: 12, revision: 1 },
|
||||||
|
});
|
||||||
|
await prisma.diplomacyLetter.create({
|
||||||
|
data: {
|
||||||
|
srcNationId: nation.id,
|
||||||
|
destNationId: 0,
|
||||||
|
state: 'PROPOSED',
|
||||||
|
textBrief: marker,
|
||||||
|
textDetail: marker,
|
||||||
|
srcSignerId: general.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const auction = await prisma.auction.create({
|
||||||
|
data: {
|
||||||
|
type: 'UNIQUE_ITEM',
|
||||||
|
targetCode: marker,
|
||||||
|
hostGeneralId: general.id,
|
||||||
|
status: 'OPEN',
|
||||||
|
closeAt: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.auctionBid.create({
|
||||||
|
data: {
|
||||||
|
auctionId: auction.id,
|
||||||
|
generalId: general.id,
|
||||||
|
amount: 1,
|
||||||
|
eventId: marker,
|
||||||
|
eventAt: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const bettingId = 990_731;
|
||||||
|
await prisma.nationBetting.create({
|
||||||
|
data: {
|
||||||
|
id: bettingId,
|
||||||
|
name: marker,
|
||||||
|
selectCount: 1,
|
||||||
|
openYearMonth: 99901,
|
||||||
|
closeYearMonth: 99902,
|
||||||
|
candidates: [{ id: nation.id }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.nationBet.create({
|
||||||
|
data: {
|
||||||
|
bettingId,
|
||||||
|
generalId: general.id,
|
||||||
|
userId: marker,
|
||||||
|
selection: [nation.id],
|
||||||
|
selectionKey: String(nation.id),
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const post = await prisma.boardPost.create({
|
||||||
|
data: {
|
||||||
|
nationId: nation.id,
|
||||||
|
authorGeneralId: general.id,
|
||||||
|
authorName: marker,
|
||||||
|
title: marker,
|
||||||
|
contentHtml: marker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.boardComment.create({
|
||||||
|
data: {
|
||||||
|
postId: post.id,
|
||||||
|
nationId: nation.id,
|
||||||
|
authorGeneralId: general.id,
|
||||||
|
authorName: marker,
|
||||||
|
contentText: marker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const poll = await prisma.votePoll.create({
|
||||||
|
data: {
|
||||||
|
title: marker,
|
||||||
|
options: [marker],
|
||||||
|
revealMode: 'never',
|
||||||
|
openerGeneralId: general.id,
|
||||||
|
openerName: marker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.vote.create({
|
||||||
|
data: { voteId: poll.id, generalId: general.id, nationId: nation.id, selection: [0] },
|
||||||
|
});
|
||||||
|
await prisma.voteComment.create({
|
||||||
|
data: {
|
||||||
|
voteId: poll.id,
|
||||||
|
generalId: general.id,
|
||||||
|
nationId: nation.id,
|
||||||
|
generalName: marker,
|
||||||
|
nationName: marker,
|
||||||
|
text: marker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.logEntry.create({
|
||||||
|
data: { scope: 'SYSTEM', category: 'HISTORY', year: 999, month: 12, text: marker },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.errorLog.create({ data: { category: marker, message: marker } });
|
||||||
|
await prisma.inheritancePoint.create({ data: { userId: marker, key: marker, value: 1 } });
|
||||||
|
await prisma.inheritanceLog.create({
|
||||||
|
data: { userId: marker, serverId, year: 999, month: 12, text: marker },
|
||||||
|
});
|
||||||
|
await prisma.inheritanceResult.create({
|
||||||
|
data: { serverId, owner: marker, generalId: general.id, year: 999, month: 12, value: { marker } },
|
||||||
|
});
|
||||||
|
await prisma.inheritanceUserState.create({ data: { userId: marker, meta: { marker } } });
|
||||||
|
await prisma.gameHistory.create({
|
||||||
|
data: {
|
||||||
|
serverId,
|
||||||
|
date: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
season: 1,
|
||||||
|
scenario: 1010,
|
||||||
|
scenarioName: marker,
|
||||||
|
env: { marker },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.oldNation.create({ data: { serverId, nation: 1, sourceId: 1, data: { marker } } });
|
||||||
|
await prisma.oldGeneral.create({
|
||||||
|
data: {
|
||||||
|
serverId,
|
||||||
|
generalNo: general.id,
|
||||||
|
owner: marker,
|
||||||
|
name: marker,
|
||||||
|
lastYearMonth: 99912,
|
||||||
|
turnTime: new Date('2033-01-01T00:00:00.000Z'),
|
||||||
|
data: { marker },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.emperor.create({ data: { serverId, name: marker, history: { marker }, aux: { marker } } });
|
||||||
|
await prisma.yearbookHistory.create({
|
||||||
|
data: {
|
||||||
|
profileName: marker,
|
||||||
|
year: 999,
|
||||||
|
month: 12,
|
||||||
|
map: {},
|
||||||
|
nations: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.legacyGameStorage.create({
|
||||||
|
data: { sourceId: 990_731, namespace: marker, key: marker, value: {}, scope: marker },
|
||||||
|
});
|
||||||
|
await prisma.hallOfFame.create({
|
||||||
|
data: {
|
||||||
|
serverId,
|
||||||
|
season: 1,
|
||||||
|
scenario: 1010,
|
||||||
|
generalNo: general.id,
|
||||||
|
type: 'reset-boundary',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await seedScenarioToDatabase({ scenarioId: 903, databaseUrl });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
Promise.all([
|
||||||
|
prisma.inputEvent.count({ where: { requestId: marker } }),
|
||||||
|
prisma.turnDaemonLease.count({ where: { profile: marker } }),
|
||||||
|
prisma.npcSelectionToken.count({ where: { ownerUserId: marker } }),
|
||||||
|
prisma.trafficPeriod.count({ where: { id: trafficPeriod.id } }),
|
||||||
|
prisma.message.count({ where: { type: marker } }),
|
||||||
|
prisma.nationTurn.count({ where: { actionCode: marker } }),
|
||||||
|
prisma.diplomacyLetter.count({ where: { textBrief: marker } }),
|
||||||
|
prisma.auction.count({ where: { targetCode: marker } }),
|
||||||
|
prisma.nationBetting.count({ where: { id: bettingId } }),
|
||||||
|
prisma.boardPost.count({ where: { title: marker } }),
|
||||||
|
prisma.votePoll.count({ where: { title: marker } }),
|
||||||
|
prisma.logEntry.count({ where: { text: marker } }),
|
||||||
|
])
|
||||||
|
).resolves.toEqual(Array.from({ length: 12 }, () => 0));
|
||||||
|
await expect(
|
||||||
|
Promise.all([
|
||||||
|
prisma.errorLog.count({ where: { category: marker } }),
|
||||||
|
prisma.inheritancePoint.count({ where: { userId: marker } }),
|
||||||
|
prisma.inheritanceLog.count({ where: { userId: marker } }),
|
||||||
|
prisma.inheritanceResult.count({ where: { owner: marker } }),
|
||||||
|
prisma.inheritanceUserState.count({ where: { userId: marker } }),
|
||||||
|
prisma.gameHistory.count({ where: { serverId } }),
|
||||||
|
prisma.oldNation.count({ where: { serverId } }),
|
||||||
|
prisma.oldGeneral.count({ where: { serverId } }),
|
||||||
|
prisma.emperor.count({ where: { serverId } }),
|
||||||
|
prisma.yearbookHistory.count({ where: { profileName: marker } }),
|
||||||
|
prisma.legacyGameStorage.count({ where: { namespace: marker } }),
|
||||||
|
prisma.hallOfFame.count({ where: { serverId } }),
|
||||||
|
])
|
||||||
|
).resolves.toEqual(Array.from({ length: 12 }, () => 1));
|
||||||
|
} finally {
|
||||||
|
await prisma.errorLog.deleteMany({ where: { category: marker } });
|
||||||
|
await prisma.inheritancePoint.deleteMany({ where: { userId: marker } });
|
||||||
|
await prisma.inheritanceLog.deleteMany({ where: { userId: marker } });
|
||||||
|
await prisma.inheritanceResult.deleteMany({ where: { owner: marker } });
|
||||||
|
await prisma.inheritanceUserState.deleteMany({ where: { userId: marker } });
|
||||||
|
await prisma.oldNation.deleteMany({ where: { serverId } });
|
||||||
|
await prisma.oldGeneral.deleteMany({ where: { serverId } });
|
||||||
|
await prisma.emperor.deleteMany({ where: { serverId } });
|
||||||
|
await prisma.gameHistory.deleteMany({ where: { serverId } });
|
||||||
|
await prisma.yearbookHistory.deleteMany({ where: { profileName: marker } });
|
||||||
|
await prisma.legacyGameStorage.deleteMany({ where: { namespace: marker } });
|
||||||
|
await prisma.hallOfFame.deleteMany({ where: { serverId } });
|
||||||
|
await connector.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tracked scenario composition', () => {
|
describe('tracked scenario composition', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user