Recover saves with malformed battle reports
This commit is contained in:
@@ -117,6 +117,29 @@ try {
|
|||||||
assert(invalidStep.step === 'new', `Expected unknown campaign step to reset safely: ${JSON.stringify(invalidStep)}`);
|
assert(invalidStep.step === 'new', `Expected unknown campaign step to reset safely: ${JSON.stringify(invalidStep)}`);
|
||||||
assert(invalidStep.gold === 90, `Expected unknown-step save to keep harmless economy data: ${JSON.stringify(invalidStep)}`);
|
assert(invalidStep.gold === 90, `Expected unknown-step save to keep harmless economy data: ${JSON.stringify(invalidStep)}`);
|
||||||
|
|
||||||
|
storage.clear();
|
||||||
|
storage.set(
|
||||||
|
campaignStorageKey,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
updatedAt: '2026-07-04T00:00:00.000Z',
|
||||||
|
step: 'fourth-camp',
|
||||||
|
activeSaveSlot: 1,
|
||||||
|
gold: 320,
|
||||||
|
firstBattleReport: {
|
||||||
|
battleId: 'first-battle-zhuo-commandery',
|
||||||
|
outcome: 'victory',
|
||||||
|
completedCampVisits: { corrupted: true }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const malformedReport = loadCampaignState();
|
||||||
|
assert(
|
||||||
|
malformedReport.step === 'fourth-camp' && malformedReport.gold === 320,
|
||||||
|
`Expected malformed report to be dropped without losing campaign progress: ${JSON.stringify(malformedReport)}`
|
||||||
|
);
|
||||||
|
assert(malformedReport.firstBattleReport === undefined, `Expected malformed report to be discarded: ${JSON.stringify(malformedReport)}`);
|
||||||
|
|
||||||
storage.clear();
|
storage.clear();
|
||||||
storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' }));
|
storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' }));
|
||||||
const unsupported = loadCampaignState();
|
const unsupported = loadCampaignState();
|
||||||
@@ -137,7 +160,7 @@ try {
|
|||||||
const fallback = loadCampaignState();
|
const fallback = loadCampaignState();
|
||||||
assert(fallback.step === 'second-camp' && fallback.activeSaveSlot === 2, `Expected valid slotted save fallback: ${JSON.stringify(fallback)}`);
|
assert(fallback.step === 'second-camp' && fallback.activeSaveSlot === 2, `Expected valid slotted save fallback: ${JSON.stringify(fallback)}`);
|
||||||
|
|
||||||
console.log('Verified campaign save normalization, malformed-field recovery, unknown-step recovery, unsupported-version rejection, and slotted fallback.');
|
console.log('Verified campaign save normalization, malformed-field recovery, unknown-step and report recovery, unsupported-version rejection, and slotted fallback.');
|
||||||
} finally {
|
} finally {
|
||||||
await server.close();
|
await server.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -812,11 +812,7 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
|||||||
normalized.sortieItemAssignments = normalizeSortieItemAssignments(recordOrEmpty(normalized.sortieItemAssignments));
|
normalized.sortieItemAssignments = normalizeSortieItemAssignments(recordOrEmpty(normalized.sortieItemAssignments));
|
||||||
normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
|
normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
|
||||||
normalized.battleHistory = recordOrEmpty<CampaignBattleSettlement>(normalized.battleHistory);
|
normalized.battleHistory = recordOrEmpty<CampaignBattleSettlement>(normalized.battleHistory);
|
||||||
if (isPlainObject(normalized.firstBattleReport)) {
|
normalized.firstBattleReport = normalizeFirstBattleReport(normalized.firstBattleReport);
|
||||||
normalized.firstBattleReport = cloneReport(normalized.firstBattleReport);
|
|
||||||
} else {
|
|
||||||
normalized.firstBattleReport = undefined;
|
|
||||||
}
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,6 +874,18 @@ function normalizeCampaignStep(step: unknown): CampaignStep {
|
|||||||
return typeof step === 'string' && campaignStepIds.has(step as CampaignStep) ? step as CampaignStep : 'new';
|
return typeof step === 'string' && campaignStepIds.has(step as CampaignStep) ? step as CampaignStep : 'new';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefined {
|
||||||
|
if (!isPlainObject(report)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cloneReport(report as FirstBattleReport);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reserveTrainingFocusDefinition(focusId?: string) {
|
function reserveTrainingFocusDefinition(focusId?: string) {
|
||||||
const normalizedId = normalizeReserveTrainingFocusId(focusId);
|
const normalizedId = normalizeReserveTrainingFocusId(focusId);
|
||||||
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];
|
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user