总总览与图例
通用 Buff 模块:门面负责装配与主线程纪律,内核负责状态机、索引、池化与事件冲刷;一切宿主差异都经
IBuffHost.TryGetCapability<T>() 协商——模块本身不认识任何具体宿主类型。
外部协作面
11
产品文件(Domain 2 / Runtime 2 / 其余各 1)
3359
行产品代码(内核 BuffRuntime 独占 1376 行)
56 / 56
EditMode 用例通过(Buff 两组,2233 行)
61 类
C5 布局校验口径:61 类 / 502 成员块全部合规
1目录与类型
按目录看职责分层。public 为对外契约面,internal 为内核实现;所有类型共用命名空间 Game.Buff,不分子命名空间。
BuffSystem.cs360 行
模块门面:装配、线程纪律、生命周期、事件出口。唯一 public 入口,业务侧只见它。
BuffSystemOptions.cs17 行
容量与池化参数(4 组):实例 128/32/256、容器 8/64、token 32/256、临时集合 4/16/256(初容量 / 预热 / 保留上限)。
Domain/BuffContracts.cs388 行
全部对外契约:宿主抽象、值语义键、定义模型、枚举与结果码常量。
Domain/BuffSnapshots.cs111 行
快照 / 事件 / 恢复报告三件套,是模块与外界交换状态的全部载体。
Application/BuffApplication.cs430 行
应用策略与三个注册表:决定「已有同名实例时怎么办」,并在装配期把关定义合法性。
Effects/BuffEffects.cs133 行
效果运行时契约 + 工厂注册表。效果是模块唯一的「向宿主施加影响」的出口。
Infrastructure/BuffTimerScheduler.cs115 行
定时器抽象与 TimerManager 适配器:把框架句柄包成 BuffTimerHandle,隔离框架变更。
Mutex/BuffMutex.cs63 行
互斥规则抽象与默认实现:三种决策 ALLOW / REJECT_INCOMING / REMOVE_EXISTING。
Pooling/BuffPools.cs246 行
两个通用池与统计结构:对象池带双重归还防护(IsInPool),列表池服务操作期临时集合。
Runtime/BuffRuntime.cs1376 行
内核本体:状态机、四索引、Apply/Remove 全路径、事件冲刷、快照两阶段恢复、代际守卫。
Runtime/BuffRuntimeModels.cs120 行
三个池化载体:实例、宿主容器、定时器回调 token;各自实现 ResetForPool 清引用防跨池泄漏。
2编译期依赖
依赖单向向下:门面 → 协作者 → 内核 → 契约 / 外部。内核可以调用策略、效果、定时器,但协作者之间不互相依赖;
只有 Application 因注册期校验效果 Key 而单向引用 Effects。
文件级依赖矩阵
| 消费者 \ 被依赖 | Contracts | Snapshots | Application | Effects | Mutex | Infra | Pooling | RtModels | Runtime | 外部 |
|---|---|---|---|---|---|---|---|---|---|---|
BuffSystem.cs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | Core / Timer / Log / Global |
BuffSystemOptions.cs | — | — | — | — | — | — | — | — | — | — |
Domain/BuffContracts.cs | — | — | — | — | — | — | — | — | — | — |
Domain/BuffSnapshots.cs | — | — | — | — | — | — | — | — | — | — |
Application/BuffApplication.cs | ✓ | — | — | ✓ | — | — | — | — | — | Log |
Effects/BuffEffects.cs | ✓ | — | — | — | — | — | — | — | — | Log |
Infrastructure/BuffTimerScheduler.cs | — | — | — | — | — | — | — | — | — | Timer |
Mutex/BuffMutex.cs | ✓ | — | — | — | — | — | — | — | — | — |
Pooling/BuffPools.cs | — | — | — | — | — | — | — | — | — | Log |
Runtime/BuffRuntime.cs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | Log |
Runtime/BuffRuntimeModels.cs | ✓ | — | — | — | — | — | ✓ | — | — | — |
测试(EditMode) | ✓ | ✓ | ✓ | ✓ | — | ✓ | — | — | ✓ | NUnit / TestTools |
矩阵读法:依赖单向向下且无环;契约层零依赖是它唯一的稳定性来源。测试行是唯一允许引用内核文件的消费者(同程序集可见性由 asmdef 控制)。
3运行时对象与索引
内核的可变状态只有三种对象:实例(一个 buff 的一次挂载)、宿主容器(同一宿主下的实例集合)、回调 token(定时器回调的载体)。 三者全部池化,并通过四张索引表被检索。
4Apply 决策流
一次 Apply 先过六道拒绝门(每道都只记日志、不改状态),再由策略决定四个动作之一; 新建路径必须连过「候选创建 → 提交双守卫 → 定时器」三关才算成功。
5叠层路径
叠层不新建实例,而是把既有实例推进 Refreshing 状态、通知每个效果层数变化,再按策略给的时长模式重排定时器。 时序被设计成「先成功、后失效」:任何一步失败都回滚层数,绝不留下「有层数、无定时器」的半成品。
6移除路径
六条移除入口,一个收口点。TeardownInstance 负责「撤效果 → 摘索引 → 发事件 → 归还池」, 因此不存在第二条把实例从索引里拿掉的代码路径;批量入口只额外做一件事:先整体摘索引,再逐条撤。
7事件模型
三种逐实例事件 + 一种宿主级事件。逐实例事件在操作边界统一派发(消费者拿到的永远是稳定状态,而非中途态); HostChanged 不需要任何额外埋点,它由已产生的事件自动推导。
8快照与恢复
fail-closed 只针对快照的内部矛盾:快照自己矛盾(id 非法 / 重复)就整份拒绝;快照引用了外部解析不了的东西 (定义被删、宿主不在场、剩余量非法)则只跳过那一条。这条界线是为了避免「删掉一条 buff 配表 ⇒ 整份存档的 buff 全丢」。
9定时器与代际守卫
有限时长 buff 用一层适配器把框架定时器包起来:调度是原子的(失败时实例字段一个都不动),取消与重排保证「任一时刻至少一个有效定时器」, 回调入口再做三重比对把陈旧回调挡在外面。
10附录 · 契约与残留
画完图之后最该记住的东西:哪些是硬约束(改了就破坏抽象),哪些是已知取舍(知道就好,别当 bug 修)。
一、硬约束
| 约束 | 内容 |
|---|---|
| 主线程 only | 模块全部 API 只允许主线程调用(含查询与快照);跨线程调用被拒绝并记 Warning,返回 SYSTEM_NOT_RUNNING。 |
| 宿主无关 | 模块只认 IBuffHost。需要宿主侧能力时走 TryGetCapability<T>();效果实现不得放进模块内部去引用具体宿主类型。 |
| 事件延迟派发 | 事件在操作边界统一冲刷,且派发时会先把内核状态摆到稳定态;消费者不得依赖「回调时操作已结束」以外的时序假设。 |
| 单一收口 | 实例的撤除只有 TeardownInstance 一条路径;新增移除需求应接入这个收口点,而不是另写一套摘索引逻辑。 |
| 索引与所有权 | _instances 是唯一权威表;容器与 source 列表都只是引用集合,不得单独作为「实例是否存在」的依据。 |
| 效果调用序列 | Apply →(OnStackChanged)* → Revert → ResetForPool;多效果按定义顺序正序应用、逆序撤销;没有中间「结算」阶段(已删除的 Refresh 就是为了取消这个概念)。 |
| 装配顺序 | 效果工厂必须先于引用它们的定义注册;策略必须先于使用它的定义注册(定义注册期会校验两者)。 |
| 注册重复即拒绝 | 三个注册表对重复 Key 一律拒绝并返回 DUPLICATE_REGISTRATION;编程错误(null 实参)仍抛异常。 |
| fail-closed 的边界 | 快照内部矛盾 ⇒ 整份拒绝;外部引用解析不了 ⇒ 逐条跳过。修改这条界线会让「删一条配表」变成「整份存档 buff 全丢」。 |
| 值语义键 | BuffId/BuffRuntimeId/BuffHostKey/BuffSourceKey 是 readonly struct,相等性与哈希已实现且无装箱;不要用字符串当键传阅。 |
| Payload 不入快照 | 实例级载荷只服务于「本次挂载」,跨存档状态必须由效果自己用 CaptureState/RestoreState 持久化。 |
| 池化纪律 | 池化对象归还前必须 ResetForPool(清引用防跨池泄漏);IsInPool 只由池写入,外部只读。 |
二、已知残留与取舍
| 项 | 级别 | 状态与理由 |
|---|---|---|
| 批量移除的索引不自洽态 | 缺口 | DetachHost/RemoveBySource 为省 O(n²) 先整体摘索引再逐条撤;若某条被 Refreshing 守卫挡下,会出现「TryGet 查得到、GetByHost 查不到」的短暂态。定时到期或显式 Remove(runtimeId) 可自愈;无限时长 buff 会残留到显式移除。修法:改成逐条摘索引 + 守卫失败即回滚索引。 |
| 代际守卫防不住 token 复用别名 | 缺口 | TimerGeneration 是每实例计数而非全局单调,池 LIFO 复用 token 时不同实例可能同代际。生产安全依赖调度器的 Cancel 契约(已写入 IBuffTimerScheduler 文档)。 |
| 有限时长 buff 的「永久化」风险 | 已封 | 叠层重排失败会回滚层数并返回 TIMER_SCHEDULE_FAILED;首次 Apply 排定时器失败会归还候选。不存在「层数加了但没定时器」的路径。 |
| 叠层回调中重入移除 | 已封 | TeardownInstance 守卫包含 Refreshing;且 Apply 的 AddStack 分支拒绝重入叠层。 |
| 叠层路径丢失互斥移除 | 已封 | 互斥移除清单在首层与叠层两条路径都被消费,语义对称。 |
| 读档后 UI 不更新 | 已封 | 恢复清场不再抑制事件,且逐实例事件会自动派生 HostChanged;「恢复后一个 buff 也不剩」的宿主同样收得到信号。 |
| 效果层的 Refresh 阶段 | 已删 | 全模块只有一个调用点、与 OnStackChanged 信息量相同,而「重算综合影响」的职责归属性系统(PropertyBlock 基线 + 修饰重算)。效果只需 Apply / OnStackChanged / Revert。 |
| 定义注册表文档与实现不一致 | 已修 | 重复 Id 改为拒绝(与兄弟注册表对齐),类文档同步为「编程错误抛异常 / 数据错误返回码」。 |
| 过期移除的剩余毫秒恒为 -1 | 已修 | 过期路径显式传 0,-1 重新回到「无限持续 / 句柄未知」单一含义。 |
| 模块内 P3 打磨项 | 记录 | 结果码常量类混域、部分 public 类型实际仅内部使用(可见性收窄需先评估 InternalsVisibleTo 成本)、BuffTimerHandle 缺 ==/!=、池化对象只 Clear 不 Trim 等;均不影响行为。 |
三、演进记录(2026-9-13)
| 批次 | 内容 |
|---|---|
| 独立审查 | 四个只读审查镜头(生命周期 / 契约 / 内存 / 测试)并行分头审,主线程逐条复核后坐实 3 个 P1、4 个 P2 与两个 P0 级测试缺口。 |
| 修复五批 | ①三个 P1 + 迭代注释 ②两个 P0 测试缺口(含改造定时器替身支持多代并存与按 id 触发)③四个 P2 ④剩余测试缺口 ⑤P3 打磨。用例 35 → 54。 |
| 复核发现 | 五批全绿后逐行复核另择出两个新问题:重编号引入的「未知动作无兜底」回归(已加守卫)与一个把语义不同的键类型改成同名的陷阱(已改名并注释)。 |
| 两项演进 | 删除效果接口的 Refresh(7 → 6 个成员);新增宿主级 BuffEvent.HostChanged,复用已有的事件缓存机制零额外埋点。用例 54 → 56。 |
| 布局规范 | 新增 C5 源文件布局规范与校验工具,本模块 61 类 / 502 成员块全部合规;测试目录不纳入 C5(测试文件按「用例在前、替身在后」惯例)。 |
四、本图维护说明
本图是现状快照,依据 HEAD 62ef744 与当时工作区(Buff 审查五批修复 + 两项演进)。 代码结构变动(新增/删除类型、索引或池、改动 Apply/Remove/事件/快照的步骤)后需重新绘制; 纯实现细节(方法名、行内逻辑)变动不影响本图。图内不写行号,引用一律用 文件:符号。