技能管理器执行、克隆复用与私有随机数(v40)
2026-09-20。本批接入管理器的执行实例选择与活动顺序,拆开施法状态/冷却状态,并保存技能私有种子的来源。已有击杀回血与受击回血使用新路径。完整攻击型 proc 事件消费者仍待接入,TYPE201/204 没有提前开放。
构造审计保持:1419 个定义,1099 可构造、273 明确阻塞、47 缺基础定义/创建等级、0 构造异常。全武器目标未完成。
原版调用链与状态
来源:原 Torchlight2.exe,SHA256 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e。只读 IDA 捕获和独立机器码参考,不 attach 原游戏、不修改 EXE/PAK。
| 入口 | 本批使用的事实 |
|---|---|
| 6D6FA0 | 按指针执行;空指针/全局禁止/CANPROC 门;exclusive abort;活动指针转名字查找;reseed 后 start;成功才追加活动项和消耗 charges |
| 6D6CD0 | 名字表里选第一个不在活动表的实例;全忙则克隆最后一个,再尝试启动 |
| 6D69D0 | 克隆共享原定义,拷贝源 charges 等字段;临时克隆不进入基础列表,但进入名字索引 |
| 6D61F0 | 退休后只有名字已不在索引才排入删除;不能因不在基础列表就删除克隆 |
| 6BE7A0 | 保存请求种子;请求为零才用 GetTickCount 初始化实际 RNG |
| 6C3A30–6C3A60 / 676D90 | 启动 CHANCE 的无符号检查与技能自身 MWC 流 |
| 6C1C30 / 6D7370 | 每次活动项 tick 扣 float 冷却;仍在施法或冷却为正都可留在活动表;移除采用末项补位 |
活动表 +52/count+56 与 CSkill+64 的施法标记不是一个状态。冷却可以已负但布局仍执行;这时重入会找另一个空闲实例或克隆。临时克隆结束后仍留在名字索引,下次可复用,而不是无限创建又立刻销毁。
EXCLUSIVE 比较技能定义的 UNIQUE_GUID。abortHard 清除施法状态/事件,不立即等同于从管理器活动表移除;指针入口和名字入口各自的 exclusive 扫描顺序保留。外部事件中止副作用要求消费者暂存处理,不能只删一条计时器。
charges 保留原 6D3B80 规则:成功后读取同名第一个实例的 charges;仅当它为正时,给所有同名实例写 first-1。零 charges 不在这个执行入口阻止 proc;手动技能使用门是另一层。
私有种子公式
6D6FA0/6D6CD0 在启动前计算:
requested = (manager_seed64 + (has_target ? target_runtime_guid64 : 0)
+ sign_extend_i32(salt)) mod 2^64
effective = requested != 0 ? requested : zero_extend_u32(captured_GetTickCount)
- requested 写 CSkill+472/+476;effective 写 +456/+460,必须分开保存。
- 没有目标指针时忽略 target GUID;有目标但 GUID 未知时不能擅自填零。
- salt 按有符号 32 位扩展,包含负数、跨高低字进位和 64 位回绕。
- 零种子的外部 TickCount 由宿主捕获后注入,纯核心不读墙钟。
- target_runtime_guid 不是 DAT 的定义 GUID,也不是 CoreSim 的整数单位 ID。
实际运行对象的 GUID 来自 CEditorBaseObject/EditorMakeGuid 路径;已有 NativeEditorGuid 内核以原 UUID 字节和 synced 流计算,但当前战斗对象生产方尚未完整接入该身份。因此不能据此宣称所有攻击型 proc 的技能私有随机数已经自动可用。
已有固定回血程序没有私有游戏随机数消费者,未捕获的两个种子字段继续保存为 null;没有伪造值。非 effect-only 程序必须提供种子和事件运行消费者,否则明确拒绝。
两层概率不混用
外层武器 proc 的概率(582380 等)即使 100% 也要掷骰,沿用已有 ProcEffects 规则。
技能内部启动 CHANCE(6C3A40)是另一层:
if uint32(CHANCE) >= 100: pass without a draw
else: roll skill_MWC[0, 100]; pass iff roll <= CHANCE
所以 CHANCE 负整数在这里转无符号后走无抽样成功分支;0 仍会抽一次且可能 roll0 成功。不能用统一的 RollPercent 快捷逻辑覆盖两层。
启动失败也必须返回变更后的技能状态:CHANCE 已消耗的私有随机数不回退,但不加入活动表、不扣 charges。NativeProcStartAttempt 用独立 Started 标志表达这一点;不再以 null 丢弃失败前状态。
实现与存档
- NativeProcExecution:实例选择、克隆/复用、exclusive 的暂存回调、私有 reseed、启动结果与活动顺序维护。
- NativeProcSkills:已有 effect-only 开始/推进改用新路径;BeginEffectOnly 返回真正启动的实例 ID,效果绑定不再默认指向最初请求的那个实例。
- F2WeaponProcs.ExecutionDefinition:读取原层级上的 CANPROC、EXCLUSIVE、CHANCE、冷却和 UNIQUE_GUID;CANPROC 不等于 CAN_PROC_FROM_SKILL。
- schema 40:ActiveOrder、Executing、InBaseList、InitialSeed、EffectOnly、Available 与已有 EffectRng 一起保存;数组在复制/恢复时脱离原存储。
- v39 以前正式实现只会在每个回血技能管理器里保留最多一个活动实例,可无歧义恢复该顺序。任意旧多活动实例缺顺序时不按注册顺序猜测,显式报告 legacy_proc_active_order_unknown。
- 冷却为 float 并保留负数过冲;当前 effect-only start 事件结束与计时结束分开,临时克隆仍在名字集合中。
验证范围
- 341 组原版管理器参考:真实执行 6D6FA0、6D6CD0、6BE180、6BE1A0、6BE7A0。覆盖不同活动顺序、空闲/全忙、克隆、模式、概率前门、exclusive、成功/失败、charges 与种子边界。
- 名字树、内存分配/克隆构造、level setter、abort 的外部事件、完整 CSkill start 和 charges 更新是明确 stub 边界;并未把它们伪称为机器码全链路实测。
- 另 54 组执行原 6C3A30 概率片段及原 676D90 MWC,比较成功标志和私有流状态。片段前后的完整事件启动流程不在这个参考内。
- 1698 条核心断言通过;包含克隆退休后复用、施法跨过冷却、末项补位、概率失败保留 RNG、数组隔离和 v40 字节稳定重放。
- 349 项相关 Python 回归通过;C# 构建 0 warning / 0 error,coresimlint 通过。
- 实际 ECHO PASS headless 主场景:GREATHAMMER_U03 原击杀回血,seed43;0.1 秒后 HP 100→137.760009765625,逐步恢复、到期和重放通过。
- 同期新跑 SWORD_U03 原受击回血,seed25;原怪物攻击先使 HP 100→94,0.1 秒后到 131.760009765625,回归通过。
两条场景都使用既有隔离 HP/等级/种子夹具。没有新增视觉资源;headless 结果不当作视觉一致性证据。
产物:build/native_proc_execution_oracle/results.json;build/proc_execution_core.log、proc_execution_regression.log、proc_execution_world.log、proc_execution_struck_world.log、proc_execution_audit.log。
剩余工作与安全
这里完成的是管理器执行/活动状态和私有 RNG 边界,不是完整 CSkill 生命周期。仍需:真实运行 GUID 的产生/恢复接线、CSkill start 的有效等级/施法上下文、事件实例创建与 tick/end/abort、施法者/目标死亡与所有者变更策略、实际世界查询,以及攻击/召唤型事件消费者。
下一批继续上述运行输入和事件消费者,优先让 Stormclaw 真正从原武器命中触发;不能以这批管理器参考代替它的完整世界验收。
按 godot-master 使用定义/状态/显示分层;没有套通用技能示例的墙钟冷却或额外全局冷却。
在 main 工作,未提交。默认 warp_autosave.json SHA256 保持 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f;测试只写隔离槽。全武器目标仍在继续。