插件协议:
- 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;金钟罩复验(防御批零漂移)
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../core/plugin'
|
|
import { SystemDef, SYSTEM_DEFS } from '../engine/capabilities'
|
|
import type { World } from '../engine/world'
|
|
import { SystemHook } from '../core/clock'
|
|
import { GameClock } from '../core/clock'
|
|
|
|
interface RuntimePlugin {
|
|
plugin: CotycPlugin
|
|
status: { installed: boolean; enabled: boolean }
|
|
unsubscribers: Array<() => void>
|
|
}
|
|
|
|
/**
|
|
* 插件管理器:统一安装/卸载/启停管线。
|
|
* 安装序即确定性序;依赖缺失或冲突 → 拒绝安装。
|
|
*/
|
|
export class PluginManager {
|
|
private runtime = new Map<string, RuntimePlugin>()
|
|
private order: string[] = []
|
|
private changes: PluginChange[] = []
|
|
|
|
constructor(private ctx: PluginContext) {}
|
|
|
|
private onChange(c: PluginChange): void {
|
|
this.changes.push(c)
|
|
}
|
|
|
|
listChanges(): PluginChange[] {
|
|
return this.changes.slice()
|
|
}
|
|
|
|
clearChanges(): void {
|
|
this.changes = []
|
|
}
|
|
|
|
install(plugin: CotycPlugin): { ok: boolean; reason?: string } {
|
|
if (this.runtime.has(plugin.id)) return { ok: false, reason: `插件已存在:${plugin.id}` }
|
|
for (const dep of plugin.dependencies ?? []) {
|
|
if (!this.runtime.has(dep)) return { ok: false, reason: `缺少依赖:${dep}` }
|
|
}
|
|
for (const c of plugin.conflicts ?? []) {
|
|
if (this.runtime.has(c)) return { ok: false, reason: `与 ${c} 冲突` }
|
|
}
|
|
for (const def of SYSTEM_DEFS) {
|
|
if (def.id === plugin.id) return { ok: false, reason: `id 冲突:${plugin.id}` }
|
|
}
|
|
|
|
const rt: RuntimePlugin = { plugin, status: { installed: true, enabled: true }, unsubscribers: [] }
|
|
this.runtime.set(plugin.id, rt)
|
|
this.order.push(plugin.id)
|
|
// 追踪型上下文:插件在时轮上的注册(phase/年首)一律归属该插件,卸载时全摘
|
|
const tracked = new Proxy(this.ctx, {
|
|
get(target, key) {
|
|
const prop = key as keyof PluginContext
|
|
if (prop === 'register' || prop === 'onYearStart') {
|
|
return (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => {
|
|
const unsub = (target[prop] as (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void)(phase, fn)
|
|
rt.unsubscribers.push(unsub)
|
|
return unsub
|
|
}
|
|
}
|
|
return (target as unknown as Record<string, unknown>)[key as string]
|
|
}
|
|
})
|
|
try {
|
|
plugin.install(tracked as PluginContext)
|
|
plugin.hooks?.onLoad?.()
|
|
} catch (e) {
|
|
rt.unsubscribers.forEach((u) => u())
|
|
this.runtime.delete(plugin.id)
|
|
const i = this.order.indexOf(plugin.id)
|
|
if (i >= 0) this.order.splice(i, 1)
|
|
return { ok: false, reason: `安装异常:${String(e)}` }
|
|
}
|
|
this.onChange({ id: plugin.id, action: 'install' })
|
|
return { ok: true }
|
|
}
|
|
|
|
remove(pluginId: string): { ok: boolean; reason?: string } {
|
|
const rt = this.runtime.get(pluginId)
|
|
if (!rt) return { ok: false, reason: '未安装' }
|
|
if (rt.plugin.protected) return { ok: false, reason: '核心插件受保护' }
|
|
rt.unsubscribers.forEach((u) => u())
|
|
rt.plugin.uninstall?.(this.ctx)
|
|
this.runtime.delete(pluginId)
|
|
const i = this.order.indexOf(pluginId)
|
|
if (i >= 0) this.order.splice(i, 1)
|
|
this.onChange({ id: pluginId, action: 'remove' })
|
|
return { ok: true }
|
|
}
|
|
|
|
setEnabled(pluginId: string, enabled: boolean): { ok: boolean; reason?: string } {
|
|
const rt = this.runtime.get(pluginId)
|
|
if (!rt) return { ok: false, reason: '未安装' }
|
|
if (enabled && !rt.plugin.hooks?.onEnable) {
|
|
// 无 enable 钩子的插件视为直接切换 enabled 状态
|
|
}
|
|
rt.status.enabled = enabled
|
|
if (enabled) rt.plugin.hooks?.onEnable?.()
|
|
else rt.plugin.hooks?.onDisable?.()
|
|
this.onChange({ id: pluginId, action: enabled ? 'enable' : 'disable' })
|
|
return { ok: true }
|
|
}
|
|
|
|
list(): PluginStatus[] {
|
|
return this.order
|
|
.filter((id) => this.runtime.has(id))
|
|
.map((id) => {
|
|
const rt = this.runtime.get(id)!
|
|
const p = rt.plugin
|
|
return {
|
|
id: p.id,
|
|
name: p.name,
|
|
version: p.version,
|
|
kind: p.kind,
|
|
installed: rt.status.installed,
|
|
enabled: rt.status.enabled,
|
|
protected: !!p.protected,
|
|
dependencies: p.dependencies
|
|
}
|
|
})
|
|
}
|
|
|
|
has(id: string): boolean {
|
|
return this.runtime.has(id)
|
|
}
|
|
|
|
get(id: string): CotycPlugin | null {
|
|
return this.runtime.get(id)?.plugin ?? null
|
|
}
|
|
}
|