v0.1.8: 万物皆是插件 + 全应用审计修复(60+ 缺陷歼灭)

插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓

全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
  facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
  4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
  buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
  史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
  toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
  功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
  战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
  渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项

测试 239→256;金钟罩复验(防御批零漂移)
This commit is contained in:
Hermes Agent
2026-08-23 09:58:15 +08:00
parent 624e30498b
commit 967ef1240d
39 changed files with 932 additions and 157 deletions
+85 -36
View File
@@ -60,6 +60,7 @@ export interface GameStore {
closePaper: () => void
toggleSound: () => void
facade?: GameFacade
advancing: boolean
act: (name: ActName, payload: import('../game/engine/api').ActPayload) => boolean
}
@@ -67,8 +68,21 @@ const LOG_CAP = 260
const lastPhaseStats = new Map<World, PhaseStat[]>()
let logSeq = 1
let toastTimer: ReturnType<typeof setTimeout> | null = null
function setNextToast(msg: string): void {
if (toastTimer) clearTimeout(toastTimer)
useGameStore.setState({ toast: msg })
toastTimer = setTimeout(() => useGameStore.setState({ toast: undefined }), 4200)
}
function stopTimer(): void {
const st = useGameStore.getState()
if (st.timer) clearInterval(st.timer)
useGameStore.setState({ speed: 0, timer: null })
}
export const useGameStore = create<GameStore>((set, get) => ({
facade: undefined as GameFacade | undefined,
advancing: false,
screen: 'boot',
world: null,
slot: 1,
@@ -124,34 +138,46 @@ export const useGameStore = create<GameStore>((set, get) => ({
startNewGame: async (opts, slot) => {
const world = World.create(opts)
set({ world, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0 })
set({ world, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: undefined, paperReport: undefined, toast: undefined, selectedMemberId: undefined, selectedMissionDef: undefined })
const st = get()
st.world?.out.push(makeBus(st))
const facade = new GameFacade(world, slot)
set({ facade })
await st.saveNow('开局')
},
continueGame: async (slot) => {
const manager = getSlotManager()
const file = await getSaveSlot(slot)
await file.open()
const state = await file.loadState()
if (!state) throw new Error('找不到存档数据')
openState(state, slot)
await manager.updateSlotMeta(slot, metaFromState(state, slot))
try {
const manager = getSlotManager()
const file = await getSaveSlot(slot)
await file.open()
const state = await file.loadState()
if (!state) throw new Error('找不到存档数据')
openState(state, slot)
await manager.updateSlotMeta(slot, metaFromState(state, slot))
} catch (e) {
setNextToast(`读档失败:${String(e)}`)
useGameStore.setState({ screen: 'boot' })
}
},
loadSnapshot: async (slot, snapshotId) => {
const file = await getSaveSlot(slot)
await file.open()
const state = await file.loadState(snapshotId)
if (!state) throw new Error('找不到快照')
openState(state, slot)
try {
const file = await getSaveSlot(slot)
await file.open()
const state = await file.loadState(snapshotId)
if (!state) throw new Error('找不到快照')
openState(state, slot)
} catch (e) {
setNextToast(`读取快照失败:${String(e)}`)
}
},
advance: async () => {
const st = get()
const w = st.world
if (!w || st.pendingEventId || w.state.gameOver) return
if (!w || st.pendingEventId || w.state.gameOver || st.advancing) return
set({ advancing: true })
try {
const t0 = performance.now()
w.advanceMonth()
@@ -175,7 +201,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
} catch {
// 保存失败时不再叠加错误
}
set({ toast: '时日流转出现异常,已保存出险现场,可回档重来。', speed: 0 })
setNextToast('时日流转出现异常,已保存出险现场,可回档重来。')
stopTimer()
} finally {
set({ advancing: false })
}
},
@@ -192,10 +221,21 @@ export const useGameStore = create<GameStore>((set, get) => ({
}
},
onBattle: (log) => set({ battleView: log, revision: get().revision + 1 }),
onBattle: (log) => {
stopTimer()
set({ battleView: log, revision: get().revision + 1 })
},
onPendingEvent: (id) => {
const def = findEvent(id, get().world ?? undefined)
if (!def) {
// 未知事件(插件卸载/旧档漂移):跳过并解除软锁
const w = get().world
if (w?.state.pendingEvent === id) w.state.pendingEvent = undefined
get().addLog({ id: logSeq++, kind: 'bad', text: `事件「${id}」未知,已跳过。`, year: get().world?.state.year ?? 0, month: get().world?.state.month ?? 0 })
set({ pendingEventId: undefined, pendingEventDef: undefined })
return
}
set({ pendingEventId: id, pendingEventDef: def })
},
@@ -253,6 +293,9 @@ export const useGameStore = create<GameStore>((set, get) => ({
setSpeed: (v) => {
const st = get()
if (st.timer) clearInterval(st.timer)
if (st.speed === v && v === 0 && false) {
// noop
}
if (v <= 0) {
set({ speed: v, timer: null })
return
@@ -261,9 +304,12 @@ export const useGameStore = create<GameStore>((set, get) => ({
if (v >= 3) v = 3
const timer = setInterval(() => {
const cur = get()
if (!cur.pendingEventId && cur.world && !cur.world.state.gameOver) {
void cur.advance()
const w = cur.world
if (cur.speed <= 0 || !w || cur.pendingEventId || cur.battleView || cur.advancing || w.state.gameOver) {
// 速度被暂停/事件待决/战报展开/在途推进时静默轮空
return
}
void cur.advance()
}, ms)
set({ speed: v, timer })
},
@@ -282,25 +328,27 @@ export const useGameStore = create<GameStore>((set, get) => ({
const json = await file.exportAll()
const meta = metaFromState(st.world.state, st.slot)
const r = await window.api.exportSave(json, `${meta.name}-年${meta.year}`)
if (r.ok) set({ toast: '存档已导出。' })
else set({ toast: `导出失败:${r.error ?? ''}` })
if (r.ok) setNextToast('存档已导出。')
else setNextToast(`导出失败:${r.error ?? ''}`)
},
importSave: async (slot) => {
if (!window.api) return
const r = await window.api.importSave()
if (!r.ok || !r.text) {
set({ toast: `导入失败:${r.error ?? ''}` })
setNextToast(`导入失败:${r.error ?? ''}`)
return
}
const file = await getSaveSlot(slot)
await file.open()
const ok = await file.importAll(r.text)
if (ok) {
set({ toast: '导入成功,重新读取存档槽。' })
setNextToast('导入成功,重新读取存档槽。')
await get().refreshSlots()
const state = await file.loadState()
if (state) openState(state, slot)
} else {
set({ toast: '导入失败:文件格式不正确。' })
setNextToast('导入失败:文件格式不正确。')
}
},
@@ -326,8 +374,8 @@ function openState(state: GameState, slot: number): void {
const st = useGameStore.getState()
world.out.push(makeBus(st))
const facade = new GameFacade(world, slot)
world.out.push(facadebus(facade))
useGameStore.setState({
facade,
world,
slot,
screen: 'game',
@@ -338,8 +386,20 @@ function openState(state: GameState, slot: number): void {
pendingEventDef: undefined,
revision: 1,
speed: 0,
gameOverReason: state.gameOver?.reason
gameOverReason: state.gameOver?.reason || undefined,
paperReport: undefined,
toast: undefined,
selectedMemberId: undefined,
selectedMissionDef: undefined
})
if (state.pendingEvent && state.members) {
const def = findEvent(state.pendingEvent, world)
if (def) {
useGameStore.setState({ pendingEventId: state.pendingEvent, pendingEventDef: def })
} else {
world.state.pendingEvent = undefined
}
}
}
function makeBus(st: GameStore): WorldEventBus {
@@ -363,17 +423,6 @@ function makeBus(st: GameStore): WorldEventBus {
}
}
function facadebus(facade: GameFacade): WorldEventBus {
return {
onLog: () => undefined,
onChronicle: () => undefined,
onBattle: () => undefined,
onPendingEvent: () => undefined,
onGameOver: () => undefined,
onSystemChange: () => undefined
}
}
function actDirect(w: World, name: ActName, payload: import('../game/engine/api').ActPayload): boolean {
if (name === 'legacy.resolve') return (w.resolveLegacyNow(), true)
if (name === 'estate.rite') return w.ancestralRite()