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() 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[0], fn: SystemHook) => { const unsub = (target[prop] as (phase: Parameters[0], fn: SystemHook) => () => void)(phase, fn) rt.unsubscribers.push(unsub) return unsub } } return (target as unknown as Record)[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 } }