Files
ChronicleOfTheImmortalClan/tests/fingerprint.helper.ts
T
thzxx da4fb9f635 v0.1.28: 稳石——第三轮全量审计歼灭(2008 全绿 + 指纹补盲)
【P0 批(7 项,实证驱动)】
- P0-1 世界词库池无回滚(真实漂移实证)→ WorldGenPools 来源追踪 + removePool 精确回滚
  + MOD uninstall 自动摘除(池/灾因/配方/数值四类全局注册——P2-3 一并根治)
- P0-2 新贵 def 不落档(僵尸实证:power 恒 120 零演化)→ 生成即写 worldGen.npcs
  + normalize 占位 def 双保险 + 新贵 id 掺 seed 指纹(P1-10 防跨档碰撞)
- P0-3 校验死代码化 → installModText 装前 modPreflight(冲突拒+警告 toast)
- P0-4 MOD 读档静默消失 → state.pluginsMissing 收集提示(与 MOD_GUIDE 承诺对齐)
- P0-5 救济按钮乌龙(哈希序首个+直改 state)→ World.reliefCalamity(景气最低+双检)
- P0-6 天下快讯永久冻结(memo 依赖恒 120 length)→ 去 memo 每次 reverse
- P0-7 MOD 导出单行道 → modParse 识别 {app:'cotymod',bundle} 逐包递归(roundtrip 闭环)

【P1 批】
- P1-8 指纹补盲(worldGen/sagaAnnals/distress/declineYears 入 hash——0.1.27 成果真实锁定)+ 双基线重算
- P1-9 重放同步 worldGen.npcs(自捕获);P1-11 worldNum 字段接线(install/uninstall 复位)
- P1-13 落盘兜底(beforeunload/visibilitychange 突发 save,堵 12-tick 节流窗口)
- P1-14 onGameOver 停 timer + IPC mods 路径白名单(扩展名正则防穿越)
- P1-15 DataPack 摘除 npcs(覆写死路删除,与五口宣言对齐);P1-4 techniques 双源统一(techniquePrice 读包)

【P2 扫尾】tauntCD 清理/sagaAnnals 上限 40/worldGenMods 落空写入/validateMod 深度校验随 P0-3 提交

【测试】69 套件 2008 全绿(audit-0.1.28 八例:池回滚/僵尸/preflight/bundle/freeze/指纹敏感/worldNum/救济);
金钟罩 0.1.28 基线重算(指纹补盲+受控时序变更);build 通过
2026-08-23 18:07:31 +08:00

98 lines
4.6 KiB
TypeScript

