102 lines
5.4 KiB
TypeScript
102 lines
5.4 KiB
TypeScript
import type { Game } from './engine';
|
|
import { TERRITORIES, validLocation } from './board';
|
|
import { HomeworldCustodyError } from './homeworld-custody';
|
|
import { TERROR_STRONGHOLDS } from './moritani-terror';
|
|
import { revivalEntryParentTuple, validRevivalEntryParent } from './revival-terror-entry';
|
|
import { caladanEntryParentTuple, validCaladanEntryParent } from './caladan-arrival';
|
|
|
|
type TerrorEntry = NonNullable<Game['pendingTerrorEntry']>;
|
|
|
|
/** Original public entry facts, independent of subsequent stage or population.
|
|
* This is a consistency receipt, not an authorization token or secret digest. */
|
|
export function terrorEntrySignature(entry: TerrorEntry): string {
|
|
return JSON.stringify([
|
|
entry.candidates === undefined ? entry.token : ['stack', entry.candidates, entry.selectionSignature ?? null],
|
|
entry.entrant,
|
|
entry.territory,
|
|
entry.sector,
|
|
entry.amount,
|
|
entry.elite,
|
|
entry.cause,
|
|
entry.turn,
|
|
entry.phase,
|
|
entry.resume,
|
|
entry.ambassadorEvent ?? null,
|
|
...(entry.nexusPlacements ? [entry.nexusPlacements] : []),
|
|
...(entry.revivalArrival === undefined ? [] : [revivalEntryParentTuple(entry.revivalArrival)]),
|
|
...(entry.victoryEvent === undefined ? [] : [['caladanVictory', entry.victoryEvent]]),
|
|
...(entry.caladanArrival === undefined ? [] : [caladanEntryParentTuple(entry.caladanArrival)]),
|
|
]);
|
|
}
|
|
|
|
export function terrorSelectionSignature(entry: TerrorEntry): string {
|
|
return JSON.stringify(['terrorSelection', entry.candidates, entry.token, entry.entrant,
|
|
entry.territory, entry.sector, entry.amount, entry.elite, entry.cause,
|
|
entry.turn, entry.phase, entry.resume, entry.ambassadorEvent ?? null,
|
|
...(entry.nexusPlacements ? [entry.nexusPlacements] : []),
|
|
...(entry.revivalArrival === undefined ? [] : [revivalEntryParentTuple(entry.revivalArrival)]),
|
|
...(entry.victoryEvent === undefined ? [] : [['caladanVictory', entry.victoryEvent]]),
|
|
...(entry.caladanArrival === undefined ? [] : [caladanEntryParentTuple(entry.caladanArrival)])]);
|
|
}
|
|
|
|
/** Older saves without a receipt remain explicit legacy state. Never invent
|
|
* their original entry count from the destination's present occupants. */
|
|
export function validateTerrorEntrySignature(entry: TerrorEntry): void {
|
|
if (entry.entrySignature === undefined && entry.candidates === undefined && entry.selectionSignature === undefined && entry.nexusPlacements === undefined && entry.revivalArrival === undefined && entry.caladanArrival === undefined && entry.victoryEvent === undefined && entry.cause !== 'caladanReinforcement' && entry.stage !== 'select') return;
|
|
if (
|
|
typeof entry.entrySignature !== 'string' ||
|
|
typeof entry.token !== 'string' ||
|
|
!entry.token ||
|
|
(entry.candidates === undefined
|
|
? entry.selectionSignature !== undefined || entry.stage === 'select'
|
|
: !Array.isArray(entry.candidates) || entry.candidates.length < 2 ||
|
|
entry.candidates.some((id) => typeof id !== 'string' || !id) ||
|
|
new Set(entry.candidates).size !== entry.candidates.length ||
|
|
!entry.candidates.includes(entry.token) ||
|
|
(entry.stage === 'select'
|
|
? entry.token !== entry.candidates[0] || entry.selectionSignature !== undefined
|
|
: entry.selectionSignature !== terrorSelectionSignature(entry))) ||
|
|
typeof entry.entrant !== 'string' ||
|
|
!entry.entrant ||
|
|
(!TERROR_STRONGHOLDS.includes(entry.territory) &&
|
|
(!TERRITORIES.some(t => t.id === entry.territory) || !entry.nexusPlacements)) ||
|
|
(entry.nexusPlacements !== undefined &&
|
|
(!entry.nexusPlacements || typeof entry.nexusPlacements !== 'object' || Array.isArray(entry.nexusPlacements) ||
|
|
Object.keys(entry.nexusPlacements).length !== (entry.candidates ?? [entry.token]).length ||
|
|
(entry.candidates ?? [entry.token]).some(id => typeof entry.nexusPlacements?.[id] !== 'string' || !entry.nexusPlacements[id]))) ||
|
|
!Number.isSafeInteger(entry.sector) ||
|
|
!validLocation(entry.territory, entry.sector) ||
|
|
!Number.isSafeInteger(entry.amount) ||
|
|
entry.amount < 0 ||
|
|
!Number.isSafeInteger(entry.elite) ||
|
|
entry.elite < 0 ||
|
|
entry.elite > entry.amount ||
|
|
!Number.isSafeInteger(entry.turn) ||
|
|
entry.turn < 1 ||
|
|
!Number.isSafeInteger(entry.phase) ||
|
|
entry.phase < 0 ||
|
|
entry.phase > 8 ||
|
|
(entry.revivalArrival !== undefined && !validRevivalEntryParent(entry.revivalArrival)) ||
|
|
(entry.caladanArrival !== undefined && (!validCaladanEntryParent(entry.caladanArrival) ||
|
|
entry.victoryEvent !== undefined || entry.revivalArrival !== undefined)) ||
|
|
(entry.victoryEvent !== undefined && (typeof entry.victoryEvent !== 'string' || !entry.victoryEvent ||
|
|
entry.cause !== 'caladanReinforcement' || entry.phase !== 6 || entry.revivalArrival !== undefined)) ||
|
|
(![
|
|
'shipment',
|
|
'movement',
|
|
'guildTransport',
|
|
'advisor',
|
|
'wormRide',
|
|
'ambassador',
|
|
].includes(entry.cause) && !(entry.cause === 'homeworldRevival' && entry.revivalArrival !== undefined) &&
|
|
!(entry.cause === 'caladanReinforcement' && entry.victoryEvent !== undefined)) ||
|
|
!['none', 'wormRide', 'ambassador'].includes(entry.resume) ||
|
|
(entry.ambassadorEvent !== undefined &&
|
|
(typeof entry.ambassadorEvent !== 'string' || !entry.ambassadorEvent)) ||
|
|
entry.entrySignature !== terrorEntrySignature(entry)
|
|
)
|
|
throw new HomeworldCustodyError(
|
|
'The saved Terror entry no longer matches its original public arrival receipt.',
|
|
);
|
|
}
|