From 3a5ee11a6d50a9fbd9d6c9d583b494531ef95544 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 13:40:20 +0900 Subject: [PATCH] Clamp battle save effect state --- scripts/verify-battle-save-normalization.mjs | 8 +++++ src/game/state/battleSaveState.ts | 32 +++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/verify-battle-save-normalization.mjs b/scripts/verify-battle-save-normalization.mjs index d9470c8..6c8f41d 100644 --- a/scripts/verify-battle-save-normalization.mjs +++ b/scripts/verify-battle-save-normalization.mjs @@ -89,9 +89,17 @@ try { ['unknown item stock unit id', { itemStocks: { 'ghost-unit': { bean: 1 } } }], ['unknown item stock item id', { itemStocks: { 'liu-bei': { phantomItem: 1 } } }], ['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }], + ['too many buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 10 }] }], + ['too long buff label', { battleBuffs: [{ ...validState.battleBuffs[0], label: 'x'.repeat(33) }] }], + ['invalid buff bonus', { battleBuffs: [{ ...validState.battleBuffs[0], hitBonus: 1.5 }] }], + ['too high buff bonus', { battleBuffs: [{ ...validState.battleBuffs[0], criticalBonus: 100 }] }], ['unknown buff unit id', { battleBuffs: [{ ...validState.battleBuffs[0], unitId: 'ghost-unit' }] }], ['duplicate buff unit id', { battleBuffs: [validState.battleBuffs[0], { ...validState.battleBuffs[0] }] }], ['invalid status kind', { battleStatuses: [{ ...validState.battleStatuses[0], kind: 'poison' }] }], + ['too many status turns', { battleStatuses: [{ ...validState.battleStatuses[0], turns: 10 }] }], + ['too long status label', { battleStatuses: [{ ...validState.battleStatuses[0], label: 'x'.repeat(33) }] }], + ['invalid status power', { battleStatuses: [{ ...validState.battleStatuses[0], power: 0 }] }], + ['too high status power', { battleStatuses: [{ ...validState.battleStatuses[0], power: 100 }] }], ['unknown status unit id', { battleStatuses: [{ ...validState.battleStatuses[0], unitId: 'ghost-unit' }] }], ['duplicate status unit kind', { battleStatuses: [validState.battleStatuses[0], { ...validState.battleStatuses[0] }] }], ['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }], diff --git a/src/game/state/battleSaveState.ts b/src/game/state/battleSaveState.ts index 96c8bcc..733ee8e 100644 --- a/src/game/state/battleSaveState.ts +++ b/src/game/state/battleSaveState.ts @@ -87,6 +87,10 @@ const maxBattleBondEntries = 128; const maxTriggeredBattleEventLength = 96; const defaultBattleSaveArrayLimit = 128; const defaultBattleItemStockLimit = 9; +const maxBattleEffectLabelLength = 32; +const maxBattleEffectTurns = 9; +const maxBattleBuffBonus = 99; +const maxBattleStatusPower = 99; const maxStatusKindsPerUnit = 2; const maxBattleItemStockById: Record = { bean: 3, @@ -165,6 +169,10 @@ function isPositiveInteger(value: unknown) { return Number.isInteger(value) && Number(value) >= 1; } +function isPositiveIntegerAtMost(value: unknown, max: number) { + return isPositiveInteger(value) && Number(value) <= max; +} + function isNonNegativeFiniteNumber(value: unknown) { return typeof value === 'number' && Number.isFinite(value) && value >= 0; } @@ -177,6 +185,10 @@ function isFiniteNumber(value: unknown) { return typeof value === 'number' && Number.isFinite(value); } +function isIntegerInRange(value: unknown, min: number, max: number) { + return Number.isInteger(value) && Number(value) >= min && Number(value) <= max; +} + function isValidIsoLikeDate(value: unknown) { return typeof value === 'string' && value.length > 0 && !Number.isNaN(Date.parse(value)); } @@ -382,6 +394,10 @@ function isValidItemStockCount(itemId: string, count: unknown) { return Number.isInteger(count) && Number(count) >= 0 && Number(count) <= (maxBattleItemStockById[itemId] ?? defaultBattleItemStockLimit); } +function isValidEffectLabel(value: unknown) { + return typeof value === 'string' && value.length > 0 && value.length <= maxBattleEffectLabelLength; +} + function isOptionalBuffArray(value: unknown, options: BattleSaveValidationOptions) { return ( value === undefined || @@ -393,11 +409,11 @@ function isOptionalBuffArray(value: unknown, options: BattleSaveValidationOption isRecord(buff) && typeof buff.unitId === 'string' && isKnownUnitId(buff.unitId, options) && - typeof buff.label === 'string' && - isPositiveInteger(buff.turns) && - isFiniteNumber(buff.attackBonus) && - isFiniteNumber(buff.hitBonus) && - isFiniteNumber(buff.criticalBonus) + isValidEffectLabel(buff.label) && + isPositiveIntegerAtMost(buff.turns, maxBattleEffectTurns) && + isIntegerInRange(buff.attackBonus, 0, maxBattleBuffBonus) && + isIntegerInRange(buff.hitBonus, 0, maxBattleBuffBonus) && + isIntegerInRange(buff.criticalBonus, 0, maxBattleBuffBonus) )) ); } @@ -414,9 +430,9 @@ function isOptionalStatusArray(value: unknown, options: BattleSaveValidationOpti typeof status.unitId === 'string' && isKnownUnitId(status.unitId, options) && isBattleStatusKind(status.kind) && - typeof status.label === 'string' && - isPositiveInteger(status.turns) && - isFiniteNumber(status.power) + isValidEffectLabel(status.label) && + isPositiveIntegerAtMost(status.turns, maxBattleEffectTurns) && + isPositiveIntegerAtMost(status.power, maxBattleStatusPower) )) ); }