import { World } from '../src/renderer/game/engine/runtime/World'
import { GameState } from '../src/renderer/game/types/domain'
export function stateFingerprint(s: GameState): string {
const parts = [
s.year, s.month, s.seq, s.totalTicks,
s.family.stones,
s.family.reputation,
s.family.generation,
JSON.stringify(sortedInv(s.family.inventory)),
JSON.stringify(sortedBld(s.family.buildings)),
s.family.headId,
s.family.techniques.join(',')
]
const members = Object.values(s.members)
.map((c) => `${c.id}:${c.alive}:${c.realm.major}/${c.realm.minor}:${Math.floor(c.realmProgress * 10) / 10}:${Math.floor(c.health * 10) / 10}:${c.state}:${c.bornYear}:${c.techniqueRank ?? 0}:${Math.floor((c.techniqueProgress ?? 0) * 10) / 10}:${c.post ?? ''}:${c.aspiration ?? ''}:${c.spouseId ?? ''}:${c.children.join('-')}`)
.sort()
parts.push(members.join('|'))
parts.push(JSON.stringify(sortedNpc(s.npcFamilies)))
parts.push(s.missions.length + ':' + s.missions.map((m) => `${m.defId}:${m.stage}:${m.stageMonth}:${m.done}:${m.memberIds.join('+')}`).join(';'))
parts.push(s.chronicle.length + ':' + s.chronicle.slice(-5).map((e) => `${e.year}${e.month}${e.category}`).join(','))
parts.push(s.battles.length)
parts.push(Object.keys(s.completedEvents).length + ':' + s.completedEvents.slice(-3).join(','))
parts.push(JSON.stringify(sortedFlags(s.family.flag)))
parts.push(s.yearlyReports.length)
const ws = s.worldSim
if (ws) {
const dyns = Object.entries(ws.npcDyn ?? {})
.map(([k, d]) => `${k}:${(d as { stance?: string; prosperity?: number }).stance ?? '-'}:${Math.floor((d as { prosperity?: number }).prosperity ?? 0) * 7}:${JSON.stringify((d as { relationsWithOthers?: Record<string, number> }).relationsWithOthers ?? {})}`)
.join(';')
parts.push(`ws:${JSON.stringify(ws.marketPool ?? {})}:${JSON.stringify(ws.secretQi ?? {})}:${ws.tide ?? 0}:${ws.npcSuccessions ?? 0}:${(ws.newsFeed ?? []).length}:${ws.era ?? '-'}:${ws.eraStartYear ?? 0}:${ws.calamity ?? '-'}:${ws.calamityLeft ?? 0}:${dyns}`)
} else {
parts.push('ws:none')
}
parts.push(`pe:${s.pendingEvent ?? '-'}:${s.eventQueue ? s.eventQueue.length : 0}`)
// P1-8 指纹补盲(0.1.28):世界轴线全量——sagaAnnals/distress/declineYears/worldGen 摘 req
const wg = s.worldGen as { npcCount?: number; eras?: string; npcs?: unknown[] } | undefined
parts.push(`wg:${wg?.npcCount ?? 0}:${wg?.eras ?? '-'}:${wg?.npcs?.length ?? 0}`)
parts.push(`saga:${(ws?.sagaAnnals ?? []).length}:${((ws?.sagaAnnals ?? []) as { year: number }[]).slice(-1).map((a) => a.year).join(',')}`)
parts.push(`distress:${ws?.distress?.id ?? '-'}`)
const declSum = Object.values(ws?.npcDyn ?? {})
.map((d) => (d as { declineYears?: number })?.declineYears ?? 0)
.reduce((a, b) => a + b, 0)
parts.push(`decl:${declSum}`)
return hash32(parts.join('\u0001'))
}
function sortedInv(inv: Record<string, number>): Array<[string, number]> {
return Object.entries(inv).sort((a, b) => (a[0] < b[0] ? -1 : 1))
}
function sortedBld(b: Record<string, number>): Array<[string, number]> {
return Object.entries(b).sort((a, b) => (a[0] < b[0] ? -1 : 1))
}
function sortedFlags(f: Record<string, number | boolean | string>): Array<[string, string]> {
return Object.entries(f)
.map(([k, v]) => [k, String(v)] as [string, string])
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
}
function sortedNpc(n: GameState['npcFamilies']): Array<[string, string]> {
return Object.entries(n)
.map(([k, v]) => [k, `${v.relation}:${v.power}:${v.allied}`] as [string, string])
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
}
function hash32(str: string): string {
let h = 2166136261
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i)
h = Math.imul(h, 16777619)
h = (h << 13) | (h >>> 19)
}
return (h >>> 0).toString(16).padStart(8, '0')
}
export function longRun(seed: string, months = 560): World {
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < months; i++) {
if (w.state.gameOver) break
w.advanceMonth()
}
return w
}
/** 自动 resolve 长跑("现实"世界:每 tick 处理待决事件——与直调冻结世界对照) */
export function longRunResolved(seed: string, months = 560): World {
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < months; i++) {
if (w.state.gameOver) break
w.advanceMonth()
let guard = 0
while (w.state.pendingEvent && guard < 4) {
w.applyEventChoice(w.state.pendingEvent, 0)
guard++
}
}
return w
}