feat(lockstep): 实现完整的锁步状态哈希算法 (refs #7)
扩展了锁步哈希验证以包含背包内容、地面物品和任务进度。通过添加 computeLockstepHash,以稳定和低成本的序列化方法对组件进行 Fnv-1a 的增量混合,并测试覆盖了所有变化。因为集成到目前游戏引擎中要求触及位于 src/scene 下禁止访问的实现,按照架构约束边界已暂停接入并供后续子系统调用。
This commit is contained in:
parent
6e6d8ed022
commit
fc201d7290
|
|
@ -228,3 +228,77 @@ export class LockstepSession {
|
||||||
return hash >>> 0
|
return hash >>> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function computeLockstepHash(world: any, inventory: any, ground: readonly any[], quests: readonly any[]): number {
|
||||||
|
let hash = 2166136261
|
||||||
|
|
||||||
|
function mix(value: number) {
|
||||||
|
hash ^= (value & 0xFFFFFFFF)
|
||||||
|
hash = Math.imul(hash, 16777619) >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixString(text: string) {
|
||||||
|
for (let index = 0; index < text.length; index += 1) {
|
||||||
|
hash ^= text.charCodeAt(index)
|
||||||
|
hash = Math.imul(hash, 16777619) >>> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const round = (val: number) => Math.round(val * 1000)
|
||||||
|
|
||||||
|
// 1. Hash World
|
||||||
|
mix(world.tick)
|
||||||
|
mix(world.kills)
|
||||||
|
|
||||||
|
for (const player of world.players) {
|
||||||
|
mix(round(player.x))
|
||||||
|
mix(round(player.y))
|
||||||
|
mix(player.hp)
|
||||||
|
mix(player.xp)
|
||||||
|
mix(player.level)
|
||||||
|
mix(player.alive ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const monster of world.monsters) {
|
||||||
|
mix(round(monster.x))
|
||||||
|
mix(round(monster.y))
|
||||||
|
mix(monster.hp)
|
||||||
|
mixString(monster.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Hash Inventory (sort by some stable key like x,y to ensure determinism if order varies)
|
||||||
|
// Actually, placed items shouldn't vary in order, but to be 100% deterministic, we can sort by index or coordinate.
|
||||||
|
if (inventory && inventory.placed) {
|
||||||
|
const placed = [...inventory.placed].sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x)
|
||||||
|
for (const entry of placed) {
|
||||||
|
mix(entry.x)
|
||||||
|
mix(entry.y)
|
||||||
|
mixString(entry.item.name)
|
||||||
|
mix(entry.item.stack ?? 1)
|
||||||
|
mix(entry.item.value ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Hash Ground Loot (order by id or coords)
|
||||||
|
if (ground) {
|
||||||
|
const groundItems = [...ground].sort((a, b) => round(a.y) !== round(b.y) ? round(a.y) - round(b.y) : round(a.x) - round(b.x))
|
||||||
|
for (const entry of groundItems) {
|
||||||
|
mix(round(entry.x))
|
||||||
|
mix(round(entry.y))
|
||||||
|
mixString(entry.item.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Hash Quests (sort by id)
|
||||||
|
if (quests) {
|
||||||
|
const sortedQuests = [...quests].sort((a, b) => a.def.id.localeCompare(b.def.id))
|
||||||
|
for (const entry of sortedQuests) {
|
||||||
|
mixString(entry.def.id)
|
||||||
|
mixString(entry.status)
|
||||||
|
mix(entry.kills)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash >>> 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { computeLockstepHash } from '../src/net/lockstep.ts'
|
||||||
|
|
||||||
|
describe('computeLockstepHash', () => {
|
||||||
|
it('detects changes in inventory', () => {
|
||||||
|
const world = { tick: 1, kills: 0, players: [], monsters: [] }
|
||||||
|
const ground: any[] = []
|
||||||
|
const quests: any[] = []
|
||||||
|
|
||||||
|
const inv1 = { width: 10, height: 4, placed: [
|
||||||
|
{ x: 0, y: 0, item: { name: 'A', stack: 1, value: 5 } }
|
||||||
|
]}
|
||||||
|
|
||||||
|
const inv2 = { width: 10, height: 4, placed: [
|
||||||
|
{ x: 0, y: 0, item: { name: 'B', stack: 1, value: 5 } }
|
||||||
|
]}
|
||||||
|
|
||||||
|
const hash1 = computeLockstepHash(world, inv1, ground, quests)
|
||||||
|
const hash2 = computeLockstepHash(world, inv2, ground, quests)
|
||||||
|
|
||||||
|
expect(hash1).not.toBe(hash2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects changes in quest progress', () => {
|
||||||
|
const world = { tick: 1, kills: 0, players: [], monsters: [] }
|
||||||
|
const inv = { width: 10, height: 4, placed: [] }
|
||||||
|
const ground: any[] = []
|
||||||
|
|
||||||
|
const quests1 = [ { def: { id: 'q1' }, status: 'active', kills: 0 } ]
|
||||||
|
const quests2 = [ { def: { id: 'q1' }, status: 'completed', kills: 0 } ]
|
||||||
|
|
||||||
|
const hash1 = computeLockstepHash(world, inv, ground, quests1)
|
||||||
|
const hash2 = computeLockstepHash(world, inv, ground, quests2)
|
||||||
|
|
||||||
|
expect(hash1).not.toBe(hash2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects changes in ground items', () => {
|
||||||
|
const world = { tick: 1, kills: 0, players: [], monsters: [] }
|
||||||
|
const inv = { width: 10, height: 4, placed: [] }
|
||||||
|
const quests: any[] = []
|
||||||
|
|
||||||
|
const ground1 = [ { x: 5, y: 5, item: { name: 'Gold' } } ]
|
||||||
|
const ground2 = [ { x: 5, y: 5, item: { name: 'Potion' } } ]
|
||||||
|
|
||||||
|
const hash1 = computeLockstepHash(world, inv, ground1, quests)
|
||||||
|
const hash2 = computeLockstepHash(world, inv, ground2, quests)
|
||||||
|
|
||||||
|
expect(hash1).not.toBe(hash2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('produces identical hashes for identical states', () => {
|
||||||
|
const world = { tick: 1, kills: 0, players: [], monsters: [] }
|
||||||
|
const inv = { width: 10, height: 4, placed: [
|
||||||
|
{ x: 1, y: 1, item: { name: 'Sword', stack: 1, value: 100 } }
|
||||||
|
]}
|
||||||
|
const ground = [ { x: 5, y: 5, item: { name: 'Gold' } } ]
|
||||||
|
const quests = [ { def: { id: 'q1' }, status: 'active', kills: 0 } ]
|
||||||
|
|
||||||
|
const hash1 = computeLockstepHash(world, inv, ground, quests)
|
||||||
|
|
||||||
|
// Deep clone to ensure stability across new references
|
||||||
|
const world2 = JSON.parse(JSON.stringify(world))
|
||||||
|
const inv2 = JSON.parse(JSON.stringify(inv))
|
||||||
|
const ground2 = JSON.parse(JSON.stringify(ground))
|
||||||
|
const quests2 = JSON.parse(JSON.stringify(quests))
|
||||||
|
|
||||||
|
const hash2 = computeLockstepHash(world2, inv2, ground2, quests2)
|
||||||
|
|
||||||
|
expect(hash1).toBe(hash2)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue