From 4290e5feb2d0f912ec6a5c43428a175464809ad9 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Mon, 14 Sep 2026 12:41:32 +0000 Subject: [PATCH] =?UTF-8?q?fix(game):=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E7=9A=84=E7=B1=BB=E5=9E=8B=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=8A=A0=E5=AD=98=E6=A1=A3=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=20(refs=20#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 character.ts 中使用 flatMap 结合类型缩窄(null 检查),移除了对 DccFile 的强制类型转换,保证内部逻辑的编译期安全。在 save.ts 中增加了对背包数据的严格运行时有效性验证,避免由于恶意或破损存档导致的隐式游戏损坏,并用专用的反序列化测试覆盖新校验过程。 --- src/game/character.ts | 8 +++++--- src/game/save.ts | 13 +++++++++++-- tests/save-schema.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 tests/save-schema.test.ts diff --git a/src/game/character.ts b/src/game/character.ts index fa24a9b..97c6295 100644 --- a/src/game/character.ts +++ b/src/game/character.ts @@ -189,10 +189,12 @@ export async function loadCharacterSheet( // the priority table is the authority on what covers what. const order = cofLayerOrder(cof, direction, index) const ordered = order - .map(layerIndex => { const sprite = sprites[layerIndex]; return { layerIndex, sprite } }) - .filter(entry => entry.sprite !== null && entry.sprite !== undefined) + .flatMap(layerIndex => { + const sprite = sprites[layerIndex] + return sprite != null ? [{ layerIndex, sprite }] : [] + }) for (const entry of ordered) { - const sprite = entry.sprite as DccFile + const sprite = entry.sprite const dccDir = dir64ToDcc(dir64, sprite.directions.length) const layerDirection = sprite.directions[dccDir] const frame = layerDirection?.frames[index] diff --git a/src/game/save.ts b/src/game/save.ts index 3761271..d047445 100644 --- a/src/game/save.ts +++ b/src/game/save.ts @@ -133,7 +133,16 @@ export function parseSnapshot(text: string): GameSnapshot { if (candidate.world === undefined || typeof candidate.world.tick !== 'number') throw new Error('save has no world') if (candidate.world.player === undefined) throw new Error('save has no player') if (!Array.isArray(candidate.world.monsters)) throw new Error('save has no monster list') - if (candidate.inventory === undefined || !Array.isArray(candidate.inventory.placed)) throw new Error('save has no inventory') + if (candidate.inventory === undefined || typeof candidate.inventory.width !== 'number' || typeof candidate.inventory.height !== 'number') { + throw new Error('save has no inventory bounds') + } + if (!Array.isArray(candidate.inventory.placed)) throw new Error('save has no inventory') + for (const [index, entry] of candidate.inventory.placed.entries()) { + if (typeof entry !== 'object' || entry === null) throw new Error(`inventory placed item [${index}] is malformed`) + if (typeof (entry as any).x !== 'number') throw new Error(`inventory placed item [${index}] has invalid x`) + if (typeof (entry as any).y !== 'number') throw new Error(`inventory placed item [${index}] has invalid y`) + if (typeof (entry as any).item !== 'object' || (entry as any).item === null) throw new Error(`inventory placed item [${index}] has no item`) + } if (!Array.isArray(candidate.quests)) throw new Error('save has no quest log') // Older saves predate ground items; an absent list is treated as empty rather // than as corruption, so the version does not have to be bumped for a field @@ -171,7 +180,7 @@ export function restoreSnapshot( // live world would otherwise leave the previous player list in place. world: rebindPlayer({ ...snapshot.world, monsters: [...snapshot.world.monsters], players: [], events: [] }), rngState: snapshot.rngState, - inventory: build.inventory(snapshot.inventory.width, snapshot.inventory.height, snapshot.inventory.placed as PlacedItem[]), + inventory: build.inventory(snapshot.inventory.width, snapshot.inventory.height, snapshot.inventory.placed), quests: build.quests(snapshot.quests), ground: snapshot.ground.map(entry => ({ x: entry.x, y: entry.y, item: entry.item })), } diff --git a/tests/save-schema.test.ts b/tests/save-schema.test.ts new file mode 100644 index 0000000..23ed623 --- /dev/null +++ b/tests/save-schema.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest' +import { parseSnapshot } from '../src/game/save.ts' + +describe('parseSnapshot', () => { + it('rejects malformed placed items in inventory', () => { + const badSnapshot = { + version: 1, + rngState: 0, + world: { + tick: 0, + player: {}, + monsters: [], + }, + inventory: { + width: 10, + height: 4, + placed: [ + { x: 1, y: 1 }, // Missing item + ] + }, + quests: [], + ground: [] + } + + expect(() => parseSnapshot(JSON.stringify(badSnapshot))).toThrow('inventory placed item [0] has no item') + }) +})