fix(game): 移除不安全的类型转换并增加存档校验 (refs #7)
在 character.ts 中使用 flatMap 结合类型缩窄(null 检查),移除了对 DccFile 的强制类型转换,保证内部逻辑的编译期安全。在 save.ts 中增加了对背包数据的严格运行时有效性验证,避免由于恶意或破损存档导致的隐式游戏损坏,并用专用的反序列化测试覆盖新校验过程。
This commit is contained in:
parent
fc201d7290
commit
4290e5feb2
|
|
@ -189,10 +189,12 @@ export async function loadCharacterSheet(
|
||||||
// the priority table is the authority on what covers what.
|
// the priority table is the authority on what covers what.
|
||||||
const order = cofLayerOrder(cof, direction, index)
|
const order = cofLayerOrder(cof, direction, index)
|
||||||
const ordered = order
|
const ordered = order
|
||||||
.map(layerIndex => { const sprite = sprites[layerIndex]; return { layerIndex, sprite } })
|
.flatMap(layerIndex => {
|
||||||
.filter(entry => entry.sprite !== null && entry.sprite !== undefined)
|
const sprite = sprites[layerIndex]
|
||||||
|
return sprite != null ? [{ layerIndex, sprite }] : []
|
||||||
|
})
|
||||||
for (const entry of ordered) {
|
for (const entry of ordered) {
|
||||||
const sprite = entry.sprite as DccFile
|
const sprite = entry.sprite
|
||||||
const dccDir = dir64ToDcc(dir64, sprite.directions.length)
|
const dccDir = dir64ToDcc(dir64, sprite.directions.length)
|
||||||
const layerDirection = sprite.directions[dccDir]
|
const layerDirection = sprite.directions[dccDir]
|
||||||
const frame = layerDirection?.frames[index]
|
const frame = layerDirection?.frames[index]
|
||||||
|
|
|
||||||
|
|
@ -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 === 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 (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 (!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')
|
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
|
// 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
|
// 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.
|
// live world would otherwise leave the previous player list in place.
|
||||||
world: rebindPlayer({ ...snapshot.world, monsters: [...snapshot.world.monsters], players: [], events: [] }),
|
world: rebindPlayer({ ...snapshot.world, monsters: [...snapshot.world.monsters], players: [], events: [] }),
|
||||||
rngState: snapshot.rngState,
|
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),
|
quests: build.quests(snapshot.quests),
|
||||||
ground: snapshot.ground.map(entry => ({ x: entry.x, y: entry.y, item: entry.item })),
|
ground: snapshot.ground.map(entry => ({ x: entry.x, y: entry.y, item: entry.item })),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue