From 20def5e151172b7ca56e85907d89d0bf969a5dee Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 11:56:24 +0900 Subject: [PATCH] Cap campaign save string lists --- .../verify-campaign-save-normalization.mjs | 33 ++++++++++++++++--- src/game/state/campaignState.ts | 11 +++++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 85b0874..fa9212e 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -207,7 +207,7 @@ try { { id: 'oath-bond', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: '2', exp: '14', battleExp: '5' }, 9 ], - completedCampDialogues: ['dialogue-a', 17, 'dialogue-a', ''], + completedCampDialogues: ['dialogue-a', 17, 'dialogue-a', '', 'x'.repeat(97)], completedCampVisits: 'visit-a', inventory: { bean: '3', ' bean ': '4', ['\uCF69']: '200', [' \uCF69 ']: '20', wine: -2, empty: 0 }, selectedSortieUnitIds: { corrupted: true }, @@ -256,6 +256,27 @@ try { `Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` ); + storage.clear(); + storage.set( + campaignStorageKey, + JSON.stringify({ + version: 1, + updatedAt: '2026-07-03T12:30:00.000Z', + step: 'third-camp', + activeSaveSlot: 1, + completedCampDialogues: Array.from({ length: 140 }, (_, index) => `dialogue-${index}`), + completedCampVisits: ['visit-a', 'x'.repeat(97), 'visit-a'] + }) + ); + const cappedStringLists = loadCampaignState(); + assert( + cappedStringLists.completedCampDialogues.length === 128 && + cappedStringLists.completedCampDialogues[0] === 'dialogue-0' && + cappedStringLists.completedCampDialogues[127] === 'dialogue-127' && + cappedStringLists.completedCampVisits.length === 1, + `Expected campaign string lists to filter long entries and cap excessive saves: ${JSON.stringify(cappedStringLists)}` + ); + storage.clear(); storage.set( campaignStorageKey, @@ -459,9 +480,9 @@ try { { id: 'broken-bond', title: 'Broken Bond', unitIds: ['liu-bei'] } ], mvp: { unitId: 'liu-bei', name: 'Liu Bei', damageDealt: '88', defeats: '2' }, - itemRewards: ['Bean', 'Bean', 10], + itemRewards: ['Bean', 'Bean', 10, 'x'.repeat(97)], campaignRewards: { - supplies: [' Bean ', 'Bean', 20], + supplies: [' Bean ', 'Bean', 20, 'x'.repeat(97)], equipment: [' Iron Sword ', 'Iron Sword'], reputation: ['Village Thanks', ' Village Thanks '], recruits: [{ unitId: ' guan-yu ', name: ' Guan Yu ' }, { unitId: '', name: 'Broken' }], @@ -555,7 +576,8 @@ try { equipment: ['Iron Sword'], reputation: 'Fame', recruits: [{ unitId: 'test-recruit', name: 'Test Recruit' }, { broken: true }], - unlocks: { corrupted: true } + unlocks: { corrupted: true }, + note: 'x'.repeat(181) }, objectives: [ { @@ -633,7 +655,8 @@ try { assert( malformedHistory.battleHistory['first-battle-zhuo-commandery']?.itemRewards.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.equipment[0] === 'Iron Sword' && - malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.length === 1, + malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.length === 1 && + malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.note === undefined, `Expected nested battle history rewards to normalize: ${JSON.stringify(malformedHistory)}` ); assert( diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index b98d0ce..8cb01c2 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -266,6 +266,10 @@ const campaignInventoryCaps: Record = { ['\uD0C1\uC8FC']: 80 }; +const maxCampaignStringListEntries = 128; +const maxCampaignStringListLength = 96; +const maxCampaignRewardNoteLength = 180; + export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [ { id: 'balanced', @@ -881,9 +885,9 @@ function uniqueStrings(value: unknown): string[] { arrayOrEmpty(value) .filter((entry): entry is string => typeof entry === 'string') .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0) + .filter((entry) => entry.length > 0 && entry.length <= maxCampaignStringListLength) ) - ]; + ].slice(0, maxCampaignStringListEntries); } function normalizedRosterUnitIds(roster: UnitData[]) { @@ -1532,6 +1536,7 @@ function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot): Campaign } const rewardRecord = recordOrEmpty(rewards); + const note = typeof rewardRecord.note === 'string' ? rewardRecord.note.trim() : ''; return { supplies: uniqueStrings(rewardRecord.supplies), equipment: uniqueStrings(rewardRecord.equipment), @@ -1550,7 +1555,7 @@ function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot): Campaign title: typeof unlock.title === 'string' ? unlock.title.trim() : '' })) .filter((unlock) => unlock.battleId && unlock.title), - note: typeof rewardRecord.note === 'string' && rewardRecord.note.trim().length > 0 ? rewardRecord.note.trim() : undefined + note: note.length > 0 && note.length <= maxCampaignRewardNoteLength ? note : undefined }; }