From 62a17274ff5a8b9bac24a0715645b213daa69956 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 12:58:16 +0900 Subject: [PATCH] Harden battle save growth validation --- scripts/verify-battle-save-normalization.mjs | 5 ++++ src/game/state/battleSaveState.ts | 26 ++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/scripts/verify-battle-save-normalization.mjs b/scripts/verify-battle-save-normalization.mjs index 9d873c0..2bfe0a3 100644 --- a/scripts/verify-battle-save-normalization.mjs +++ b/scripts/verify-battle-save-normalization.mjs @@ -54,6 +54,8 @@ try { ['unknown attack intent unit id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'ghost-unit' }] }], ['duplicate attack intent attacker id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'rebel-1' }, { attackerId: 'liu-bei', targetId: 'rebel-1' }] }], ['invalid units array', { units: {} }], + ['invalid unit exp threshold', { units: patchUnit(0, { exp: 100 }) }], + ['invalid max-level unit exp threshold', { units: patchUnit(0, { level: 99, exp: 101 }) }], ['invalid unit hp', { units: patchUnit(0, { hp: 99, maxHp: 30 }) }], ['invalid unit x', { units: patchUnit(0, { x: 12 }) }], ['invalid unit direction', { units: patchUnit(0, { direction: 'down' }) }], @@ -62,7 +64,10 @@ try { ['unknown equipment item id', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'phantom-sword', level: 1, exp: 0 } } }) }], ['wrong equipment slot item id', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'cloth-armor', level: 1, exp: 0 } } }) }], ['invalid equipment level', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 0, exp: 0 } } }) }], + ['too high equipment level', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 10, exp: 0 } } }) }], ['invalid equipment exp', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 1, exp: -1 } } }) }], + ['invalid equipment exp threshold', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 1, exp: 70 } } }) }], + ['invalid max-level equipment exp threshold', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 9, exp: 231 } } }) }], ['unknown unit id', { units: patchUnit(0, { id: 'ghost-unit' }) }], ['duplicate unit id', { units: [validState.units[0], { ...validState.units[0], x: 2 }, validState.units[2]] }], ['invalid bonds', { bonds: [{ ...validState.bonds[0], unitIds: ['liu-bei'] }] }], diff --git a/src/game/state/battleSaveState.ts b/src/game/state/battleSaveState.ts index b258ec2..ea2f6ff 100644 --- a/src/game/state/battleSaveState.ts +++ b/src/game/state/battleSaveState.ts @@ -1,7 +1,7 @@ import type { BattleBond, UnitData } from '../data/scenario'; import type { CampaignStep } from './campaignState'; import type { UnitDirection } from '../data/unitAssets'; -import { equipmentSlots, type EquipmentSlot } from '../data/battleItems'; +import { equipmentExpToNext, equipmentSlots, type EquipmentSlot } from '../data/battleItems'; export type BattleSaveFaction = 'ally' | 'enemy'; export type BattleSaveRosterTab = 'ally' | 'enemy'; @@ -239,8 +239,7 @@ function isSavedBattleUnitState(value: unknown, options: BattleSaveValidationOpt } if ( - !isPositiveInteger(value.level) || - !isNonNegativeFiniteNumber(value.exp) || + !isValidCharacterProgress(value.level, value.exp) || !isNonNegativeFiniteNumber(value.hp) || !isPositiveInteger(value.maxHp) || Number(value.hp) > Number(value.maxHp) || @@ -282,11 +281,28 @@ function isEquipmentStateForSlot(value: unknown, slot: EquipmentSlot, options: B itemId.length > 0 && (!options.validEquipmentItemIds || options.validEquipmentItemIds.has(itemId)) && (!options.validEquipmentSlotItems?.[slot] || options.validEquipmentSlotItems[slot].has(itemId)) && - isPositiveInteger(level) && - isNonNegativeFiniteNumber(exp) + isValidEquipmentProgress(level, exp) ); } +function isValidCharacterProgress(level: unknown, exp: unknown) { + return ( + isPositiveInteger(level) && + Number.isInteger(exp) && + Number(exp) >= 0 && + (Number(level) >= 99 ? Number(exp) <= 100 : Number(exp) < 100) + ); +} + +function isValidEquipmentProgress(level: unknown, exp: unknown) { + if (!Number.isInteger(level) || Number(level) < 1 || Number(level) > 9 || !Number.isInteger(exp) || Number(exp) < 0) { + return false; + } + + const next = equipmentExpToNext(Number(level)); + return Number(level) >= 9 ? Number(exp) <= next : Number(exp) < next; +} + function isBondArray(value: unknown, options: BattleSaveValidationOptions) { return ( Array.isArray(value) &&