文档定位与维护规范: 本文是 tl2-mikuro-runtime 项目的核心技术账本与拦截规范。在代码库中增补、废弃或修改任何 Hook 时,必须同步更新本文对应条目及末尾的「更新记录」。

  • 开源代码仓库:heiybb/tl2-mikuro-runtime
  • 虚拟地址基准:文中所注 sub_XXXXXX 与十六进制绝对地址 0xXXXXXX 均以零售版 Torchlight2.exe v1.25.9.5 的静态逆向工程数据库(IDA Pro 默认基址 0x400000)为参考坐标,非运行期绝对内存地址。由于原版程序启用了 ASLR(地址空间布局随机化),运行期全部基于 AOB(Array of Bytes)字节特征码进行动态内存扫描与重定位。
  • 实证性与结论边界:本文所有机制结论均严格提取自源码及其实机断言验证过的「实证」范畴;对于源码中明确标明「未证实」或「待验」的边界,均予以客观保留,坚决不进行主观外推。

0. 架构定位:Runtime 的运行机制与功能矩阵 ​

tl2-mikuro-runtime 在物理交付层面表现为一个 d3d9.dll 动态链接库代理(底层集成 MinHook 框架与 ImGui 渲染管线)。部署至游戏根目录后,宿主进程 Torchlight2.exe 在初始化 Direct3D 9 运行时将优先将其加载。

运行时首先链接底层的真实 Direct3D 9 实现(优先检索并加载同目录下更名的 d3d9_dxvk.dll,若不存在则回退至 Windows 系统目录的 d3d9.dll),随后创建独立的初始化工作线程,利用 AOB 字节特征码在已加载的游戏进程镜像中动态检索数十个目标引擎函数,有序实施入口 Detour 拦截或函数体内联补丁(Inline Patch / Code Cave)。

其承载的功能覆盖十大技术领域,面向不同受众提供清晰的工程支持:

功能大类纯原版游戏受众挑战者大陆(MOD)受众索引章节
原版缺陷托底(崩溃预防、内存溢出修复、技能冷却生效修复)直接获得系统级稳定性提升直接获得系统级稳定性提升§3.2
原版机制修正(矫正原版底层逻辑矛盾,默认开启,可配置关闭)获得数值与机制一致性保障获得数值与机制一致性保障§3.3
MOD 兼容托底(原版数据保持逐位等价,特定 MOD 环境激活兼容逻辑)机制完全透明(无侵入)提供关键兼容性支撑§3.4
启动性能优化(重构热点路径瓶颈算法,消除无用拷贝开销)显著缩短启动耗时启动耗时大幅缩减(数据量越大收益越显著)§3.5
掉落生命周期过滤(规则化拦截不合规道具落地生成)按需配置生效按需配置生效§3.6
死槽 131 自定义效果体系(属性上限突破、扩展词缀机制、宠物嘲讽)机制完全透明(原版数据未引用)构成扩展玩法与装备构筑的核心底座§3.7
卷轴与附魔渠道扩展(乱铸、强化、词缀迁移、附魔剥离、神明祝福、涤除)机制完全透明(基于专属菜单名物理隔离)构成扩展玩法与高级打造的核心底座§3.8
视觉渲染 / HUD 强化(镶嵌发光、传奇光晕、护盾实时吸收计量面板、战斗伤害统计面板)按需配置生效按需配置生效§3.9
操控模式扩展(WASD 平滑键盘移动)按需配置生效按需配置生效§3.10
底层诊断探针(地形栅格导出、网格可视化投影、VFS 耗时剖析、帧耗时采样,默认禁用)仅在系统排障与逆向分析时挂载仅在系统排障与逆向分析时挂载§3.11

实机验证构建版本:

  • 官方零售版(Retail)v1.25.9.5;
  • Steam 数字版 v1.25.5.6。 除个别注明「Steam 版待验」的项目外,所有内置 AOB 特征码均通过了双版本的唯一命中性交叉核验。

1. 安全总则:动态拦截的八大工程规范 ​

为确保「系统托底」的确定性,Runtime 遵循以下最高工程准则:宁可个别扩展特性静默失效,也绝不引入比未加载时更差的系统状态或额外不稳定因素。

  1. 全面采用 AOB 动态签名定位,严禁硬编码 RVA:
    • 早期通过静态 RVA 定位的方案在跨版本(如 Steam 数字版)执行时导致代码偏移错位(例如曾引发宠物嘲讽必崩问题),自 2026-07-23 起已全量切换为字节特征码动态扫描。
    • 签名规范要求:受重定位表(Relocation Table)影响的动态修正字节(SEH 结构化异常处理器入口、mov reg, offset 寻址、IAT 导入调用)一律强制通配;E8/E9 相对寻址指令(rel32)的操作数一律强制通配(该操作数虽不受 ASLR 影响,但跨版本重编译时位移必定发生改变)。
    • 交付前由工程审计脚本 tools/aob_check.py 直接解析多版本 PE 结构,校验特征签名的全局唯一性及重定位掩码覆盖率,完全脱离对外部逆向工具环境的单点依赖。
  2. 特征未命中则静默自禁用,贯彻原子性启停:
    • 任一功能模块所依赖的关键特征码若未实现唯一命中,该模块必须整体拒绝挂载,并在运行日志中明确记录为「未启用 / 已搁置 / 不支持的游戏版本」。具备多重拦截点的复合模块(如 DoT 护盾结算、外观槽多人同步、技能冷却修复等)必须执行严格的原子性启停逻辑——要么全量部署,要么整体撤销,绝不允许残留半套拦截逻辑。
  3. 只读内存探测路径全面包裹 SEH 异常防护(__try / __except):
    • 针对游戏内部未初始化指针或野指针的探测性解引用,一律通过结构化异常处理进行边界防护。偶发异常仅抛弃当前单帧计算,坚决杜绝进程崩溃。
    • 由于 MSVC 编译器限制 __try 块内不可存在依赖栈展开(Stack Unwinding)的 C++ 局部对象(编译器告警 C2712),所有直接访问游戏堆栈的底层叶子函数均被严格解耦为纯 POD(Plain Old Data)接口。
  4. 共享引擎全局常量遵循瞬态修改模式(Scoped Ephemeral Patching):
    • 当需要修改引擎全局常量时,必须严格执行「读取并暂存原值 → 写入目标值 → 触发原版逻辑 → 立即恢复原值」的作用域局部修改模式,严禁长期常驻修改。
    • 原因在于核心数值上限常量具有跨系统共享特征(例如闪避率上限与格挡率上限在引擎内部复用同一个 75.0 浮点常量),全局常驻修改势必污染其他无关调用链。鉴于游戏核心战斗与 UI 渲染均运行于引擎主线程且原版算法无递归嵌套,该执行窗口在时间切片上完全免疫多线程并发冲突。
  5. 跨 CRT 堆对象生命周期严格委托游戏引擎原生接口管理:
    • 原版游戏基于 VC90 编译构建并动态链接 MSVCP90.dll,而 Runtime 代理 DLL 基于现代 C++ 工具链编译,两者底层的堆分配器与 STL 容器内存布局物理隔离。
    • 涉及跨模块交互的 std::wstring 构造、赋值与析构必须显式从 MSVCP90.dll 导出表按符号修饰名动态导出并调用;新增 CEffect 对象必须调用引擎内存池分配接口(allocHolder);注销 Effect 对象必须调用引擎原生的 expireEffects(将 duration 持续时间写入 -900.0 哨兵标记)。
  6. 多级分层运行期开关配置体系:
    • 支持 mikuro_runtime.conf 配置文件、游戏根目录标记文件与环境变量三种控制手段。
    • 在工程实践中,文件系统标记文件具备最高可靠性:由于环境变量在经过启动器或 Steam 客户端多级进程派生调用时极易发生继承丢失(2026-07-25 实机测试踩坑坐实),底层关键模块的开关均以标记文件为首选硬件级断开机制。
  7. DLL_PROCESS_DETACH 规避侵入式 Hook 卸载:
    • 在进程正常退出时,Windows 处于 Loader Lock 状态。此时强行挂起工作线程或撤销 MinHook 挂钩容易触发死锁或内存访问违例。代理 DLL 的生命周期与宿主进程完全绑定,统一由操作系统在进程销毁时完成资源回收。
  8. 前置核心总闸防护机制(Master Gatekeeper):
    • 掉落物统一落地放置函数 sub_5EAA80 与 CItem 的 RTTI 类型描述符构成本框架最核心的两条基础特征码。在 InitThread 执行期,若此二者未成功命中,初始化流程将立即终止返回,后续所有核心 Hook 一律放弃挂载。
    • 仅有时序上排在总闸之前的先行模块不受其制约:包括两项启动性能补丁、VFS 探针、WASD 基础地址解析、外观槽多人同步以及冷却词条修复(上述模块均自备独立的特征码与自检校验器)。

2. 装载时序机制(hooks.cpp InitThread) ​

模块装载顺序具有严格的因果依赖关系,必须依照以下时序流水线依序执行:

DllMain 在创建 InitThread 之前先同步执行 ApplyModCapEarly()(父 MOD 上限,仅 1.26 且勾选时生效,见 §3.4):它必须早于数据加载线程,放进 InitThread 任何位置都有竞态。

  1. optfindfile::Install():⛔ 2026-09-23 起已停用(编译期硬关,直接返回 Unsupported,不装任何 hook);以下为历史说明。挂载 VFS 文件检索哈希索引。该操作必须抢在 AOB 特征码全量扫描前完成:DllMain 注入触发时,宿主游戏的数据加载线程往往已在数毫秒内拉起,若等待二十余条签名检索完毕则将错过热点加载窗口。由于该性能补丁与 VFS 探针均需拦截 sub_68F630 邻近区域,而 MinHook 不允许重复挂载同一入口,因此补丁模块享有绝对优先权。
  2. optunitscan::Install():挂载 unittype 全量扫描无用拷贝优化补丁。
  3. vfsprobe::Install():挂载 VFS 加载耗时探针(默认处于未装载状态)。
  4. ResolveEngine():启动全局 AOB 签名扫描,检索核心系统地址:道具落地函数、命名属性访问器、GUID 到实体定义映射器、本地化翻译表实例、资源管理器以及四大 RTTI 描述符。
  5. lf::LoadConfig():解析配置文件 mikuro_runtime.conf(该配置必须在回放 WASD 状态前完成读取)。
  6. wasd::Init():仅动态解析 WASD 所需的两组内存指针并暂存,不挂载任何 Hook。
  7. MH_Initialize():初始化 MinHook 核心引擎;若初始化失败则整个 Runtime 立即终止。
  8. cosmeticmp::Install():挂载外观槽多人同步模块(自备独立 AOB 特征码与自校验逻辑,不依赖前置总闸)。
  9. skillcdfix::Install():挂载技能冷却词条生效修复模块(同样自备特征码与自校验逻辑)。
  10. 依据配置项判定若 wasd == 1,则调用 wasd::SetActive(true) 执行延迟懒装载。
  11. 核心总闸安全判定:检查 pPlace 或 CItem RTTI 指针;若解析为空则打出警告日志并终止后续拦截安装。
  12. 挂载道具落地 Detour;注册掉落决议器探针(编译期宏默认关闭);注册走格探针(受标记文件门控);执行护盾 HUD 布局签名静态核验(零 Hook);挂载青莲术士定身阵的怪物 AI 冻结 Detour(qlssdingshen::Install,见 §3.13)。
  13. 挂载跨关卡不持久化地面掉落逻辑(拦截两组关卡序列化函数);挂载存盘/读档崩溃保护补丁。
  14. 挂载主动技能施放 Detour(百分比冷却缩减与宠物嘲讽令);挂载宝石镶嵌部位限制(两处拦截点);挂载宝石发光与传奇装备光晕渲染模块。
  15. 挂载诅咒/强化卷轴应用逻辑;挂载附魔迁移与剥离控制器(一次性注入 21 处底层原语地址)。
  16. 挂载 EFFECTSLIST 数据加载器 Detour(必须先执行 ResolveEffectArrays() 扫描未被修改的原始函数体,再挂载 Hook,防止入口补丁指令破坏特征匹配)。
  17. 挂载属性重算防崩溃内联补丁;挂载暴击状态门控;挂载暴击吸血逻辑;随后装抵命符(deathward,§3.7)与青莲术士的曲则全 / 神霄雷池 / 墨斗弹线 / 五鬼搬运 / 九老仙都印(§3.13)—— 它们回血扣血都借 modifyHealth 的原函数,所以排在暴击吸血之后;九老仙都印还必须排在第 14 步的主动技能 Detour 之后,原因见 §3.13;最后挂载 Tooltip 描述信息子类型分流器。
  18. 挂载四大数值上限突破(减伤、暴击伤害、闪避率、格挡率)以及角色属性面板减伤数值同步补丁(面板同步复用减伤全局常量地址,因而严格排在减伤挂载之后)。
  19. 挂载 DoT 伤害护盾吸收结算补丁(要求三项底层符号全数命中,缺一不可)。
  20. ResolveTaunt():预热检索嘲讽所需的四条辅助特征码(规避首次施法时在关键帧热点路径上全镜像扫码引发卡顿)。
  21. overlay::InstallD3D9Hooks():构建虚拟 Dummy D3D 设备以提取设备虚表(vtable),随后对虚表槽位挂载 EndScene 与 Reset 拦截点;走格可视化探针在此阶段一并挂载 SetTransform。

3. 内存拦截与补丁实现总账 ​

表格字段规范:

  • 拦截方式:detour 表示基于 MinHook 的函数入口跳转拦截;内联 表示在函数体内写入跳转中继(Code Cave / 补丁桩);改向 表示仅重定向单个 call rel32 的相对跳转偏移;vtable 表示劫持虚表函数指针;子类化 表示通过 SetWindowLongPtr 劫持窗口消息处理过程。
  • 未命中回退:记录特征签名不匹配或底层依赖缺失时的系统降级策略。

3.1 核心基础设施(注入与覆盖层渲染) ​

目标标识拦截方式源文件 / 入口点功能职责配置开关未命中回退
Direct3DCreate9 / Direct3DCreate9Ex导出转发hooks.cpp
Wrap_*、d3d9.def
注入引导入口。优先动态加载游戏同级目录下的 d3d9_dxvk.dll(DXVK 运行库),若不存在则加载系统原生的 d3d9.dll;对其余 D3DPERF_* 接口执行无损透传转发。—底层 Direct3D 驱动加载失败时返回空指针。
IDirect3DDevice9::EndScene
(虚表槽位 42)
vtableoverlay.cpp
HookEndScene
仅在「当前渲染目标 == 主后备缓冲区(BackBuffer)」的调用切片内驱动 ImGui 绘制管线。每帧驱动:WASD 移动更新、走格探针更新、附魔面板关闭后 NPC 对话关闭兜底、宝石发光与传奇光晕绘制、走格网格透视投影、护盾 HUD 仪表、诅咒结果结算面板及主配置菜单;另跑抵命符的护盾施放 / 碎符销毁与「抵命符碎裂」提示(§3.7)、青莲术士各模块的每帧逻辑(§3.13)。当捕获到底层设备重置(指针变更)时自动重新绑定 ImGui 后端。Home 键切换主面板显隐无法构建 Dummy 设备时覆盖层禁用,其余 Hook 保持正常运行。
IDirect3DDevice9::Reset
(虚表槽位 16)
vtableoverlay.cpp
HookReset
在图形设备执行 Reset 之前强制释放 ImGui 占用的 D3DPOOL_DEFAULT 资源,在设备重置成功后重新构建显存资源(杜绝重置后引用野指针引发崩溃,大幅提升 DXVK 环境下的重置成功率)。重置时一并清空诅咒结算记录与宝石发光渲染缓存。—同上。
游戏窗口消息过程子类化overlay.cpp
HookWndProc
处理 Home 快捷键;面板展开时将消息分发给 ImGui。光标位于 ImGui 面板区域时强制吞掉全部鼠标输入(包含 WM_LBUTTONUP,若仅拦截按下事件会导致游戏角色被地面寻路逻辑唤醒移动),获取文本焦点时吞掉键盘输入;面板处于关闭状态时仅分发 WM_MOUSEMOVE 且不拦截消息(维持护盾条悬停提示所需的光标坐标更新)。严格规避干预系统光标状态(启用 NoMouseCursorChange),防止与引擎原生的 SetCursor 产生竞态闪烁。——
IDirect3DDevice9::SetTransform
(虚表槽位 44)
vtablegridprobe.cpp
Detour_SetTransform
拦截并捕获引擎的 VIEW 与 PROJECTION 变换矩阵,供 F10 快捷键在场景地表投影绘制可视化走格网格。仅在显式启用走格探针时动态挂载;探针未激活时函数体仅保留单次布尔判断。详见 §3.11探针未挂载时不实施拦截。

3.2 原版缺陷托底(稳定性与正确性修复) ​

此类补丁旨在修复原版可执行文件的程序实现缺陷,完全不干预原版预期的游戏机制设计;在不加载任何 MOD 的纯原版环境下依然能够显著提升系统稳定性。

目标标识拦截方式源文件 / 入口点缺陷根因与修复实现方案配置开关(默认)未命中回退
存档编码器 sub_680DC0
(关卡过渡 / 存盘序列化)
detour
(Naked 桩函数)
hook_savefix.cpp
Detour_Scramble
缺陷机理:原版底层在序列化期间连续执行两次 malloc(bodyLen),且未对返回指针做任何空指针校验。在大容量背包或大仓库 MOD 环境下,存档数据显著膨胀,32 位进程地址空间极易触发内存耗尽(OOM)。解引用空指针直接引发进程崩溃,表现为「过图切关卡时瞬间闪退」。
修复实现:入口桩函数预先利用 VirtualAlloc 分配独立临时缓冲区并安全完成数据加扰,将内部状态位标记为 flag = 1,随后无条件跳转回原函数;原函数检测到 flag == 1 时在执行 malloc 之前即刻提前返回,既避免了原版内存二次分配崩溃,又安全利用原函数析构了以值传递的 std::wstring 临时变量。数据峰值内存消耗由 $2 \times \text{body}$ 骤降至 $1 \times \text{body}$。
saveFix
(默认开启,关闭即完全等价于原版行为,供故障比对)
签名未命中则跳过修复,记录警告日志 [W]。
存档解码器 sub_681210
(读档反序列化)
detour
(Naked 桩全量替换)
hook_savefix.cpp
Detour_Descramble
缺陷机理:解码端同样存在双 malloc 缺陷,在 OOM 状态下直接调用 memset(NULL) 引发硬崩溃,表现为「选择角色载入或打开储物箱时闪退」。由于原函数在执行 malloc 之后才判断状态位,入口桩无法直接借用其退出路径,必须实施全量替代。
修复实现:流式解密过程中同步计算 djb2 校验和,严格确认校验和无误后方才提交明文数据并主动析构 std::wstring 返回 1;遭遇 OOM、校验和失配、异常存档版本或文件 IO 错误时一律拒绝提交,跳转至原函数降级执行(完全复刻原版异常行为)。该算法与 tl2-save-toolkit 工具链在字节级一致,经 1,200 个合成存档与 21 个生产真实存档校验实现零误差。
同上同上。
CEffect 属性缩放求值循环 sub_7A6530
(fmul 指令前 27 字节)
内联
Code Cave
hooks.cpp
InstallEffScaleGuard
缺陷机理(2026-07-29 经 5 份实机 Crash Dump 与动态探针证实):在 recomputeStats 遍历 Effect 链表执行属性重算期间,同帧失效过期的 Buff 或词缀注销操作会提前释放当前正在被求值的 CEffect 对象内存。外层循环携带已析构的悬垂 this 指针执行 fmul dword ptr [eax] 指令解引用空指针崩溃。该竞态概率与词缀销毁频次正相关,在多被动技能或高频触发词缀的构筑中呈极高发病率。
修复实现:补丁必须打在循环体求值流程内部(此时对象在入口时尚未析构),注入内联桩校验 [esi+104h] 是否已置零;若检测为已释放的悬垂状态,则将乘数修正为 0.0f(严禁修正为 1.0,st0 寄存器中存有基础属性值,乘 1.0 会误将已销毁的词缀按 100% 权重结算)。正常健康数据下绝不满足触发条件,不干涉底层内存分配生命周期。
无开关
(致命崩溃修复,必须强制常驻)
签名未命中则拒绝安装,记录警告日志 [W]。
CUnit::executeInstantEffect
Case 216 与 Case 217 尾部
内联
30 字节补丁桩 ×2
skill_cd_fix.cpp
CdApplyAll
缺陷机理:Case 216(MODIFY SKILL COOLDOWN REMAINING)与 Case 217(SET SKILL COOLDOWN REMAINING)原版在处理技能冷却变更时,仅调用 findSkillByName 获取同名技能链表的首个实例 list[0]。但在技能连招或连续施法过程中,原技能实例若仍在活动队列中,引擎会自动克隆生成新的 CSkill 实例(克隆体常驻不会被立即销毁)。若当前正在冷却的是克隆体,原版修改的却是早已冷却完毕的基准实例,直接导致玩家观察到「冷却缩减词条完全失效」;该异常随世界刷新或重启进程恢复,但在地图切换或洗点时持续存在。
修复实现:重写两处 Case 出口,压入 4 项参数调用 CdApplyAll,根据定义指针(CSkill+0x10)递归遍历活动表与基准表内的全部同技能实例:MODIFY 执行累加运算,SET 执行直接赋值,负数及 NaN 异常值自动归零纠正;仅当实例原先未在活动表中且冷却时间由 $\le 0$ 跃迁为 $> 0$ 时方才调用 pushActiveSkill(防止重复入表导致每帧双倍衰减);顺带根治了 SET 0(强制移除当前冷却)以往遗漏克隆体的问题。
装载期安全自校验:两处补丁点必须各自唯一命中,且解析出的 3 组内部引擎函数地址完全一致,findSkillByName / setCooldownRemaining / pushActiveSkill 函数体经逐字节签名比对完全吻合,任一校验失败则整体放弃注入。
写入事务(2026-09-12 重做):内联补丁改变指令边界并以 0xCC 填平尾部,装载那一刻若有线程正处在这 44/50 字节里、或从这里 call 出去尚未返回,它一回来就落进错位的指令流——从旧 findSkillByName 返回到 site+0x0E(新桩里是 push mode,随后 add esp,10h 多抬 12 字节,尾声从错位的栈恢复寄存器与 fs:[0])或从旧 setCooldownRemaining 返回到 0xCC 上,栈与 SEH 链同时损坏;玩家 WER 中 38 例 ntdll 栈访问违规与 FAST_FAIL_INVALID_EXCEPTION_CHAIN 与此吻合(docs/player-crash-records-2026-09-12.md)。旧实现只检查「EIP ∈ 补丁区间」且 MODIFY / SET 两次独立写入,恰好漏掉上述两种在途形态并会留下只改一半的 Partial。现改为:由 SIG_EXEC_HEAD 定位 CUnit::executeInstantEffect 函数头,取其专属 x86 SEH handler(序言 68 <handler> 操作数,位于 head+3);判据为「线程 fs:[0] 链上挂着该 handler(= 线程在此函数或其被调者内)∨ EIP ∈ [head, head+21)(帧尚未挂上的序言窗口)」(seh_frame_probe.h);写入是一次事务(quiescent_write.h):逐线程预检 → 枚举并打开全部线程 → 全体挂起 → 挂起态逐线程复核 → 两处一起 memcpy → 恢复,任一条件不成立整轮放弃重试,预算 20 s;耗尽则两处都不写,状态 Failed,不再存在 Partial。挂起窗口内不打日志、不分配、不 VirtualProtect(页权限在事务外改好)。压测 tools/test_quiescent_write.cpp:12 线程反复进出手工挂帧函数,帧内自检字节从未在脚下变化、两处始终同代;赖在帧内的线程令事务在预算内放弃且一字节不写。
2026-09-13 复核修正(docs/review-fixes-2026-09-13.md):① 事务在全体挂起之后再枚举、直到一轮没有新线程——第一次枚举之后、全体挂起之前出生的线程原本不在检查集合里,却能跑进目标函数(复核第 1 条,探针确定性复现);能创建线程的只有还在跑的线程,已知线程全部挂起后新面孔只可能来自这段窗口,把它们也挂起再枚举即收敛。窗口内的枚举改用 ntdll!NtQuerySystemInformation 写入挂起前 VirtualAlloc 的缓冲(CreateToolhelp32Snapshot 内部用进程堆,在挂起窗口里是死锁隐患);压测新增「第一次枚举后出生并进帧的线程」用例,须被第二轮枚举抓到且一字节不写。同日二次复核 F1:SuspendThread 只是发出异步请求,返回时目标可能还在跑(例如正在内核里把子线程创建完);GetThreadContext 才会等到它真正停下。故每次挂起请求之后立刻对该线程做一次 GetThreadContext,成功了才算"停了",之后再枚举;已发出请求的线程一律计入待恢复集合。压测再加「异步挂起模型」用例(创建者的挂起到 GetThreadContext 才落地、落地前把子线程创建完并进帧),事务须不写。② 两处补丁点同页,页属性改为按一段改一次、恢复一次——原先分两次改、按原顺序恢复,会把第二次拿到的 RWX 当"原值"写回,页永久 RWX(复核第 4 条)。
标记文件 mikuro_no_skill_cd_fix.txt
或环境变量 MIKURO_SKILL_CD_FIX=0 可手动关闭
命中数 $\ne 1$ 或自校验不通过则不修改任何字节;预算内等不到在途帧排空同样一字节不写(Failed,无 Partial)。
进程顶层异常过滤器
(SetUnhandledExceptionFilter,先于 Torchlight2.exe 0x8147A0 TopLevelExceptionFilter)
顶层过滤器链
(非 hook,独立助手线程)
crash_dump.cpp
crashdump::Install / VehFilter / Filter / Helper
缺陷机理(2026-09-13 经 53 份玩家 WER + 本机复现实验证实):游戏自带的崩溃处理器在崩溃线程自己的栈上 LoadLibrary(dbghelp) → MiniDumpWriteDump → 再加载 dbgcore.dll;原始异常是栈溢出(0xC00000FD)时栈只剩几 KB,加载器在 SxS 查表的 push 上二次 AV,WER 因此记成 ntdll.dll+0x53B02 c0000005,游戏目录外的 dump 是 0 字节——玩家 39 次「ntdll 闪退」全是这一形态,真实原因被完全掩盖。修复(两道入口):① AddVectoredExceptionHandler(1,…) 只认栈溢出 —— VEH 是独立的链,SetUnhandledExceptionFilter 抢不走它(玩家实测:装好后进游戏,游戏在 WinMain 与每次 C++ 抛异常经 CRT 路径反复重设该槽位,把我们那份顶层过滤器顶掉,崩溃只落到游戏处理器 → 仍 0 字节;tools/test_crash_dump.cpp 的 o 用例专测此路:装好后夺走槽位,VEH 仍写出 dump)。② 顶层过滤器兜其余未处理崩溃。两道都在崩溃线程上只做抢占标志 / 交出 EXCEPTION_POINTERS / SetEvent / 等待(不进 CRT、不分配、不 LoadLibrary);自带 1 MB 保留栈的助手线程(启动时预加载 dbghelp + dbgcore)写 MiniDumpNormal | WithIndirectlyReferencedMemory | WithThreadInfo | WithUnloadedModules | WithHandleData | WithProcessThreadData,并经 MINIDUMP_CALLBACK_INFORMATION 的 MemoryCallback 自行补内存段(CollectRanges:在助手线程上扫崩溃线程栈里的指针,对指向已提交私有堆的每个指针按页取 〔前 1 页 + 所在页 + 后 2 页〕= 16 KB 加进 dump;页号哈希去重,条数 2048 / 字节 16 MB 双闸;崩溃线程此刻阻塞在 WaitForSingleObject(g_done) 上,栈不再变动,全程只读 + VirtualQuery,外包 __except)——WithIndirectlyReferencedMemory 自带的窗口实测只到 +0x2FF,CCharacter(0x7EC)的 状态 +0x314 / 当前目标 +0x320 / CAIManager +0x7E8 全在窗口外,2026-09-15 那批栈溢出转储因此只能看出「1246 层递归都是同一批对象」却读不出那只单位的状态。到 tl2_mikuro_runtime.log 同目录 tl2_mikuro_crash_<日期>_<时间>_<异常码>_tid<线程>.dmp(留 2 份 —— 补了对象内存段后单份约 10 MB,留 5 份反馈包就发不出去了)。原始异常为栈溢出时不再链给游戏过滤器(返回 EXCEPTION_CONTINUE_SEARCH,WER 记下真实的 C00000FD);其余异常照旧链过去,原版行为不变。③ 挂游戏自己的 MiniDumpWriteDump(2026-09-15 加,覆盖访问违例那一族):①② 只兜得住栈溢出与「我们仍占着 UEF 槽位」的未处理崩溃;访问违例这一族游戏是用帧内 __except 接住的,异常从不变成「未处理」,②永远不触发 —— 玩家 2026-09-15 的 sub_583AD0(武器抛射物生成)空指针崩溃即是此形态:游戏自己写出一份正常的 80 KB 转储,我们一份也没有,只看得出崩在哪条指令、看不出是哪个单位哪把武器。改为用 MinHook 挂 dbghelp!MiniDumpWriteDump:游戏调到它的那一刻正是它判定「这次是致命的」的一刻,且它把 EXCEPTION_POINTERS 与崩溃线程 id 当参数一并交了过来,我们借这一刻按 ① 的同一条助手线路另写一份带堆内存的。不在 VEH 里认访问违例 —— VEH 看到的是第一次异常,混着大量游戏(以及我们自己的IsBadReadPtr 页探测)会正常处理掉的 AV,见一个写一份会把磁盘和帧率一起打爆。两处实现要点:(a)detour 内立刻拷贝 EXCEPTION_RECORD 与 CONTEXT 再交给助手 —— 游戏是在 __except 处理块里调的,异常帧已展开,那两块内存随时被后续调用覆盖;不拷贝则 dbghelp 认为异常信息无效、转储里根本没有 ExceptionStream(g 用例实测到过)。(b)挂上之后把 g_write 指向 MinHook 跳板,否则助手自己写转储时会再入本 detour。挂钩失败不致命,只是访问违例那一族退回「只有游戏自带转储、没有堆内存」。④ 防护点现场转储 GuardDump(2026-09-15 加):我们在引擎的空指针缺陷处挂「拦下不崩」的防护(§3 的空 this 一族)。拦下之后玩家不崩了,代价是现场也没了 —— 而这些防护恰恰最需要核对:拦的是不是同一个东西?拦对了没有?所以每个防护点第一次命中时就地写一份带堆内存的转储,文件名另起 tl2_mikuro_guard_<日期>_<时间>_<站点>.dmp,与真崩溃的 tl2_mikuro_crash_* 分族保留、分族收包(各留 2 份,免得把真崩溃那两份挤掉)。三条硬规则:(a) 每站点只写第一次 —— 这类防护可能每帧都命中(复活音效就会在倒计时归零那几帧反复走),见一次写一份 ~10 MB 会把磁盘和帧率一起打爆;(b) 不是真异常,所以自造一份:RtlCaptureContext 取当前上下文,异常码用 0xE0004D4B(customer 位置位,不会和系统码撞),ExceptionAddress 填 _ReturnAddress() —— 转储里 .ecxr 直接指到是哪个调用点;(c) 写完释放助手占用(真崩溃那两道是一次性 latch,这道不是),否则之后真崩了就没 dump 了;真崩溃正在写时本函数直接让路返回 false,绝不跟致命路径抢。进程级实测 tools/test_crash_dump.cpp 五个用例(s 真栈溢出 / a 真 AV / o 装好后槽位被夺 / g 游戏式:帧内 __except 接住 AV 后自己调 MiniDumpWriteDump / p 防护点:写一份现场转储后再真崩),父进程读 dump 的 ExceptionStream 核对异常码与线程;p 用例同时断言「同站点第二次不写」与「防护转储 1 份 + 崩溃转储 1 份」——后者正是 (c) 的回归。crashDump(开)dbghelp 不可用 / 事件或线程建不出来 → 不装,游戏处理器照旧。
技能动画名解析器 sub_527000
(retail 0x527000 / Steam 0x528990,函数头)
detour
(MinHook,__thiscall)
hooks.cpp
Detour_ResolveModelName
缺陷机理(2026-09-13 由 6 份玩家实机 dump 证实,全为 esi=0 的 mov eax,[esi]):该函数按施法者所持武器把技能的动画基名解析成带武器后缀的动画索引,第一条即解 this 的虚表。链路 CSkill::start(skill, caster) → sub_6BED90(skill, caster) → 把 caster RTDynamicCast 成 CCharacter。只要技能定义有非空 <STRING>ANIMATION(→ CSkillProperty+0x1A8,反汇编实证:push "ANIMATION" → CDataGroup::getString → lea ecx,[esi+1A8h];共 5 个动画字段 +424/+452/+480/+508/+536),就用该结果调 sub_527000 —— 施法者若不派生自 CCharacter 即为 NULL,空 this 解虚表必崩。★元凶不是怪物:已读游戏 RTTI,CMonster 的基类表含 CCharacter(基类为 CCharacter / CBaseUnit / CRunicCore / CPositionableObject / CEditorBaseObject / iInventoryListener / iMissile),故怪物、玩家、以及 UNITTYPE=MONSTER 的常规召唤物都不会触发;旁证是原版怪物技能 Eyeblast / BASILISKGAZE / ManaStorm 均带非空 ANIMATION 且由常见原版怪物天天施放却从不崩。真正会崩的是别的非 CCharacter 单位(可破坏物 / 触发器 / 某类道具或道具型召唤物),具体是谁静态数据定不了(游戏自带 dump 为 MiniDumpNormal、不含堆内存)。修复:self==NULL 时原样返回传入的 std::string* —— 与原函数「解析不出就返回该串、不改名」的自身语义一致,该单位保留基础动画、零副作用;同时带诊断:命中时去重打印动画基名到日志([W][modelname]),靠玩家下次日志反查技能 → 再反查单位。AOB 双 build 唯一命中且落点=函数头,aob_check.py 通过。—(崩溃修复,常开)AOB 未命中 → 不装,原版行为不变。
单位音效事件播放器 sub_664CE0
(retail 0x664CE0 / Steam 0x00AE66A0,函数头)
detour
(MinHook,__thiscall,retn 0Ch)
hooks.cpp
Detour_PlayUnitSound
缺陷机理(2026-09-15 由玩家实机 dump 逐指令证实,Steam 1.25.5.6):该函数入口第三条即 mov esi,ecx; mov eax,[esi+8],自身完全不检查 this。各调用点的 this 一律取自 <owner>+0x2A4(该单位的音效库),而这个指针可以为 NULL —— 引擎自己心里有数,在部分调用点判了空(如 CBaseUnit__modifyHealth 在 0x53C183 先 cmp [edi+2A4h],0),但 51 个调用点并非处处都判。玩家那次崩在 sub_5C2530(玩家每帧 tick)+0x4B6 的复活音效:死亡 → 复活倒计时归零 → 放事件 77 → this=NULL → 崩在函数 +0x3。这是原版引擎自己的缺陷(与 MOD 无关,只是自定义职业/单位更容易出现没有音效库的情况)。修复:this==NULL 即直接返回 0(= 这次没播出声音),不碰任何游戏状态;返回 0 安全 —— 扫过全部 51 个调用点紧随其后的两条指令,没有一个读 al/eax。诊断:首次命中调 crashdump::GuardDump("sound_nullthis", _ReturnAddress()) 写一份带堆内存的现场转储(每站点只写一次,见 §3.2),之后只累加计数打日志(第 1 / 100 / 10000 次各一行)。AOB 双 build 唯一命中且落点=函数头(脚本实测)。—(崩溃修复,默认常开,无 conf 开关)AOB 未命中 → 不装,原版行为不变。
武器抛射物生成 sub_583AD0
(retail 0x583AD0 / Steam 0x00A055A0,函数头)
detour
(MinHook,__thiscall,retn 8)
hooks.cpp
Detour_SpawnWeaponProjectile
缺陷机理(两名玩家、两个 build 同一处;2026-09-15 由玩家转储 + 全静态链路逐指令坐实):崩在 +0x141 的 mov edx,[ecx],四层解引用只有第一层判了空 —— ① this->虚表[+0x110]() → 该单位的 CGenericModel*(判了:test eax,eax; jz 跳过整块)。★这一槽是按类各自重载的取值器,不是固定偏移:CCharacter 那份是 sub_555EE0(return this[+0x220]),而本函数的 this 是 CEquipment,它那份是 sub_585700(return this[+0x2E0])—— 2026-09-19 玩家转储(tid7744,仍崩在 sub_583AD0+0x141)就是踩在这上面:防护第一版硬读 self+0x220,读到的根本不是引擎用的那个对象,于是恒判「不用拦」=等于没装,装了防护的玩家照样崩。现已改为走虚调用,引擎取哪个我们就取哪个,按构造不可能再分叉;② [① + 0x5C] = Ogre::MovableObject*(该模型的 Ogre 实体,由 sub_5A2440 在 0x5A2633 直接拿它当 this 调 Ogre::MovableObject::setCastShadows 认死)—— ★没判空,崩在这;③ ②->虚表[+0x4C]() → 带 Vector3(+0x0C) 与枚举 (+0x18) 的描述符。② 为什么会空:模型初始化 sub_5A2440 调 sub_674770 建实体,后者第一道就是 PackageMgr__findFile(网格名),包里找不到即 return 0(createEntity 失败同样 return 0);返回 0 → 0x5A2617 的 jz 进入错误块(该块唯一前驱就是这条 jz),块内 Ogre::LogManager::logMessage("[Genericmodel] Error creating entity\nUnable to find file : X", 3),随后 0x5A27AA mov [esi+5Ch], 0。模型对象照建、单位照走照打,直到它做一次生成抛射物的武器攻击才闪退 —— 这是 MOD 数据问题:某个单位的网格文件没打进包。另有一条静默路径:0x5A25D6 cmp byte ptr [esi+256h],0 非零会整个跳过建实体,+0x5C 从未被赋值,而构造 sub_5A3350 里没有任何 memset,新提交的页天然为 0,故该路径同样得到 NULL 且不留日志。缺失的文件名在对象上取不到 —— 失败路径把模型的两个名字字段(+0x108 wstring / +0x140 string)都写成空串(两个常量实测均为空),所以运行时只报得出是哪个单位(MikuroUnitDisplayName),是哪个文件必须看玩家的 Ogre.log(启动器反馈包已相应加收该文件;注意它每次启动游戏被清空重写)。修复:函数头判「模型非空 且 模型的实体为空」即 return 0 = 本次不生成抛射物。返回 0 安全:原函数自己就有两处早退返回 0(!this[282] || !a3、!v16),调用方 CCharacter__strike_resolveTargets+0xB4 对返回值的用法是 test al,al; jnz —— 0 就是走正常后续;条件卡死:模型本身为空时原函数本来走 else 分支正常运行,不碰那种情况。调用约定:retn 8 + this 在 ecx = __thiscall 两个栈参(按 esp 逐条核过,调用方也确是两次 push);ebx 不是入参 —— 函数内第一次碰 ebx 是 push ebx 保存,随后 mov ebx,[esp+0xA0] 写在读之前,Hex-Rays 标的 a2@<ebx> 是假的,因此普通 __fastcall detour 安全、不需要裸汇编。诊断:首次命中①调 crashdump::GuardDump("proj_nullentity", _ReturnAddress())(见 §3.2);②就地把玩家 Ogre.log 里的 Unable to find file 行抄进我们自己的日志(ogre_log_scan.h 的 ScanFile,去重、最多 12 条、读满 16 MB 收手)。抄的理由是时效:那份日志每次启动游戏被清空重写,玩家崩完若先重开一次游戏再打包,那行就永远没了;而我们的日志有 .prev.log 轮转、能多扛一次重开,反馈包本来也在收。两个实现坑:打开必须带 FILE_SHARE_WRITE(游戏正持着写句柄,否则 ERROR_SHARING_VIOLATION);「我的文档」不能硬拼 %USERPROFILE%\Documents(OneDrive 重定向常见),走 GetProcAddress(shell32, "SHGetFolderPathW") 取当前值,也免得为这一个调用给 DLL 添链接依赖。解析单测 tools/test_ogre_log_scan.cpp:每个用例把读取块大小从 1 一路试到 32768,要求结果与块大小完全无关(分块拼行是这段最容易错的地方),并专测「文件末行没有换行」—— 崩溃现场那行恰好是最后一行时就是这个形态,第一版正是漏了它。之后只累加计数。AOB 双 build 唯一命中且落点=函数头(脚本实测;窗口内唯一跨 build 差异就是 SEH 的 push offset 那 4 字节,已通配)。—(崩溃修复,默认常开,无 conf 开关)AOB 未命中 → 不装,原版行为不变。

技能冷却词条修复说明:该模块最初于 2026-09-05 通过独立补丁器 tools/tl2_cdfix_patch.py 生成二进制补丁 Exe 供玩家测试通过,随后移植为当前动态拦截模块,底层逻辑逐字节完全等价。截至本文档首版,该模块的最终挂载接线在工作区处于就绪状态。

3.3 原版机制修正(战斗与数值逻辑矫正) ​

此类补丁旨在矫正原版底层逻辑的固有设计缺陷,属于机制行为层面的修正。默认处于启用状态,可通过 mikuro_runtime.conf 独立关闭。

目标标识拦截方式源文件 / 入口点缺陷根因与修复实现方案配置开关(默认)未命中回退
护盾吸收结算 sub_51FEB0
(CUnit::consumeAbsorbBuffers)
+ 回血/DoT 轮询 sub_53C2F0
(CUnit::tickHealthRegenAndDoT);
网络权威查询 sub_511AC0
(CUnit::isLocallyAuthoritative,只读调用)
detour ×2hooks.cpp
Detour_DotAbsorb
Detour_DotTick
缺陷机理(汇编指令级实证):原版吸收函数的第四参数并非控制「是否吸收伤害」,而是控制「是否真实扣除护盾容量池」(bCommit 标志)。在 DoT 结算路径(computeNetHealthDeltaPerSec)中,该参数被硬编码恒传为 0。由此产生的缺陷是同属性(或 ALL)护盾对 DoT 绝对无敌、护盾池却从不扣减(如工程师力场自带 DAMAGE_TYPE:ALL,在持续 30 秒内对任何属性 DoT 处于事实上的无限吸收状态,护盾值纹丝不动)。异属性护盾挡不住 DoT(如 PHYSICAL 余烬护盾对 FIRE 燃烧)不是缺陷:引擎的直接伤害路径 applyArmorSoakAndAbsorb 同样只按真实通道匹配一次,物理护盾对火球直伤也不吸收;2026-09-13 复核前本行曾把 applyDamage 中 dmgType=0 的调用误读为「直伤余量通吃物理盾」,实际那是反伤路径(活体反汇编:函数头 mov esi,ecx、该处 mov ecx,esi ⇒ this 为被打者自己;金额 = 伤害 × 效果 32「PERCENT DAMAGE REFLECTED」%,先过被打者自己的物理盾、bCommit 取攻击者的权威判定,再加效果 48「DAMAGE REFLECTION」对攻击者递归 applyDamage;效果 id 与 EFFECTSLIST 行号的对应以 26/52/122 三行核过),据此设计的「物理护盾兜底」已撤销。
修复实现:在 Detour_DotTick 中捕获并记录当前帧时间步长 $\Delta t$;在 Detour_DotAbsorb 中仅接管满足 self == 正在轮询的单位 && bCommit == 0 && dt > 0 条件的 7 次通道轮询:以伤害速率 $\times \Delta t$ 得到当前帧真实伤害 $\rightarrow$ 在对应属性通道内真实扣减护盾池 $\rightarrow$ 按 $\text{rate}' = \text{rate} - \text{已吸收量} / \Delta t_{\text{真实}}$ 写回速率语义(引擎随后以 rate$'$ × 真实 $\Delta t$ 扣血,钳后的 $\Delta t$ 只限制本帧最多扣多少护盾,不吞伤害);通道 6 为轮询终点,结算完毕后立即关闭当前帧判定窗口。联机状态下护盾池扣除严格遵循 isLocallyAuthoritative 判定,非权威客户端仅抵扣本地伤害而不变动权威护盾池。
已知系统影响边界:若游戏设置中开启了 COMBAT LOG,每帧每个通道均会输出一行结算日志;怪物自身的通用护盾(通常为 ALL 类型)亦会被 DoT 伤害正常击破;至于直接写入生命值而不经由吸收管道的生命流失效果(Effect 185, HP MOD OVER TIME),不在本拦截覆盖范围内。
2026-09-13 复核(docs/dotshield-audit-2026-09-13-evidence/,映射零售 EXE 的原生回放):① 撤销物理护盾兜底第二趟——除前提读错外,它在 bCommit=0(非权威端)时会对同一个 ALL 盾池吸两次(池子未扣,第二趟又看到满池);② 写回改为守恒式——原先 rate$'$ = 余量 / 钳后 $\Delta t$,引擎再乘真实 $\Delta t$,长帧下一段伤害凭空消失($\Delta t$=1 s、钳 0.25 s:100 伤害只剩 60,盾才吸了 10);③ 加同线程守卫与吸收/吸空计数。另:CEffect::expire → notifyRemoved 对 LINK_NAME 成环的效果会无限递归到栈溢出,这是引擎自身缺陷、任何到期路径都会触发,对三处 MOD 数据的 37954 个 DAT 扫描未发现护盾可达的环,未在运行时侧处理。
dotShield
(默认开启,关闭则原样放行原版逻辑)
三项底层符号若缺失任一,则整套拦截逻辑完全放弃安装。

3.4 MOD 兼容托底(数据透明性与多人互通) ​

此类补丁在原版数据环境下保持逐位等价,仅在特定 MOD 加载并激活非原生数据结构时提供底层行为兼容。

目标标识拦截方式源文件 / 入口点缺陷根因与修复实现方案配置开关(默认)未命中回退
CUnit::sendInventoryToPeer
(0x535940)
+ 三处 call 指令重定向:
P0: CInventoryContainer::collectItems+0x5E
(唯一一处 call isEquipable);
P1: broadcastItemPlacement
(选择消息类型分支);
P2: 同函数计算 msg+60 衣柜标志处
detour ×1
+ 改向 ×3
cosmetic_mp.cpp
Det_SendInv
Pred_NetCollect
Pred_MsgSelect
Pred_WardrobeFlag
缺陷机理:外观装备槽 MOD(如 cosmetic_slots)通常将 6 个身体护甲槽位的数据属性配置为 EQUIPABLE:false 以作为纯外观位。然而,游戏引擎底层将 EQUIPABLE 标志作为判定「是否属于有效真实装备位」的唯一基准,同时联动控制属性生效、套装计数、属性需求校验以及网络数据包序列化广播。这导致外观槽内的物品在发送端直接被网络同步逻辑剔除,其他联机客户端无法接收外观装备数据,表现为联机时角色模型呈裸体状态。
修复实现:仅在发送端放宽上述三处条件分支判定,接收端代码完全保持原样(接收端底层原生支持将道具存入 EQUIPABLE:false 的槽位,且仅在 isEquipable 为真时附加词缀属性,这与外观槽的设计语义完全一致)。放宽判据严格收敛为「目标槽位的硬编码装备位是否落在护甲六槽(索引 3 至 8)」,坚决规避使用宽泛的 != 0 判定(否则将误将索引 13 的 POINTER 光标拖拽悬空槽纳入广播,导致原版行为受损)。P0 拦截点受到严格的 TLS 线程局部存储作用域控制(仅在 sendInventoryToPeer 调用栈上下文内放宽条件,规避污染 collectItems(equippedOnly=1) 在引擎内部的其余二十余处本地调用)。
装载期安全自校验:三处修改点必须为 call 指令且跳转目标与动态扫描到的属性访问器绝对一致;两处锚点距离必须严格等于 0x164。
标记文件 mikuro_no_cosmetic_mp.txt
或环境变量 MIKURO_COSMETIC_MP=0 可手动关闭
七项特征签名缺失任一,或自校验未通过,则不写入任何补丁字节;Hook 挂载失败则整体撤销。
父 MOD 数量上限门 sub_7DDD00
(mov eax,0Ah 的立即数,SIG_MODCAP 命中 +1)
单字节 poke
(无 detour)
hooks.cpp
ApplyModCapEarly(由 DllMain 同步调用)
用途:把引擎 10 个父 MOD 的上限抬到 100。上限只在 CGameData__ctor_loadAllData_STAGE(+0x45B)→ sub_7DEA10(读 MODSCHEME / restart.txt)→ sub_7DDD00 这一处、整个进程只查一次,超出的 MOD 只在 Ogre.log 留一行 Unable to activate mod because there are already 10 parent mods active. 后被跳过。
分工:仅 1.26.x(XD 加壳版) 由这里打 —— SteamStub 壳让启动器挂起那刻 .text 仍是密文、扫不到;其余版本(1.25.9.5 / 1.25.5.6)仍由启动器 ExePatch 在挂起启动时写入,不设环境变量,这里不动。
时序(2026-09-23 修正):首版放在 InitThread 后段,实测约 +6 s 才落地,而数据加载线程 +0.19 s 已在跑,检查早已做完 —— 等于没改,日志却报成功。现挪进 DllMain、早于 CreateThread(InitThread):此刻主线程在加载 D3D9 插件,数据加载线程尚未创建,顺序确定。loader lock 下只做读内存 + VirtualProtect + 写一字节,不加载模块、不等线程;代价一次全镜像扫描(约 0.1 s)。立即数已是 0x64 时视为已打过、不重复写。1.26.0.1 实机已验(2026-09-23):12 个 MOD 全部加载,poke 在 +0.008 s 由主线程落地,扫描约 3 ms,Ogre.log 无拒载。
环境变量 MIKURO_ALLOW_OVER10=1(启动器勾「允许 10+ MOD」且为 1.26 时设置)未命中 / 立即数不是 0x0A → 不改,记 [W][modcap],上限保持 10。

3.5 启动性能优化补丁(算法与内存流重构) ​

此类补丁始终保持开启,不提供运行时开关。其核心原则为「严格复刻原函数的全部逻辑分支与对外语义,仅对耗时瓶颈算法进行重构或剥离汇编级死代码」。基准数据取自加载 9 款主流 MOD 的 VFS 探针实机采样。

目标标识拦截方式源文件 / 入口点优化机理与算法实现实测吞吐收益失败回退策略
⛔ 已停用(2026-09-23 撤下,编译期硬关)
PackageMgr::findFile
目录内条目线性遍历 sub_68F630
detouropt_findfile.cpp
Detour_FindFile
优化机理:原版在检索 VFS 虚拟文件系统时,对目标目录内的每一个项循环执行 std::operator== 字符串比较,平均单次查找需全量遍历 1,275 个条目,耗时占整个函数执行时间的 95%。
算法实现:重构循环体检索策略:仅当目录条目数超过 256 时,自适应构建基于 (hash, original_index) 的排序二分检索索引,检索命中后再精确复核文件名与标志位,严格维持「返回首个匹配项」的原版语义;当桶内条目数发生变动时自适应重构索引。针对目录缺失的冷路径依然回退调用原函数。辅助函数地址均直接从 findFile 函数体内的内部 call 相对偏移反解(规避独立签名因绝对重定位地址差异在 Steam 数字版上失效的问题)。
检索耗时:
1,173 ms → 88 ms
签名或内部 call 寻址失败则拒绝启用;Hook 冲突(MH_ERROR_ALREADY_CREATED)或索引分配异常时置入中毒标志,自动回退至原版线性查找。
装索引前须确认没有绕过 detour 的在途 mountIndex(seh_frame_probe.h 逐线程走 SEH 链);有线程查不了则 5 s 超时后放弃装索引、回退原函数。取 TEB 先用 GetThreadSelectorEntry,失败时改用 NtQueryInformationThread(ThreadBasicInformation),两条路都须过 NT_TIB.Self 校验。
unittype 体系全量扫描 sub_65DC90detouropt_unitscan.cpp
Detour_UnitScan
优化机理:原版在中序遍历两层红黑树时,对遍历到的每一个节点均盲目执行一次 std::wstring 拷贝构造与析构。汇编级审查证实该临时变量自始至终未被后续指令读取,属于完全的死代码;由于单位类型字符串长度普遍 $\ge 8$ 字符,超出了 VC9 STL 的 SSO(小字符串优化)7 字节容量上限,每次拷贝均在堆上触发一次昂贵的内存分配与释放。
算法实现:逐指令级复刻原函数遍历逻辑,精准剔除两处无用拷贝构造与析构指令。该优化不仅作用于启动期,亦加速运行期的 Tooltip 动态构建与掉落生成。
扫描耗时:
726 ms → 49 ms;
数据加载总耗时:
3,874 ms → 3,296 ms
同上。
DAT 属性名哈希计算 sub_4C9FE0—opt_dathash.cpp
(代码保留,未接入)
实现位级等价的快速哈希算法,通过了 25/25 项静态自检与 16,384 次影子比对验证。
实测结论:吞吐收益为 0(1,349 ms → 1,349 ms)。原因在于原方案高估了 MSVCP90 中 length() 与 operator[] 的调用开销,哈希计算在整体属性读取耗时中占比不足 4%,属于非瓶颈路径。源码保留作为算法参考。
收益为 0—

影子验证机制的移除说明:两项优化补丁在早期版本中均集成了运行期影子验证(在过渡窗口内同时执行新旧路径并比对返回指针)。在历经数百次启动测试保持零失配后,为消除冗余开销已于 2026-08-17 彻底移除影子分支,单次启动进一步节省约 38 ms。其维护代价在于:运行期不再提供自动容错交叉比对,后续针对新版游戏执行适配时,必须严格依照 AOB 审计流程核对特征命中、内部 Call 偏移及结构体位移。

3.6 掉落生命周期过滤(道具生成与序列化控制) ​

该模块为 Runtime 最早落地的核心系统(2026-06-25 首版实现),用于在游戏底层拦截垃圾道具的实例化与持久化。

目标标识拦截方式源文件 / 入口点功能实现与过滤管线配置开关(默认)未命中回退
掉落物统一落地入口 sub_5EAA80
(CLevel 放置实体,全场景掉落汇聚点)
detourhooks.cpp
Detour_Place
实现机理:在道具实体被加入关卡前读取活跃 CItem 内存指针(偏移 +0x110 道具等级、+0x1AC 单位类型 ID、+0x1A0 GUID 标识符)。判定符合过滤规则时照常放行原函数完成落地,随后把 (关卡, 单位) 入队,由下一帧 EndScene 主线程回调 MikuroLootTickDeferred 调用引擎自己的 CLevel::removeAndDestroyUnit(sub_5EACA0,IDB 旧名 CMonster_despawnCorpse;零售 0x5EACA0 / Steam 0x5EC500,SIG_LEVEL_REMOVEUNIT 双 build 唯一命中)拆除:从关卡三条单位链表摘除 → vt[0](0) 析构 → 分配器释放(或 owner->vt[268])。
为何不再直接阻断(2026-09-12):placeUnit 的 16 个调用方中有的在返回后继续使用该指针(如 sub_6FE680 先 RTDynamicCast 再虚调用),而被否决的 CItem 在我们读到的调用链上没有任何释放路径(spawnUnit 那条「虚析构 + 还给分配器」的失败分支属于放置前的空间检查,不看 placeUnit 的返回值)——按此推断每个被过滤的掉落泄漏一个 CItem 及其 CEffect 等子对象。这是源码层推断,所有权链尚未完整追踪(2026-09-13 复核限定),不作为玩家崩溃根因的定论。拆除前置条件(2026-09-13 复核第 2 条后加严:vftable 只认类型不认实例,同类对象在同一地址重建并进入同一链表会被误认):入队年龄 ≤ 2 s(EndScene 没来得及跑的——过图——宁可漏过滤);关卡与单位的 vftable 与入队时一致;单位 64 位实例 id(unit+8/+12,引擎网络层以它指名单位:CInventory__broadcastItemPlacement 发 item[2]/item[3]、NetUnitfxParticleMsg 填 *(unit+8)/*(unit+12),对端要靠它对到实例;由根基类 CEditorBaseObject 构造函数调 EditorMakeGuid() 现生成,每个对象一枚)与入队时一致;单位自己认的关卡(CUnit::getLevel = *(*(unit+0x1C4)+8),+0x1C4 由 CBaseUnit 构造函数写入 UnitMgr 指针)仍是入队时那个;单位仍挂在关卡 +0x98 物品链表上(removeAndDestroyUnit 对不在链表上的单位照样析构释放,故必须先查)。id 与 getLevel 两条布局假设在入队时自检,不成立则该件不入队、留在地上(同日二次复核 F2:没有实例身份就去拆,等于回到"同地址新对象也拆"的老路;漏过滤是可接受的降级,错拆不是),并记一次日志与计数;队列为 256 项环形缓冲,溢出只记日志不拆;removeAndDestroyUnit 签名未命中则真过滤降级为只记录。代价:被过滤的掉落可能存在 ≤ 1 帧(可能有落地音效或一闪)。
实体合法性三级过滤门:
① RTTI 类型安全校验:确保指针满足 is-a CItem;
② 单位类型预置表校验:检查是否命中硬编码内置道具表(140 条)或排除非道具表(158 条);
③ 动态类型回退:对于 MOD 动态注册的未知类型,动态查询引擎内部的 unittype 注册表二叉树,严格确认满足「引擎层级继承自 ITEM」且排除 CBreakable(可破坏物)与 CTriggerUnit(环境触发器),防止等级过滤误删宝箱或机关导致场景缺损。
名称与本地化解析:根据 GUID 调用 sub_65FDE0 获取持久化定义节点,调用 sub_677F00 提取 DISPLAYNAME(落地点生成的临时定义节点数据不完整,NAME 字段在落地后方被填入);中文名称直接从内存中的 CStringTranslate 键值树读取,真实反映 MOD 加载覆盖优先级。
规则判定优先级:道具名称白名单 > 道具名称黑名单 > 单位类型白名单 > 单位类型黑名单 > 稀有度阈值 > 需求等级下限;QUEST* 任务道具对除 blockQuest 之外的所有过滤规则享有绝对豁免权。
enabled(默认开启)
logOnly(默认开启:仅记录日志不实际拦截,供观察验证)
具体黑白名单与等级见 §4.1
核心总闸:若该函数特征未命中,整个 Runtime 后续所有核心 Hook 均放弃挂载。
关卡存盘逐实体序列化 sub_5FF570
与其孪生函数 sub_5FF630
(仅被关卡状态持久化 sub_5C51A0 内部调用)
detour ×2hook_b.cpp
Detour_Ser0
Detour_Ser1
实现机理(架构代号 Hook B):在关卡切换触发地图数据序列化时,针对地面真实掉落物(满足 is-a CItem 且严格排除 CTriggerUnit 与 CBreakable)直接跳过其存盘序列化流程。当玩家重新返回该关卡时,地面残留掉落已被自然清除。该逻辑仅跳过存盘编码,不释放活跃对象,内存开销为零。
边界防护:早期误在角色存档函数 sub_435E10 挂载的实现会导致玩家装备丢失,已彻底纠正;2026-07-12 补全了触发器与可破坏物排除逻辑,杜绝了此前因任务门开启状态未被持久化导致回图时任务门重新锁死的缺陷。
clearOnTransition
(默认关闭)
签名未中或三项 RTTI 描述符缺失任一,则强制禁用该模块(杜绝误锁任务门)。
掉落决议探针 sub_6FCA10
(只读调用点探针)
—hooks.cpp
Detour_LootResolve
作为掉落生成链路验证探针开发,用于统计各 TREASURE_MONSTERLOOT_* 掉落表的命中频次,验证按名称检索道具原型的可行性。编译期宏控制:
MIKURO_LOOT_HOOK_ENABLED=0
(2026-07-25 起整条技术路线搁置,转为优先分析原版数据配置)
—

3.7 死槽 131 自定义效果体系(属性多路复用与突破) ​

原版引擎 EFFECTSLIST.DAT 中的第 131 项 FUTURISTICNESS 在原版所有基础数据和词缀中均未被引用。Runtime 将该未分配槽位作为多路复用信道:利用 VALUE3(结构体偏移 CEffect+0xE4)作为 SUBTYPE 子功能选择器,在单一 Effect 类型下派生扩展出数十种自定义战斗与机制效果。

数值提取路径统一标准化为:遍历角色装备栏本体及其镶嵌灵石的内部效果管理器(CItem+0x1B4),累加所有满足 type == 131 && VALUE3 == 目标子类型 的 VALUE(偏移 +0xD4)数值。统计范围严格限定于主装备槽及所有挂载有 EQUIPABLE 标志的子容器,排除行囊与共享储物箱,基于 CItem 指针执行去重计算,并维护 500 ms 的瞬态缓存。

SUBTYPE 子功能号段分配表(新增或调整效果必须首先在此分配):

子类型号功能语义挂载载体消费与生效模块
1暴击吸血百分比(%)装备本体 / 灵石Detour_ModHP + Detour_RollCrit
2宠物嘲讽光环半径(米)宠物项圈Detour_SkillAct → PetTauntFor
3技能百分比冷却缩减(乘法叠乘)灵石Detour_SkillAct;镶嵌孔位规则限制
4伤害减免上限突破(+)灵石Detour_DmgCap / Detour_DmgCapPanel
5暴击伤害倍率上限突破(+)灵石Detour_CritCap
6闪避几率上限突破(+)灵石Detour_DodgeCap
7格挡几率上限突破(+)灵石Detour_BlockCap;镶嵌孔位规则限制
8保留空闲——
9消耗品标记:卷轴「鬼工·乱铸」消耗品curse.cpp
10消耗品标记:卷轴「天工·强化」(VALUE5 > 0 标识启迪模式)消耗品curse.cpp
11装备词缀账本:诅咒记录(VALUE4 已用次数 / VALUE5 上限)装备本体curse.cpp
12装备词缀账本:强化记录装备本体curse.cpp
13保留空缺(原计划开发「归真」以撤销诅咒,已作废:确立诅咒不可逆设计原则)——
14消耗品标记:卷轴「天工·返璞」(移除强化)—— 仅占位,尚未实现底层逻辑,仅含 Tooltip 文本预置消耗品—
15 ~ 19空闲(2026-10-01 全仓库查过,没有代码读这几个号)——
20天尊请神暗场标记(青莲术士 1_7 请神的 4 秒计时词缀,VALUE4 = 阶位)玩家身上tianzun_sky.cpp(只读;该模块本文尚未收录)
21抵命符标记(护身符 MIKURO_DIMING_AMULET 的词缀 MIKURO_DIMING_AMULET_AFF)护身符本体deathward.cpp;Detour_BuildTip 出触发条件文案

历史机制沿革说明:在 2026-07-16 架构重构前,冷却缩减原由 SOCKS(Type 133)承载,目前已全量收拢至 131/3;源码中遗留的 GemSocksValue 等函数命名属于历史兼容符号,实际判定语义均以当前代码为准。

死槽 131 拦截与消费实现清单:

目标标识拦截方式源文件 / 入口点功能实现与运行逻辑配置开关未命中回退
主动技能激活 sub_6C39E0
(CSkillActivation::Activate;qlss_yin.cpp / skill_trace.cpp 里称 CSkill::start,同一函数)
detourhooks.cpp
Detour_SkillAct
开头先转调九老仙都印(2026-10-01 加):调原函数之前先调 qlssyin::PreStart(self, caster, a6 = targetPos, a7 = target),让青莲术士 2_10 在原函数派发 EVENT_START 之前写好「这一招盖过印」。本 Detour 比 qlss_yin 先装,函数头已是跳转,qlss_yin 自己的签名落空,MinHook 也不能对同一地址挂第二次,只能由这里转调(详见 §3.13 九老仙都印行)。
① 动态冷却缩减:原版将本次计算所得的冷却时间写入结构体 this+0xF0。Detour 拦截后,提取角色所穿戴灵石的乘法因子 $\prod(1 - v/100)$ 进行乘积缩减,硬编码保底时间下限为 0.1 秒(杜绝除零或无限连发),不设游戏性数值硬顶;技能就绪状态检测与 UI 技能栏转圈冷却动画均直接读取该字段,表现高度自洽。仅当施法者 RTTI 为 CCharacter 时生效。
② 宠物嘲讽指令:若技能名称包含「御兽」或标识符 MIKURO_AZURE_CHAOFENG,则检索施法者(支持宠物自主施法或继承旧档机制)周围 10 米范围内存活且具备有效 AI 的敌方目标,调用 setTarget(mon, pet) 改写仇恨目标;宠物必须佩戴 131/2 装备方被视作合法嘲讽主体;自动排除玩家及友方召唤单位。2026-09-15 起不再下发 CAIManager::setFlag(5, 4.0s)(NEVER CHANGE TARGET) —— 该标志会使 CCharacter::setTarget 变为空操作(闸门:当前目标仍存活即拒绝改写),进而让怪物 AI 的「验证目标」与「持有目标」永久错开:待机态(unit+0x314=2)经 AIIdle_dispatchByAIType→AIIdle_NORMAL_pickTargetAndSkill 选出新目标 v8、以 v8 验证技能通过→setTarget(v8) 被拒→状态置 13→虚表 +0x208 立即重入 updateAI→状态 13 分支(虚表 +0x25C = sub_546840)改用当前目标复验技能失败→状态设回 2 并再次重入,该重入路径既无深度限制亦无进展保证,构成无界自递归。玩家转储实证:递归 1246 层、1 MB 线程栈耗尽、0xC00000FD;崩溃单位七个 AI 标志位中仅标志位 5 置位(余 2.658s / 4.0s),其当前目标为玩家所属宠物、自身 owner 为空(野怪),与日志中崩溃前 2.0 秒的嘲讽施放完全吻合。替代方案:仅下发 setTarget,4 秒窗口内由 MikuroTauntTick(overlay.cpp 的 EndScene 每帧驱动、节流 200ms)重走施放路径把目标按回宠物。setTarget 本身无时长(纯赋值写 unit+0x320 弱引用),重设同一目标不重复抛逻辑事件(内部有「目标确实变更」判定),开销可忽略;刷新不跨帧持有任何裸指针(每次从单例重新走玩家→戴项圈宠物→关卡单位链表),故无悬垂指针风险。两点有意接受的差异:刷新以本地玩家为锚点,故多只项圈宠物在窗口内均会拉仇恨;刷新亦会拉入窗口期间新进入 10 米的怪物,等效由一次性变为 4 秒光环。窗口状态用 32 位 GetTickCount + 带符号差值比较(x86 上 DWORD 单次读写原子、且回绕安全)。底层复用 setTarget(sub_524AF0)、getPosition(sub_61DCE0)及全局单例获取接口 sub_41CE60;setFlag(sub_701590)仍解析但不再调用,作为「该 build 是否认得 CAIManager 布局」的探针保留。原有的 T 键物理快捷键因绕过技能消耗与冷却已于 2026-08-08 彻底移除。
数据侧驱动(取决于装备是否存在对应效果)特征未命中则技能冷却与嘲讽效果维持原版行为;qlss_yin 的签名(本行签名的前缀)若唯一命中,九老仙都印改由它自己挂,见 §3.13。
暴击判定入口 sub_52AC60
(rollCritical)
detourhooks.cpp
Detour_RollCrit
记录「本次命中是否触发暴击」与「当前攻击发起者指针」两组全局上下文变量,为后续吸血计算提供原子化门控。原函数负责弹出 Critical 战斗跳字、分发联机网络数据包及播放暴击光效,在 applyDamage 实际扣血前被调用。—特征未命中则暴击吸血降级为无门控的常规普攻吸血。
实体生命值变更 sub_53BC80
(modifyHealth)
detourhooks.cpp
Detour_ModHP
优先放行原版扣血逻辑;若检测到满足 delta < 0 && a5 == 刚判定暴击的攻击者 && 攻击者装备包含 131/1,则计算 $\lvert delta \rvert \times \text{pct}%$,重新调用原函数对攻击者实施生命恢复(内置递归重入防护锁),并当场消费复位暴击标志(防止同攻击者后续的多段非暴击 DoT Tick 误触发吸血)。
别的模块也搭这个 Detour:原函数返回后、吸血之前,依次交给战斗统计(§3.9)、抵命符 OnHealthDepleted(这一下把血从正数打到 ≤ 0 时,见下方抵命符行)、曲则全 qlssquze::OnDamage、墨斗弹线 qlssmodou::OnDamage(§3.13)。曲则全排在抵命符之后,抵命符已救回的那一下不会重复补血。
—特征未命中则吸血机制整体禁用;抵命符、曲则全、神霄雷池、墨斗弹线随之不装(它们回血扣血都走这里的原函数)。
减伤上限计算 sub_522AE0detour
+ 瞬态改常量
hooks.cpp
Detour_DmgCap
原版针对玩家单位将 PERCENT DAMAGE TAKEN 减伤词条的累加值硬封顶于 $-75.0%$(浮点常量 flt_2170218,比较分支与写入赋值共用该地址)。若受击玩家装备挂载有 131/4 效果,则瞬时将常量值覆写为 $-\min(75 + futu, 95)$ $\rightarrow$ 调用原版求值函数 $\rightarrow$ 立即恢复原始常量。硬编码 95% 上限以防免伤无敌;怪物单位继续走原版基准路径。数据侧驱动特征未命中或常量寻址异常(提取值非负)则禁用上限突破。
属性面板减伤数值获取 sub_51AD60detour
+ 瞬态改常量
+ 瞬态改立即数
hooks.cpp
Detour_DmgCapPanel
独立拦截动因:原版角色面板所显示的减伤比例经由独立的 Getter 函数计算,当检测到数值 $v < -75.0$ 时,直接执行 return 75(该 75 在机器码中体现为硬编码立即数 mov edi, -75)。为促使属性面板如实展示 $\min(\text{真实减伤}, cap)$,必须在调用切片内对浮点常量 flt_2170218 与汇编立即数实施双重瞬态修改:若仅改浮点阈值,当实际减伤超过上限时面板依然卡死在 75;若仅改立即数,处于 75 至新上限之间的过渡区间会被错误放大拔高。已知显示特性:面板仅支持整型渲染,当上限为 77.5% 时向下取整显示为 77%。数据侧驱动减伤常量未解析则跳过;特征或立即数寻址失败仅禁用面板显示同步,不影响实际战斗承伤计算。
暴击伤害倍率获取 sub_521020detour
+ 瞬态改常量
hooks.cpp
Detour_CritCap
原版将暴击伤害倍率硬截断于 $[0, 500]$ 区间(浮点常量 flt_2141E00)。攻击方玩家若装备 131/5 效果,则将上限动态调整为 $500 + futu$,且不设硬性天花板(标准灵石每颗提供 +20%)。由于暴击倍率与闪避率的 Getter 函数序言机器码高度同构,旧版特征码因直接绑定绝对内存地址导致在 ASLR 下从未成功装载(2026-07-22 修复):现采用统一特征码 ScanAll 扫描,根据入栈操作数所指向的宽字符串字面量 CRIT_BONUS 与 DODGE_CHANCE 实现精准分流。数据侧驱动入口未命中或常量非正数则禁用扩展。
闪避几率计算 sub_520F10detour
+ 瞬态改常量
hooks.cpp
Detour_DodgeCap
原版玩家闪避率被限制为 $\min(dodge, 75)$(浮点常量 flt_2170884,此常量在引擎内部被三处逻辑跨系统共享,因此必须严格采用局部瞬态修改)。若装备 131/6 效果,上限提升至 $\min(75 + futu, 95)$(每颗标准灵石提供 +2.5%)。闪避判定在引擎底层仅对近战武器攻击生效。数据侧驱动同上。
格挡几率计算 sub_524090
(getBlockChance)
detour
+ 瞬态改常量
hooks.cpp
Detour_BlockCap
复用上述 flt_2170884 共享常量;装备 131/7 效果时,上限提升至 $\min(75 + futu, \mathbf{85})$。格挡上限之所以严于闪避,是因为格挡在近战攻击与通用伤害管线中均会触发投骰,对技能、投射物、范围伤害(AOE)、持续伤害(DoT)及反弹伤害均具有全额格挡效果。
汇编调用约定特殊性:该函数返回整型(三个返回出口均跳转至 jmp __ftol2_sse),且无栈参数直接以 retn 0 退出——若误按其他浮点 Getter 编写 retn 4 桩函数将导致堆栈指针破坏与返回值错乱。
数据侧驱动同上。
宝石镶嵌合法性校验虚方法 sub_7CBCC0
(高亮显示与落孔共用)
detourhooks.cpp
Detour_CanSocket
原函数在校验时仅检查 SOCKETABLE、空插孔及固定大类,底层完全不解析词缀数据中的 NOT_UNITTYPES 规则(实机断言证实),导致在数据层对宝石限制镶嵌部位无效。疾速灵石(131/3)限制为:武器(8)、头盔(12)、护手(23)与盾牌(21),每件装备限镶 $\le 2$ 颗(双手武器 10 判定为可镶 4 颗);格挡灵石(131/7)限制为相同四类,不设颗数上限(85% 绝对上限构成天然天花板)。判定对象依据宝石自身的 EffectMgr 是否挂载对应 SUBTYPE,免去硬编码 GUID 清单。部位判定调用 sub_511FA0 IsUnittype。数据侧驱动特征未命中则限位失效(回退为全部位自由镶嵌)。
拖拽落孔放置动作 sub_7CCC50
(虚表槽位 13)
detourhooks.cpp
Detour_SocketDrop
必须在此虚函数入口点直接截断并执行 return 0:原版内部实现为先执行插入逻辑,紧接着无条件从源物品中销毁移除源宝石;若在更底层的子流程进行拦截,会导致「宝石插入被阻断,但源宝石依然被强制扣除销毁」的吞宝石致命缺陷(实机测试曾两次踩坑坐实)。数据侧驱动特征未命中则落孔拦截失效,仅保留高亮提示。
EFFECTSLIST 数据加载器 sub_401000detourhooks.cpp
Detour_EffLoad
原加载器负责将 EffectsList.dat 结构反序列化为 SoA(结构体数组)全局内存表(每个字段一张数组、共 239 项、每项容纳 0x1C 字节的 std::wstring)。Detour 在原版加载完毕后,直接调用游戏 CRT 的 wstring 接口,在内存中动态修改 FUTURISTICNESS 对应的 NAME、GOODDES、GOODDESOT、BADDES、BADDESOT 及 DISPLAYPRECISION 字段为自定义说明文案——彻底脱离了对全局 EFFECTSLIST.DAT 物理文件的覆盖式分发依赖(杜绝了多 MOD 冲突覆盖)。7 组内部数组基址通过解析加载器函数体内的 lea ecx, [edi+ARR]、mov [esi*4+ARR] 及 fstp [esi*4+ARR] 指令操作数动态提取,取代早期硬编码 RVA。安装若滞后于数据加载,则在挂载瞬间强制触发一次覆写。—特征未命中则效果描述文本维持未汉化原状(回退依赖外部物理文件补丁)。
词缀 Tooltip 文本生成 sub_7A70B0
(CEffect::buildTooltipDescription)
detourhooks.cpp
Detour_BuildTip
引擎对每种 Effect 类型在全局描述表中仅分配一条模板,而 131 承载了多套子效果。原函数构造好基础描述后,若检测到 type == 131,根据 VALUE3 调用 SetGameWStr 覆写输出内容:1 至 7 号子类型各自映射为独立的动态数值描述;9、10、14 号卷轴覆盖为其专属说明(消耗品道具在引擎中确实会强行渲染其内部携带的 Effect 描述行,2026-08-16 实机截图确认,若不实施拦截会荒谬地展示出「暴击时吸取 0% 伤害为生命」的死词缀);11 与 12 号账本读取 VALUE4/VALUE5 格式化为「已用/上限」进度;21 号抵命符只出一行触发条件「受到致命伤害时碎裂,替你挡下这一击」(盾量、时长写在物品 DESCRIPTION 和护盾技能 DAT 里);其余非法子类统一输出「Mikuro 自定义效果 #n」兜底。—特征未命中则回退至引擎全局模板文本。
角色 / 怪物死亡 sub_54E3F0
(虚表第 120 格 +0x1E0,CCharacter 与 CMonster 共用;die(killer, pos*, float, 5×char),retn 20h)
+ 搭 modifyHealth sub_53BC80 的既有 Detour(本表 Detour_ModHP)
调用 CBaseUnit::getOrCreateSkill 0x5138F0、CSkillMgr::setSkillLevel 0x6D78F0、CSkillList::executeSkill 0x6D6FA0、isLocallyAuthoritative
detour ×1
+ 搭车
deathward.cpp
Detour_Die / OnHealthDepleted / Tick / DrawOverlay;
回血借 hooks.cpp DeathwardHealFull;
对外导出 CastPlayerSkill / CastPlayerSkillAt / CastPlayerSkillAtPos
抵命符(131/21,2026-09-28):本地玩家受到致命伤害时碎一枚,这一击不落下、生命回满、15 秒 300 万护盾。数据侧在护身符组件:物品 MIKURO_DIMING_AMULET 带一条 (131, VALUE3 = 21);护盾技能 MIKURO_DIMING_SHATTER(隐藏、TARGET SELF,EVENT_START 挂 SHIELD BUFFER 3000000 / 15 秒)。盾量和时长只在 DAT 里,改了不用重编 runtime。
认符:遍历玩家主装备槽和所有 EQUIPABLE 子容器(+0x33 == 1,护身符栏属于这一类;布局同 EquippedCdFactor),物品自己的 CEffectMgr(CItem+0x1B4,三条链表在 +0x18 / +0x28 / +0x38)里有 type 131、VALUE3(+0xE4)== 21、且 +0x28 没有 0x20(已失效)位的效果才算。背包、仓库里的不算,镶在装备上的宝石也不看。
主路:modifyHealth 之后(IDA 实证,零售 1.25.9.5)。modifyHealth 自己不判死,HP 扣到 0 以下就写 0 返回,死亡由调用方看 HP ≤ 0 再调虚表第 120 格(applyDamage 0x53DD46、DoT 的 tickHealthRegenAndDoT 0x53C7B4 都是这样)。所以 Detour_ModHP 在原函数前后各读一次血量,看到「这一下把血从正数打到 ≤ 0」就调 OnHealthDepleted,当场回满,调用方就看不到 HP ≤ 0,死亡流程不会启动。判定看原函数跑完的结果而不是预测 delta:原函数里有七八道闸(非权威端、IGNORES_PLAYER_DAMAGE、+0x6AB、+0x6AA 的 POISON 保底 1 点……),预测一定漏。
兜底:Detour_Die。KILL 效果(executeInstantEffect 0x55A61A)、tickActiveEffects 0x566FBB 等直接调 die,不经过 modifyHealth。本地玩家、状态 +0x314 不是 5 / 6(已死)、本地权威、身上有符 → 原 die 一行都不跑(掉落、尸体、死亡界面都不会有)。其他单位原样放行。
回满血:DeathwardHealFull 直接调 modifyHealth 原函数(不再进 Detour),给一个远超血池的值让原函数自己按上限截;调之前 HP 若已是 0 先写成 1 —— 原函数对「HP ≤ 0 且回血量 ≥ 上限」走复活分支(清 +0x5A0、sub_535570 把动作切回站立 / 复活状态 50 / 87 / 91),玩家其实没死过,切动作只会打断正在出的招;a4 = 0 不飘回血数字。
施法和删物品推到下一帧:modifyHealth 可能是在遍历玩家效果表的调用栈里被调的(tickActiveEffects → 伤害效果 → applyDamage → modifyHealth),当场加护盾、当场摘护身符都可能让上层手里的迭代器失效。回血只改 +0x560 一个 float,当场做;Tick(主后备缓冲的 EndScene)里放 MIKURO_DIMING_SHATTER,再核对那枚符还在装备容器里、64 位实例 id(+0x08)没变,才经 enchant_transfer::DestroyPlayerItem 销毁。待办超过 5 秒还没执行(不在局内)就作废,符不删。
宽限:触发后 1.5 秒内、施法后至少再 0.5 秒内,以及护盾 / 销毁待办还没跑完时(EndScene 停摆不能只靠时间窗),再吃致命伤害只回血、不碎第二枚。
施放接口:照抄引擎装触发施法的做法(registerCastSkillEffect 0x514AB0 → execProcCastSkill 0x53CB92;陷阱施法 0x5C8AD1 也是这一串):getOrCreateSkill(unit, 名字, forceNew = 0)(第二次起复用同一个技能实例)→ setSkillLevel(unit+0x1BC, skill, level) → executeSkill(mgr, skill, 施法者, 0 = 不查法力, 自己的位置, (*(unit+0x58))->vt[7](), 目标点, 目标, 0, 0)。技能名用 MSVCP90.dll 导出的 std::wstring 构造 / 析构现建现拆(§1 第 5 条)。只能在主线程、调用栈干净时调;技能不存在或技能管理器取不到时返回空并记一行日志。§3.13 的青莲术士模块施放子技能全走这三个函数。
提示:碎符那一刻屏幕上方出金色「抵命符碎裂」,2.5 秒淡出(overlay.cpp 的 ImGui 帧内调 DrawOverlay)。
联机:只认本地权威的本地玩家;销毁物品不发 NetDeleteItemMsg(与附魔迁移同一限制),所以只在单机 / 房主上用。
四条签名(die / getOrCreateSkill / executeSkill / setSkillLevel)在零售 1.25.9.5、Steam 1.25.5.6、1.26.0.1 脱壳版上各自唯一命中(tools/aob_check.py --sig,2026-09-28);die 的签名是 SEH 序言加 sub esp,17Ch 帧大小,三个 build 都只有它一个。日志标签 deathward。
数据侧驱动(装上抵命符才生效;没装时 Detour_Die 对本地玩家只多一次遍历装备)四条签名、isLocallyAuthoritative、MSVCP90 的 wstring 构造 / 析构任一缺,modifyHealth 的 Detour 没建起来(回血回调为空),或 die 挂不上 → 整块禁用,记 [W]:抵命符只是一枚没用的护身符,三个施放接口全返回空(§3.13 各模块放不出子技能)。附魔迁移模块没装上时符照样救命,只是碎掉的那枚删不掉(记 [W])。

3.8 卷轴与附魔渠道扩展(自定义打造管道) ​

该系统在数据侧依托 tl2-mikuro-runtime-mod(定义新增打造 NPC、自制 .LAYOUT 交互界面与卷轴物品),在引擎侧由本模块提供拦截支持。原生逆向工程报告沉淀于该仓库的 docs/ 目录。

目标标识拦截方式源文件 / 入口点功能实现与运行逻辑配置开关未命中回退
动态效果施加入口 sub_583080
(CEquipment::ApplyDynamicEffect,右键卷轴点选目标装备触发;装备鉴定、祛魔、附魔均由此虚方法分发)
detourcurse.cpp
Detour_ApplyDynEffect
判定施加的 Effect 是否为 131/9 或 131/10;若非本系统效果则无损放行原版逻辑。
乱铸机制(子类型 9):遍历目标装备的三个 Effect 链表,跳过 Type 131 及 238(引擎内置的「作弊装备」标记),将普通词缀的 VALUE、VALUE1、VALUE2 按 $[0.7, 1.3]$ 浮点随机数执行缩放,基础攻击、防御与伤害加成同步浮动 $\pm 30%$;已锻造次数记录在装备自身的账本 Effect(子类型 11)中随存档持久化,默认上限 3 次(支持数据驱动,优先采纳账本的 VALUE5);达到上限时返回 0 $\rightarrow$ 原版判定为施加失败 $\rightarrow$ 不扣除消耗品卷轴,弹出橙色失败警告提示。若装备尚未建立账本,有意调用一次原函数利用其 default 分支将卷轴内部的 Effect 深度拷贝至装备上,随后就地将其属性重写转化为正规账本。
强化机制(子类型 10):若 VALUE5 > 0 判定为「启迪」模式(激活隐藏潜能,默认策略取大值),若为 0 则作为常规强化处理,附加带有 MIKURO_ 前缀的新增词缀(命中原版同名词缀时遵循同名顶替规则)。
不可撤销设计决策:底层确立「诅咒不可逆」原则(重 Roll 算法基于全局随机源且不持久化种子,二次重 Roll 必将生成另一组词缀;禁止引入「附加负面词缀」作为惩罚——Tooltip 合并机制仅比对 Type 而忽略名称,会导致负面词缀与原有词缀异常合并为单行)。操作完成后调用 CEffectMgr::invalidateTooltipCache(sub_7ADD60)刷新视图。锻造结果面板采用 ImGui 自主渲染(呈现锻造前后属性对比,数值增涨显示为绿色,数值衰减显示为红色,12 秒平滑淡出),完全抛弃原生 Tooltip 渲染(原生 Tooltip 显示值经由等级曲线换算,与写入内存的真实数值存在系统性偏差)。
编译期宏控制:
CURSE_MUTATE_ENABLED=1
(置 0 则仅输出调试内存 dump,不执行数据变更)
特征未命中则模块整体禁用;invalidateTip 未命中会导致 Tooltip 延迟至下一次交互事件方才更新。
存档记录反序列化 0x591CB0
(CEquipment::loadFromSaveRecord)
detourcurse.cpp
Detour_LoadFromSaveRecord
原版存档在反序列化期间不保证 Effect 链表的原始顺序,读档后装备账本往往被置于链表末尾(导致 Tooltip 中「已诅咒 N/次」的标注行从基础属性下方下坠至描述最底端)。Detour 在原版恢复完 Effect 之后,强制执行重排序逻辑将账本 Effect 重新置顶。该函数序言包含 and esp, -64 栈 64 字节对齐指令,MinHook 能够原生兼容处理。—特征未命中则诅咒机制正常运行,仅读档后 Tooltip 提示行排版下移。
打造状态机判定 0x7429A0
(CEquipmentModifierController::updateCanCombine)
detourenchant_transfer.cpp
Detour_UpdateCanCombine
原版合成师交互界面由该控制器统一调度,菜单、槽位与容器完全依托数据驱动。Runtime 的 4 套自制 .LAYOUT 界面复用此控制器,Detour 优先比对当前菜单的标识符后执行精准分流:MIKURO_ENCHANT_TRANSFER(双槽词缀迁移)、MIKURO_ENCHANT_STRIP(单槽词缀移除)、MIKURO_BLESS(神明祝福)、MIKURO_UNBLESS(神明涤除);若不属于上述标识符则无条件回退调用原版逻辑——确保原版合成师、附魔师与宝石拆解功能不受丝毫干扰。原版依据配方匹配控制按钮高亮,本系统根据专属业务规则分发 402/403 交互事件。—8 处核心依赖地址若缺失任一,则模块整体安全禁用。
打造执行管线 0x741D70
(doCombine)
detourenchant_transfer.cpp
Detour_DoCombine
词缀迁移:严格限定仅迁移原版附魔师所附加的词缀属性(判据为 CEffect+0x28 & 0x400,即附魔师执行 markNewEffectsAsEnchanted 所打上的标志位;掉落随机词缀、DAT 基础属性及宝石镶嵌效果天然被排除)。迁移过程必须同步贯通两条底层通道:
① Effect 逻辑层:调用引擎堆拷贝构造并在目标装备上调用 linkEffect,将 +0xD4、+0xD8、+0xDC 与 +0xE0 四组浮点数值原样写回目标(针对区间型词缀,若仅执行浅拷贝会导致引擎重新随机投骰,实测同一来源经三次拷贝数值分别漂移为 5.087、5.231 与 4.788);
② 附加伤害投影层:附魔烘焙后原始 Effect 已被注销,仅在桶 B(CEquipment+0x3FC)中残留物理投影,必须通过 addBakedDamageOfType 完成数据注入。同步刷新 STAT_ENCHANTCOUNT(偏移 +0x3B4,控制 Tooltip 中「附魔: N」文本行,归零时该行自动隐藏)。若接收装备放回背包时空间不足,立即终止事务且不销毁源装备(坚决杜绝吞装备);事务提交成功后将源装备移出容器,触发析构并将内存安全归还至引擎内存池。单件装备支持迁移上限为 5 条,支持 UI 勾选。
词缀剥离:将目标 Effect 的持续时间置为 -900.0 哨兵标记,随后调用 expireEffects(精准复刻原版清除烘焙伤害词缀的标准手段);仅遍历检索 0 至 2 号链表,3 号条件触发链表无法清除(与原版祛魔逻辑一致)。
神明祝福:调用 addRandomAffixes 抽取词缀,以 NPC 自身的 UNITTYPE(MIKURO_BLESSER)作为词缀池检索索引,打上专属的 0x400000 标记位(区别于附魔师的 0x400),词缀上限为 3 条。
神明涤除:仅定向清洗携带 0x400000 标志的祝福词缀。
金币扣除:统一调用 CUnit::addGold 传入负数金额实现明码标价;若底层未解析到 addGold 接口,两大面板一律拒绝执行交易,坚决杜绝免费漏洞。
编译期宏控制:
TRANSFER_ENABLED=1
TRANSFER_DESTROY_SOURCE=1
TRANSFER_SEND_NPC_AWAY=0
(迁移大师定义为常驻城镇 NPC)
同上;非致命辅助地址缺失仅触发功能降级并在模块日志中告警。
单选复选按钮状态变更 0x7247F0
(CWidgetToggleButton::setToggled)
detourenchant_transfer.cpp
Detour_SetToggled
当玩家在界面上勾选或取消迁移词缀条目时,必须实时重新计算并刷新「当前选定迁移数量」。此函数为引擎全局 Hook(游戏内所有 Toggle Button 事件均由此派发),Detour 入口首要操作即校验按钮所属指针,若非本系统控件则以极低开销瞬间返回。状态读取直接解引用结构体 +0x144,规避多余函数调用。—挂载失败则勾选功能依然可用,但状态统计行数字无法实现实时刷新。
Tooltip 主显示区文本生成 0x7B0720
(CEffectMgr 内部实现)
detourenchant_transfer.cpp
Detour_MainTipText
原版 Tooltip 主区过滤条件为 !(flags & 0x400),附魔专区过滤条件为 flags & 0x400。由于「MIKURO 的祝福」未标记 0x400,默认会落入主显示区并与常规基础词缀混杂在一起。Detour 拦截后提取带有 0x400000 标记的祝福词缀,在排版中单独剥离为专属的墨绿色文本块。底层调用 AppendGameWStr 直接在引擎已构建好的 std::wstring 尾部进行格式化追加。—特征未命中则祝福词缀渲染为普通白字,功能生效不受影响。
NPC 对话强制终结 0x7062F0
(CGame::endNPCConversation,参数 force=1)
只读调用点
(每帧轮询)
enchant_transfer.cpp
TickEndConversation
当自制打造菜单关闭后,由于引擎内部「已打开菜单链表中若存在任一未关闭菜单(sub_7A0CA0 为真)则阻止对话退出」的判定持续为真(诱因尚未完全确定,已排除的假设记录于相关文档),导致游戏摄像机镜头永久卡死在近景拉近特写视角。本模块在检测到自制面板已全量关闭但 NPC 对话状态依然挂起时,直接绕过前置闸门调用此底层接口完成摄像机视角的安全复位。全局单例地址从 CGame::update 内部提取。编译期宏控制:
XFER_CONV_PROBE=0
依赖缺失会导致关闭自定义面板后摄像机镜头偶发残留于特写视角。

3.9 视觉渲染与 HUD 交互强化 ​

目标标识拦截方式源文件 / 入口点功能实现与视觉管线配置开关(默认)未命中回退
装备 Tooltip 插孔遍历渲染 sub_588180
(格式化拼接 SOCKET{n} 控件名称,检索对应 Widget 填入宝石图标及名称;鼠标悬停 Rollover 与装备对比 Compare 均经由此接口)
detoursocket_glow.cpp
Detour_SockRow
在数据结构中动态登记「当前插孔的 SOCKETICON Widget 实例指针 $\leftrightarrow$ 目标特效光晕配置」;在 EndScene 渲染周期内,利用 ImGui 背景绘图列表(Background DrawList)在图标屏幕投影矩形区域叠加特效。v1 规则:若宝石显示名(CItem+0x208)包含「青莲」,则在图标边缘渲染青色法阵结界与动态雷弧特效。控件检索复用 sub_70C420;控件屏幕可见性与绝对像素坐标通过 CEGUIBase.dll 导出接口 isVisible 与 getUnclippedPixelRect 解析(实机内存断言证实:CEGUI 的 Rect 内存成员布局严格为 {top, bottom, left, right},早期静态推导出的 {left, top, right, bottom} 存在严重偏差)。该渲染属于纯叠加图层,完全不修改游戏原生 UI 的内部状态。签名批注:零售 1.25.9.5 与 Steam 1.25.5.6 双 build 唯一命中(tools/aob_check.py,2026-09-12)。
生命周期(2026-09-12):socketGlow=0 时一个 detour 都不装(懒装载,第一次开启才挂,与 WASD 同款);关闭后 detour 首句返回原函数结果并立即清空全部 CEGUI 指针缓存;每个缓存项钉住登记时 wrapper 与 CEGUI::Window 的两个 vftable,绘制前核对,对不上即剔(被释放后又被别的对象占坑的地址 IsBadReadPtr 拦不住,vftable 拦得住);另 detour CEGUIBase.dll 导出 WindowManager::destroyWindow(Window*)(QAE_NPAVWindow@2@@Z,retn 4)与 destroyAllWindows(QAEXXZ),窗口进 dead pool 那一刻剔除指向它的登记项——回主菜单 / 换关卡重建 UI 不一定触发 D3D Reset,光靠 OnDeviceReset 清不到;destroyWindow 导出缺失则拒绝开启。hook 表在进程生命周期内只增不减(2026-09-13 复核第 3 条):MH_CreateHook 之后永不 MH_RemoveHook、原函数指针永不清空——MH_DisableHook 只拦后来者,拦不住已经跑进 detour 的线程(例如正在 Detour_DestroyWindow 里等互斥锁的那条),它稍后要经原函数指针走跳板;装一半失败只 Disable 本次启用成功的那几个、保留 created 状态,下次开启从断点续装,不会对未 Create 的地址调 Disable/Remove。记账只跟真实状态走(同日二次复核 F3):Disable 失败的那个保持 enabled=true(detour 按功能开关早退,留着无害),MH_EnableHook 返回 MH_ERROR_ENABLED 视为已开启而非失败——否则状态一旦错位功能就永远打不开。
socketGlow
(默认开启;关闭时零 hook)
特征码或 CEGUI 导出接口缺失则模块安全禁用;四处 detour 全部启用才开启;任一失败只关回本次启用的、跳板保留,下次续装。
装备 Tooltip 整体渲染封装 sub_588600detoursocket_glow.cpp
Detour_TipFill
在引擎完成整个 Tooltip 绘制后,解析对应物品的稀有度属性(前缀包含 LEGENDARY,检索策略为预编译静态表优先、动态注册表回退)$\rightarrow$ 捕获并登记 Tooltip 根 Widget 的外边框屏幕矩形,在边框边缘叠加绘制金色动态火舌、符文流转与火星飞溅动效。同上特征未命中则金边火焰效果禁用。
护盾实时计量仪表盘
(零 Hook 纯只读实现)
只读轮询shield_hud.cpp每帧主动轮询玩家及其全部随从单位(解引用玩家结构体 +0x72C 宠物数组指针与 +0x738 数组计数;宠物与召唤物均通过 addPet 统一纳入该数组),读取其实体上挂载的 SHIELD BUFFER(Effect 122)的剩余吸收量(结构体偏移 +0xD4;由于引擎底层不持久化护盾初始上限,数值计算完毕后仅保留当前瞬时值,仪表盘通过自适应算法记录峰值以呈现「当前/上限」百分比),并在屏幕上渲染平滑护盾条。
技术路线选型动因:坚持采用每帧只读轮询而非拦截吸收函数——吸收函数仅在单位遭受攻击时方才被动触发,当护盾自然持续时间结束过期、或遭受 SHIELD BREAK 机制瞬间驱散时,拦截方案会导致 HUD 残留陈旧数值。装载期对 5 组关键布局特征码执行严格静态校验(包括护盾值偏移、效果类型判定、EffectMgr 偏移、EffectList 结构布局及随从数组偏移),任一签名未命中则整块禁用——与其呈现失真的脏数据,坚决杜绝渲染不准确的 HUD。内置 8 组高清图标(编译为 RCDATA 资源嵌入 DLL)。
shieldHud(默认开启);
快捷键 F8 随时向日志输出全场景所有带盾实体的微秒级详细分析 Dump
5 项布局签名未全数命中则整体拒绝挂载。
战斗统计面板(DPS meter)
护甲减免结算 sub_52C560(CBaseUnit::applyArmorSoakAndAbsorb)
+ INSTANT 效果执行器 sub_559690(CUnit::executeInstantEffect)
+ CSkillEvent 挥击执行体 sub_6C9CC0
另复用三处既有 Detour:sub_53BC80(modifyHealth)、sub_51FEB0(consumeAbsorbBuffers)、sub_52AC60(rollCritical)
detour ×3
(地基 2 + 技能归属 1;另搭车 3 处既有 Detour)
combat_meter.cpp
Detour_Soak
Detour_ExecInst
Detour_SkillEvt
记账入口 OnHealthDelta / OnDotChannel
面板内点「开始统计」与「结束统计」划定一个统计区间,区间内把玩家及其宠物/召唤物打出的伤害按 来源 / 技能 / 伤害类型 / 目标 四个维度分桶,实时刷新并绘制每秒伤害曲线;结束后数据冻结为完整汇总。
取数原理:① 伤害量取 modifyHealth 的入参 delta(即实际打出的伤害:护甲、抗性、格挡、吸收盾与暴击均已结算完毕);同时在调用原函数前后各读取一次 CBaseUnit+0x560,其差值为生命条实际扣减量,二者之差即溢出(Overkill)。
为何不以生命条扣减量为统计口径:原函数自带下限钳制(生命值归零;CUnit+0x6AA 置位的单位在生命值低于 1 时会被抬回 1.0 并同步清除 POISON——即引擎层面的「不可死」标记,练功假人靶子正属此类)。2026-09-11 实测:以生命条扣减量为口径时,靶子测得的 DPS 仅为真实输出的约一半,而靶子恰是衡量 Build 输出的标准场地。故总伤害/DPS/最大单击/元素拆分一律采用「打出」口径,生命条未承受的部分单列于「溢出」列,有效伤害 = 总伤害 − 溢出。
已证伪:modifyHealth 中 vt+0x1A8 的「无敌则回满血」分支对 CCharacter 与 CMonster 均指向 0x6A4650(xor al,al; retn,恒为假),不构成溢出来源;② 伤害类型取自 applyArmorSoakAndAbsorb 的 dmgType 参数(通道 0 物理 / 1 魔法 / 2 火 / 3 冰 / 4 电 / 5 毒 / 6 全类型),一次武器挥击是逐元素分别过护甲卷、最后合并成一次扣血(resolveStrikeHit 中 dmgType 为循环变量而 applyDamage 仅调用一次),故各元素占比是按护甲后数值回推的近似值,面板已就地标注;③ 技能名有两个来源,按精细度排序:(1) 效果自带的来源技能取自 CEffect+0x48 的 CSkill* → CSkill+0x10 的 dat 根节点 → DISPLAYNAME(缺失则退 NAME);(2) 武器挥击的来源技能取自 CSkillEvent+0x14 的 CSkill*(构造函数第一个参数,来源链可一路追到 CSkill 构造自身的 push esi),施法者取 +0x34 覆盖角色优先、否则 +0x2C 放置角色,并在 +0x136(USE_MASTER_FOR_ATTACKS)置位且施法者有主人(CUnit+0x71C)时改取主人。为何必须有 (2):TL2 里近战/远程技能的伤害主力是事件块上的 WEAPONDAMAGEPCT,它走 strike_pickWeapon → resolveStrikeHit → applyDamage,整条链一次都不进 executeInstantEffect;出厂技能 dat 实测 3034 份出伤技能里 2012 份属于这一类,不接则三分之二记不准。挥击上下文按施法者而非按目标配对——一次挥击可命中多个目标(武器弧 / MAX_UNITS_HIT),按目标配会漏掉第一个之外的全部;施法者比对同时是防误挂闸门:怪物技能作用域内玩家反伤扣怪物血时施法者对不上,自动落回「普通攻击」。嵌套(武器 proc 施法、击杀事件经来源技能再入)按 RAII 栈式存还,另加时间窗兜底以防 SEH 异常穿过 detour 时析构不执行。抛射物无需额外处理:技能事件自己发射的抛射物会把自身 iMissile 子对象(CSkillEvent+0x0C)挂进 CMissile+0x24C 的行为向量,命中时槽 4 即 sub_6CB1B0 → execute(this-12),等于重新进入 (2) 的 Detour,上下文当场重建。而「装备自己发射的抛射物」(sub_583AD0)那条路对技能伤害不可达:strike_resolveTargets 开头 553969 test bl,10h / jnz 会跳过整段装备发射逻辑,而挥击执行体里 v33 = v31 | 0x10 是无条件的 ⇒ 技能事件发起的挥击 100% 带 0x10;能走到 sub_583AD0 的只剩左键平砍,而平砍伤害本就该计入「普通攻击」、无技能名可归。(2026-09-19 三轮实机 + 回读汇编确认;此前为此加的 sub_5835B0 / sub_57D690 一对 Detour 与跨帧打标表已整体删除。)仍不覆盖:反射回去的抛射物(CMissile+0x304 置位那条分支绕开行为派发自行挥击,标志常量 0x13423 与效果 61 MISSILE REFLECT VISUAL 同源),量级极小,计入「普通攻击」;单位名取自 CUnit+0x5EC(引擎自身缓存的显示名:CCharacter_ctor 构造、CMonster_initFromUnitDef 按 DISPLAYNAME → NAME → "MONSTER" 三级兜底填入、promoteToChampion 将精英称号写回同一格、CMonster_updateFloatingUI_perframe 即取此格渲染头顶名牌),该偏移由 CMonster_initFromUnitDef 内 lea ecx,[esi+5ECh] 的 disp32 运行期反解,不硬编码;上述两处名称均为译后文本——DISPLAYNAME 在 DAT 中标注为 <TRANSLATE>(值类型 8),CDataGroup::getString 内部即 CDataValue__asWString(…, bTranslate=1),故 Runtime 侧不得重复翻译;面板若仍显示英文,即表明 TRANSLATION.DAT 中确实缺失该词条(MOD 数据缺口,非拦截层缺陷)。名称字符串统一经 overlay::RenderColorTagged 渲染 TL2 富文本颜色标签(|cAARRGGBB … |u),与掉落列表、诅咒结算面板共用同一套解析器;④ 持续伤害(DoT)在引擎里 modifyHealth 的攻击者恒为空指针,逐通道数值只在 computeNetHealthDeltaPerSec 内部的 7 次吸收查询里可见,故复用 §3.3 的 Detour_DotAbsorb 逐通道记账,单列为「持续伤害」来源。
统计边界:modifyHealth 入口带 isLocallyAuthoritative(this) || a3 闸门,联机非权威端不会为非己方权威单位执行本地扣血 ⇒ 纯客户端必然少计,单机与房主准确;木桶、箱子(CBreakable)与环境触发器(CTriggerUnit)按 RTTI 排除,不计入 DPS。
开销控制:三处 Detour 在未开启统计区间时函数体仅为一次布尔判断(含提前返回,连 RAII 守卫都不构造);单位名按定义 GUID、技能名按 dat 节点指针各自驻留为整数下标,战斗热路径上不构造字符串。会话结束时按伤害把前 8 个技能连同「来源」(效果/挥击/普攻/持续)写入日志,使归属结果不再只存在于面板。
[hooks] combat_meter
(默认开启,关闭则三处 Detour 完全不安装)
快捷键 End 或主面板按钮开关窗口;
统计本身只在点了「开始统计」后才运行
地基两条特征码(SIG_SOAK / SIG_EXECINST)任一未命中则整块放弃安装并记录警告日志 [W],面板显示不可用提示。技能归属那条(SIG_SKILLEVT)属可选增强,独立安装:未命中只让武器系技能退回「普通攻击」,面板其余部分照常工作并记 [W],面板脚注同步降级说明。
游戏原生 UI 风格化封装
(零 Hook 纯资源实现)
ImGui 渲染ui_frame.cpp从原版游戏纹理图集 MEDIA/UI/HUD/*.DDS 中精准切分出九宫格金边纹理与金色分割线并打包为 DLL 资源。驱动 ImGui 控件完全复刻《火炬之光 2》原生的沉浸式羊皮纸金边风格;通过 Windows WIC 引擎动态解码 RCDATA 资源并实例化为 D3D9 显存纹理(护盾 HUD 亦复用此图元管线)。——

3.10 操控模式扩展(WASD 键盘平滑移动) ​

目标标识拦截方式源文件 / 入口点功能实现与运动管线配置开关(默认)未命中回退
user32!GetCursorPosdetour
(懒安装)
wasd.cpp
HookGetCursorPos
仅在内部标志位有效的作用域切片内(即引擎调用该接口计算角色移动目标航点的切片),返回根据 WASD 键盘输入合成的虚拟屏幕光标坐标;其余常规渲染与交互周期内完全透传原生系统调用。从而保证光标视觉图标、法术技能瞄准射线等继续获取物理真实光标坐标,玩家的真实物理鼠标在键盘移动全程中保持绝对静止。wasd(默认关闭);
游戏内配置面板勾选为唯一激活入口
挂载失败则回退至通过 SetCursorPos 直接操纵真实系统光标的旧方案。
光标到世界地表坐标转换 sub_4258D0
(角色寻路移动与法术瞄准共用底层原语)
detour
(懒安装)
wasd.cpp
HookGroundPt
当满足 this == 游戏单例 && WASD 激活 && 调用栈不处于技能执行函数 sub_545070 内部 条件时,开启合成点注入标志并模拟持续按下鼠标左键。游戏引擎无缝沿用原生的「持续按住左键向目标地面点移动」逻辑,全量复用底层寻路网格、动态碰撞体排斥、地形坡度贴地及角色运动骨骼动画管线,完全不直接覆写角色的世界坐标,具备天然的多人联机网络安全性。
合成目标点设定为以屏幕中心为基准沿按键方向偏移 radius 像素(默认 100 像素,支持快捷键 [ 与 ] 动态微调)。开发早期曾尝试直接调用寻路接口或 SetDestination,实机验证均告失败(原生接口仅计算折线路径而不驱动物理运动状态机)。
生命周期管理:Hook 采取延迟懒装载策略,直至玩家在面板首次显式勾选时方才挂载;挂载后常驻不卸载(未激活时仅开销单次布尔分支透传,相比频繁挂载/卸载开销更低且杜绝了卸载失败风险)。
同上;
快捷键 F6 切换启闭(仅在当前运行会话中已于面板显式开启过时响应,防止全局误触);
快捷键 F7 标定模式(已整体停用,进入等同于暂停)
Init 两项特征未命中则面板隐藏该功能项;技能执行函数特征未命中则仅依赖单例指针分流(在按住 WASD 施放地面法术时可能发生目标点抖动)。

3.11 底层诊断探针(默认处于未装载状态) ​

目标标识拦截方式源文件 / 入口点功能实现与分析能力配置开关维护状态
可行走性判定 sub_5DB690
(CLevel::isWalkable,每帧移动更新密集调用)
detourgridprobe.cpp
Detour_WalkQ
仅捕获并缓存 this 指针,以零性能损耗获取当前活跃的 CLevel 关卡对象实例。
功能特性:
① 快捷键 F9:将游戏运行时内存中多层动态合成完毕后的整关网格数据导出为 .mpp 兼容二进制文件(生成 mikuro_griddump_NN_walk.mpp 与 _flag.mpp,结构包含 24 字节文件头与 $W \times H$ 矩阵字节,与原版发布格式逐字节二进制同构,可直接被离线仿真分析工具 tl2-zone-merge-sim 读取加载),并同步在日志中记录玩家坐标、当前单元格标志及 $13 \times 13$ 的 ASCII 字符邻域视图;
② 快捷键 F10:切换场景贴地网格透视可视化(将玩家周围 $\pm 24$ 栅格投影逆变换至屏幕坐标,直观甄别「不可通行空气墙 vs 地形美术表现」);摄像机矩阵依托 IDirect3DDevice9::SetTransform(槽位 44)捕获,多组渲染候选流基于「玩家投影点最贴近屏幕中心」算法自动标定。
安装前执行严格的序言字节偏移校验(+92 与 +688 偏移必须与逆向定案吻合),检测到多重命中则拒绝挂载。
标记文件 mikuro_grid_probe.txt
或环境变量 MIKURO_GRID_PROBE=1;
编译期宏控制:
MIKURO_GRID_PROBE_ENABLED=1
在编可用;2026-08-08 走格分析收尾后曾关闭,08-12 收到空气墙异常反馈后重新激活。
VFS 虚拟文件系统关键调用:
loadAll(sub_64A590)、
mount(sub_691360)、
setStatus(sub_67C2C0)与 fread
detour ×3~4probe_vfs.cpp度量游戏资源反序列化各阶段的微秒级耗时(包含 loadAll 状态转换、资源包挂载与磁盘流读取;启动性能补丁模块自身维护独立计时,避免对相同入口重复挂钩)。
逆向归因结论:MOD 挂载时的二次方排重耗时($O(n^2)$)已通过在打包阶段拆分 SPAWNCLASSES 子目录彻底规避(加载耗时由 1,817 ms 降至 563 ms,无需侵入式补丁);Spawnclasses 交叉链接产生的 1,349 ms 耗时分散于 913 万个瞬态 std::wstring 构造中,因无单一瓶颈点已决定放弃打补丁。
标记文件 mikuro_vfs_probe.txt
或环境变量 MIKURO_VFS_PROBE=1;
编译期宏控制:
MIKURO_VFS_PROBE_ENABLED=1
在编可用,2026-08-18 起重构为纯按需启用机制。
图形与逻辑耗时剖析器:
OgreMain.dll、CEGUIBase.dll、Plugin_OctreeSceneManager.dll 导出函数(含 renderOneFrame、_updateAllRenderTargets、renderGUI 等)+ D3D9 接口 + 游戏逻辑驱动 CGame::tick(sub_444130)、属性重算 sub_7AFE00、效果注销 sub_7AF2A0 等
detour
(导出表挂接 + AOB 特征扫描)
profiler.cpp全链路帧耗时剖析器。快捷键 F7 唤起仪表盘,按 Ogre 场景渲染阶段与 Direct3D 9 指令提交两个维度剖析单帧 CPU 时间分配;基于硬件 rdtsc 指令度量。额外记录「每帧 Effect 缩放求值迭代次数」(读取 §3.2 内联补丁维护的原子计数器)。内部代码调用 prof::Install() 已被显式注释屏蔽当前停用。源码归档记录:2026-07-25 性能分析收官(证实原版引擎各渲染与计算层级均慢于现代工业标准,但无单点绝对热点,D3D9 指令提交仅占总帧时的 2%~3%,主要瓶颈在 Ogre 图形树与玩法逻辑 CPU 线程);07-29 曾短暂启用以排查「升级被动技能瞬间掉帧」问题,随后复原。重新启用仅需解除两处调用注释。
掉落决议调用点探针—hooks.cpp详见 §3.6。编译期宏控制:
MIKURO_LOOT_HOOK_ENABLED=0
技术路线搁置。
帧耗时采样器:怪物 tick CMonster_tick_main(sub_56A420,虚表 +300;零售 0x56A420 / Steam 0x56BED0)+ 世界更新 sub_43F890(零售 0x43F890 / Steam 0x43FD70)+ OgreMain.dll 导出 RenderSystem::_updateAllRenderTargets 与 _swapAllRenderTargetBuffersdetour ×4(AOB ×2 + 导出表 ×2)+ 旁路采样线程。
怪物 tick 是 __userpurge(ecx=this,ebx/edi 也传参,retn 0Ch),普通 C detour 会踩坏寄存器传参,故用 naked 汇编包一层:记时 → 把 3 个栈参原样再压一遍 → 调跳板 → 记时 → ret 0Ch 替调用方清栈。
frame_sampler.cpp
Det_MonsterTick / Det_WorldUpd / Det_RsUpdate / Det_RsSwap / SamplerThread
回答「怪物一多,帧时间花在哪、随怪物数怎么涨」,只量不改。① 区域计时:每帧一行 CSV —— 帧 / 世界更新 / 怪物 tick 总计与最慢单怪 / 渲染 / Present 等待各多少 ms,本帧 tick 的怪物数(按引擎自己写的距离桶 CMonster+0x698 0..3 分列)与可见数。② 主线程 EIP 采样约 1 kHz(SuspendThread + GetThreadContext,挂起期间只读几个 volatile,不打日志不分配不取锁),每个样本带同刻的区域标签(世界更新 / 怪物 tick / 渲染 / Present),所以不回溯调用栈(exe 是 FPO 优化的,回溯本就不可靠)也能把叶子函数归到包含区域里。离线 tools/frame_sampler_report.py 用 IDA 函数表 tools/tl2_funcs_retail_1.25.9.5.json 归因、按怪物数分桶、做 a+bN+cN² 拟合、单列「占比随 N 涨得最快」的函数(合成数据自检:埋入的 400 ns·N² 二次项与热点函数均被准确还原)。
起因:2026-07-05《性能瓶颈分析报告_怪物密度掉帧》断言 O(N²) 的元凶是「房间全局单位链表被每怪每帧全量遍历」,2026-09-20 复核不成立 —— CLevel+0x88 是玩家链表(插入点 0x5E7B16 守卫 hasUnitType(0x1C),UNITTYPES.HIE 里 28 = Player;CMonster__updateVisibilityAndAILOD 拿它算的正是「到最近玩家的距离」),单机下那几处遍历是 O(N);报告称「已实现」的并行 broadphase 与 steering 降频从未进过仓库。超线性项尚未定位,先量再改。产出与日志同目录:tl2_fsample_frames.csv / tl2_fsample_samples.bin / tl2_fsample_modules.txt,每约 5 秒落盘一次、每约 50 秒重写模块表并打一行进度日志。
[hooks] frame_sampler(默认 0;面板「补丁及持久化设置」可勾,重启生效)。开着约 1% 主线程开销加每几秒一次的小段落盘,量完就关。在编可用,2026-09-20 新增。两条 AOB 任一未命中 → Unsupported 不装;Ogre 导出缺位 → Partial(帧边界退回主 RT EndScene,渲染列为 0)。签名已用 tools/aob_check.py 在零售与 Steam 两 build 验过:唯一命中且落在函数头。

3.12 基础引擎原语与解析符号表(只读引用,未实施 Hook) ​

下表整理了 Runtime 各模块频繁调用、但其自身代码段未被注入任何拦截桩的原生引擎接口与数据结构基址,供后续架构维护检索:

引擎底层原语解析来源与方式核心系统用途与消费场景
命名属性读取 sub_677F00、
GUID 到单位定义节点映射器 sub_65FDE0、
Units 静态数据库全局单例(从 sub_65FDE0 内部指令 mov ecx, [X] 反解提取)
hooks.cpp
ResolveEngine
掉落物名称解析与属性提取。
CStringTranslate 本地化翻译单例(从 Getter sub_6580E0 内部指令 mov eax, [X] 提取);
结构体偏移 +0x38 加载标志位、+0x8 BST 二叉树根节点;节点键位于 +0x10,翻译值指针位于 +0x2C
同上提取单位类型的中文本地化名称(仅在中文构建版本生效)。
资源管理器全局单例(从 Getter sub_6488C0 提取);
unittype 注册表指针 *(resmgr+0x6C),偏移 +0x18 加载守卫位、+0x8 BST 根节点;层级继承判定表位于 +0x1C、+0x20 与 +0x24
同上MOD 动态 unittype 逆向反查、is-a ITEM 类型判定、配置面板下拉列表构建以及扩展稀有度自适应发现(阈值 6 票)。
RTTI 类型描述符 TypeDescriptor ×4:
CItem、CCharacter、CBreakable、CTriggerUnit(名称字符串偏移 -0x8);手写 is-a 遍历管线走 COL $\rightarrow$ CHD $\rightarrow$ 基类数组
同上支撑整个框架所有「判定目标是否为玩家 / 道具 / 可破坏物 / 机关触发器」的 RTTI 类型安全判定。
MSVCP90.dll 导出接口:
wstring(wchar_t const*)、operator= 与 ~wstring
hooks.cpp
ResolveWString、
hook_savefix.cpp、
opt_findfile.cpp
支撑所有涉及修改或析构引擎内部 std::wstring 对象的安全操作。
严重警告:必须严格绑定 MSVCP90.dll,绝不可链接 MSVCP100.dll(早期描述符修改整体失效即因引用错动态库版本)。
Tooltip 单行渲染原语 0x7A91D0
(CEffect::getTooltipLine,调用参数 useStored=1)
hooks.cpp
EffectTooltipLine
在自制 UI 面板中渲染与游戏原生 Tooltip 逐像素一致的属性文本描述;useStored=1 保证只读零副作用,若误传 0 会覆写结构体 +0xD4 与 +0xD8 原始数值。
层级判定原语 sub_511FA0(CUnit::IsUnittype)、
标志位查询 CUnit::hasUnitType(早期逆向工程数据库误命名为 CCharacter_hasEffectTypeFlag,现已修正)
hooks.cpp 特征扫描 $\rightarrow$ 镶嵌门控 / enchant_transfer::Install宝石镶嵌合法性校验;附魔迁移装备大类校验(258 Equippable、8 Weapon、13 Armor;戒指标记链同时挂载 39 TRINKET 与 13 ARMOR)。
宠物嘲讽底层支持原语:
setTarget(sub_524AF0)、
CAIManager::setFlag(sub_701590)、
getPosition(sub_61DCE0)与游戏全局单例 Getter sub_41CE60
hooks.cpp
ResolveTaunt;导出辅助接口 MikuroGetPlayerPos 与 MikuroGetLocalPlayer
驱动宠物嘲讽逻辑;为走格探针与护盾 HUD 提供本地玩家绝对锚点定位(单例偏移 +0x2C + CCharacter RTTI 校验)。
附魔迁移系统底层原语:
allocHolder(需要长达 127 字节的复杂特征码以实现唯一命中,本质为 MSVC 静态局部变量初始化样板代码)、
CEffect 拷贝构造函数、itemLinkEffect、recomputeStats、invalidateTooltipCache、removeFromContainer / addToContainer、menuFindWidget(在 CUIMenu 内部查找,与 Tooltip 控件检索 sub_70C420 物理隔离)、widgetSetText、CWidget::setVisible / setToggled、addBakedDamageOfType、expireEffects、addGold、addRandomAffixes、endNPCConversation 及游戏全局单例
hooks.cpp 特征扫描 $\rightarrow$ enchant_transfer::Install详见 §3.8 附魔与打造系统的完整实现链路。
CEGUI 基础动态库导出接口:
?isVisible@Window@CEGUI@@QBE_N_N@Z、
?getUnclippedPixelRect@Window@CEGUI@@QBE?AVRect@2@XZ
socket_glow.cpp检索原生 UI 控件的屏幕可见性及其在视口内的绝对像素裁剪矩形。
权威状态判定 sub_511AC0
(CUnit::isLocallyAuthoritative)
hooks.cpp多人联机状态下判定本地实体是否具备网络权威性,驱动 DoT 护盾扣池与吸收决策;青莲术士各模块也用它只认本地权威的玩家。
青莲术士模块用到的引擎原语(2026-09-30 起):
STAT 查表 CStatManager::findStat 0x7BBBE0(只查不建)、
CUnit::getMaxHealth 0x521750、CUnit::getMaxMana 0x521950、CUnit::modifyMana 0x5245D0、
Boss 判定 CCharacter_isBossOrNoDestroy 0x41CC10、
CPlayer::getMaxCharges 0x5B4580、CPlayer::updateChargeBar 0x5B45D0、
CLevel::gatherUnitsInRadius 0x5EB670 及其输出 vector 的释放 0x6F4470、
CUnit::getLevel(0x5144E0 / 0x701513 两份相同代码)
hooks.cpp InitThread 扫前四条传给各模块;qlss_dingshen.cpp 自扫 findStat / isBoss;qlss_modou.cpp 自扫 getMaxCharges / getLevel / gatherUnitsInRadius / vector 释放四条,另加可选的 updateChargeBar读写 CLASS_QLSS_* STAT、算每秒消耗、读写充能条(炁)、收集玩家周围的敌我单位。用法与偏移见 §3.13。

3.13 青莲术士职业机制(qlss_*,2026-09-30 起) ​

青莲术士(MIKURO_CLASS_QLSS)有几个技能的效果光靠 DAT 做不出来,由下面这些模块补上。它们有几处共同点,表里不再逐行重复:

  • 参数全在 MOD 数据里。被动或技能事件把数值写成玩家 / 怪身上的 STAT(CLASS_QLSS_*),模块按 GUID 读写:CUnit+0x114 → CStatManager::findStat 0x7BBBE0(只查不建,所以数据侧要先把 STAT 建出来),CStat+0x90 为 1 = 整数型,+0x6C 是值。没有 conf 开关;原版数据和别的 MOD 不会写这些 STAT,模块读到 0 就什么都不做。
  • 只认本地权威的玩家(isLocallyAuthoritative sub_511AC0),联机客机上不做事。
  • 每帧逻辑都在 overlay.cpp 主后备缓冲那一次 EndScene 里跑,顺序是曲则全 → 神霄雷池 → 五鬼搬运 → 墨斗弹线 → 九老仙都印(五鬼必须在墨斗前:还炁和涨炁可能落在同一帧)。这时游戏逻辑的调用栈都已退出,可以安全施放技能。
  • 施放子技能统一走 deathward::CastPlayerSkill / CastPlayerSkillAt / CastPlayerSkillAtPos(抵命符模块提供,见 §3.7 抵命符行)。抵命符没装上时这三个函数返回空,这些模块就放不出任何子技能。
  • 计时用 GetTickCount 的带符号差。「没设过」的计时存 0,单独判:直接比 (LONG)(until - now) > 0 的话,开机超过 24.8 天后 0 会被当成「还没到」(2026-10-01 实机开机 26 天时撞上,墨斗和五鬼当天修掉)。
  • 只有定身阵自己挂 detour;九老仙都印一般借 §3.7 的主动技能 Detour 转调;其余四个不挂钩,只搭 modifyHealth 的既有 Detour 或在 EndScene 里读写 STAT、调引擎函数。
  • 模块没装上时数据侧照常运行,只是缺了 runtime 这一半效果,不会坏档、不会崩。
目标标识拦截方式源文件 / 入口点功能实现与运行逻辑配置开关(默认)未命中回退
怪物 AI 每帧更新 sub_5B1E40
(CMonster::updateAI_tickAndDispatch,__thiscall(this, float dt, char force),retn 8)
detourqlss_dingshen.cpp
Detour_Tick
2_2 定身阵的「真定身」:原版 IMMOBILIZE 只让怪不能走,它照样出招、放技能。
原版 AIFREEZE 怎么冻的(IDA 实证,零售 1.25.9.5):控制台派发器 sub_789B00 遇到 AIFREEZE 只翻转一个设置项(local_settings.txt 的 FREEZE AI)。全 exe 读这个键的只有本函数,位置在「思考」那一步:设置为真就整段 AI 状态机(追人 / 出招 / 放技能)不跑、直接返回。所以对单只怪让本函数直接返回,就只冻住它一只;少掉的只有函数前半段的攻击冷却倒计时,冻住期间本来也出不了手。
判定:数据侧两条计时 STAT(TIMED 0.1 秒、每跳减 1,词缀写入 秒数 × 10):CLASS_QLSS_DINGSHEN 每级都挂;CLASS_QLSS_DINGSHEN_BOSS 10 级(II 阶)起才挂、时长减半。Boss 判定调 CCharacter_isBossOrNoDestroy 0x41CC10(byte[this+0x836] 非 0,或 hasUnitType(0x10C))。普通怪:前一条 > 0,或 qlssmodou::LinkFrozen 说它被墨线连带冻住(见本表墨斗行)→ 这一帧不调原函数。Boss:看后一条;后一条为 0、前一条 > 0、且本地玩家 CLASS_QLSS_YIN_S2 > 0(九老仙都印盖过印的定身阵还在)时也冻,即印过的定身阵对 Boss 不减半。Boss 不吃墨线连带。
开销:每只怪每次 AI 更新多一次 isBoss 和一到两次 findStat。
日志(dingshen):普通怪定住 / 解开前 40 次;Boss 另记 40 次,注明是不是印过不减半。
三条签名(AI 更新 / isBoss / findStat)在零售 1.25.9.5、Steam 1.25.5.6、1.26.0.1 脱壳版上全映像唯一(tools/aob_check.py --sig,2026-09-30)。装在总闸之后(§2 第 12 步)。
无开关(数据侧驱动)三条签名任一命中数 ≠ 1 → 整个模块不装,记 [W];定身符退回原版效果(只能定住走路)。
无独立 hook:搭 modifyHealth sub_53BC80 的既有 Detour(§3.7 Detour_ModHP)+ EndScene 每帧
调用 CUnit::getMaxHealth 0x521750(__thiscall 无参 → int)、findStat、isLocallyAuthoritative
搭车 + 只读调用qlss_quze.cpp
OnDamage / Tick;
回血扣血借 hooks.cpp QuzeRestore / QuzeDrain
3_2 曲则全:窗口内挨的打先记成「劫」,到期或攒满一起放出去。
窗口 = CLASS_QLSS_QUZE > 0(计时 STAT,0.1 秒一跳)。窗口内本地玩家每挨一下(Detour_ModHP 在原函数返回后调 OnDamage):① 把这一下伤害的 ABSORB‰ 补回去(数据给 999 = 99.9%)。modifyHealth 自己不判死,调用方返回后看 HP ≤ 0 才走死亡流程,所以在这里补血,死亡流程不会启动(抵命符同一招)。② 劫 +1,最多 10。同一个攻击者两次计数至少隔 1 秒,它的多段 / 持续伤害一秒内只算一道;不同攻击者互不影响,一群怪一起打一下能涨好几道。记最近 32 个攻击者,满了挤掉最久没打的;没有来源的伤害共用一格。每计一道写 BANK(劫数),HIT 在 1 ↔ 2 翻一次(原生 UI 靠值的变化重播闪光);劫到 3 / 5 / 8 放 _STAGE 1~3 级换光环档;每道放一次 _PULSE(推到 Tick 再放)。
放劫:窗口到期或劫满 10,放 CLASS_QLSS_SKILL_3_2_BURST,等级 = 劫数 + BONUS(二阶起 10,用 11~20 级那套伤害);再扣反噬 = 劫数 × BACK‰ × 最大生命,至少留 1 点血。BACK = 0 是三阶(没有反噬,数据侧另给 +1 炁)。满劫放出时把窗口 STAT 写 0。窗口被外力清掉而劫没放出时,清账并放 _STAGE 4 级摘掉光环。
在 Detour_ModHP 里的位置:排在抵命符之后。抵命符已经救回满血的那一下,算出来的补血量是 0,不会重复补。QuzeRestore / QuzeDrain 直接调 modifyHealth 原函数,不再进 Detour;QuzeRestore 遇到单位已是 0 血时先写成 1,避开原函数的复活分支(理由同抵命符)。
getMaxHealth 签名只在零售 1.25.9.5 上确认过全映像唯一(IDA make_sig,2026-09-30),Steam / 1.26 待验。日志标签 quze。
无开关(数据侧驱动)findStat / getMaxHealth / isLocallyAuthoritative 任一未命中,或 modifyHealth 的 Detour 没建起来(回血扣血回调为空)→ 不就绪,记 [W];技能只剩一段没用的窗口和一次空炸。
无 hook:EndScene 每帧
调用 CUnit::getMaxMana 0x521950(无参 → int)、CUnit::modifyMana 0x5245D0(__thiscall(float delta),retn 4)、getMaxHealth、findStat、isLocallyAuthoritative
只读调用qlss_leichi.cpp
Tick;
扣血借 hooks.cpp QuzeDrain
1_4 神霄雷池改成状态技能:按一下开、再按一下关,开着时每秒扣血扣蓝。
数据侧每按一下写 PRESS = 1,以及 LV、COST(每秒扣最大生命 ‰)、COST_MP(每秒扣最大法力 ‰)、SELF_CHANCE、SELF_PM。模块看到 PRESS == 1 就写回 0、翻转开关,ON 写成 1 / 0(数据侧靠它挑「开」的那一下落步罡星桩)。
开着时每 1000 毫秒一片:当前法力(+0x584)付不起这一秒的消耗 → 自动关;否则扣法力(modifyMana 自己按上下限截)、扣生命(至少留 1 点),再按 LV(落在 1~15 内)放一片 CLASS_QLSS_SKILL_1_4_AURA。第一片在开的那一帧就放。COST_MP 读出 0(老存档没有这条 STAT)时按 COST 扣法力。卡帧、切窗口落下的片不补,从当前时刻重新起算,免得一口气扣好几秒。
劈到自己:每片按 SELF_CHANCE% 掷一次,中了就在这一片的 100~850 毫秒之间随机挑一个时刻放 _ZI(只有特效),额外扣 SELF_PM‰ 最大生命(至少留 1 点)和最大法力;关掉时还没落下的那一道作废。
换角色 / 读图(本地玩家指针变了)、人死了 → 关。
getMaxMana / modifyMana 两条签名在零售 1.25.9.5 全映像唯一(IDA make_sig / count_sig,2026-09-30),modifyMana 开头 E8 的 rel32 已通配;Steam / 1.26 待验。日志标签 leichi。
无开关(数据侧驱动)任一签名或扣血回调缺 → 不就绪,记 [W];按了只写几条 STAT,什么都不发生。
无 hook:EndScene 每帧 + 搭 Detour_ModHP
自己扫并调用 CPlayer::getMaxCharges 0x5B4580(无参 → int)、CUnit::getLevel(全映像两份逐字节相同的代码 0x5144E0 / 0x701513,都跳到 sub_403B80)、CLevel::gatherUnitsInRadius 0x5EB670(__thiscall,retn 1Ch)、vector 释放 0x6F4470、CPlayer::updateChargeBar 0x5B45D0(retn 4,可选)
只读调用qlss_modou.cpp
Tick / OnDamage;
连坐扣血借 hooks.cpp ModouHit;
对外导出 ChargeState / SetCharge / Enemies / Allies / SnapFrom / LinkFrozen
2_8 墨斗弹线:每次换行涨炁,从玩家身上弹一根墨线串住几只怪(几格炁连几只,最多 5),线留 4 秒,同一根线上的怪「连坐」分伤。
充能条(炁)怎么读(IDA 实证,零售 1.25.9.5):CPlayer+0xC2C = 格数基数(int,非 0 才有充能条),+0xC30 = 当前充能 0..1(float),+0xC44 = 衰减暂停倒计时;最大格数 = getMaxCharges = [+0xC2C] + getStat(170)。涨炁时 updateChargeBar 把暂停倒计时重置为 [+0xC40] + getStat(173),走完后 +0xC48 按秒减、+0xC30 线性往下掉。本模块读 +0xC30 / +0xC44 / getMaxCharges;要改充能只走 updateChargeBar(SetCharge),满格、跌出满格、涨了这些事件照常发。
弹线:格数比上一帧多 → 第一只取玩家 MODOU_RANGE 分米内最近的怪,之后每只取上一只 MODOU_HOP 分米内最近的(不重复,只看水平面);每段放 _LINE(只有画面),每只放 _TAN。同时最多 8 条链,满了顶掉最老的。
连坐:玩家打中链上一只(Detour_ModHP 原函数之后调 OnDamage,a5 是本地玩家),这一下的伤害(超出剩余血量的部分不算;打不掉血的假人照样算)× MODOU_PCT% 分给它所在各条链上的其他成员(去重)。分伤走 ModouHit = modifyHealth 原函数,a3 = 1、a4 = 1、a5 = 玩家:飘字,伤害记在玩家头上,不再进 Detour,所以不会连坐套连坐。分到不足 1 点的跳过;会打死的那一下改到下一帧放 _JUE(KILL 效果,击杀走引擎正路、算玩家的)。没学被动、只有纸人替死弹出来的链按 I 阶 18%。
II 阶控制顺线传:链上某只自己身上有 CLASS_QLSS_DINGSHEN(定身阵写的计时),整条链每帧续冻 150 毫秒,定身行的 Detour 经 LinkFrozen 得知;被连带冻住的不再往外传,Boss 不吃连带。链上某只的失魂层数 CLASS_QLSS_SHEHUN_CENG 这一帧涨了,就对这一帧没涨的其他成员各放一次摄魂阵的逐只结算 _ZHEN_JS;两条链共用一只时只补一次(2026-10-01 实机:重叠链一扫给了 +2),之后 0.4 秒内的涨层当成自己传的。
III 阶五点成阵:炁满 5 格且正好串满 5 只 → 再补五根星形墨线(第 i 只连第 i+2 只),每只放 _XING,整条链定身 1 秒(同样经 LinkFrozen)。
悬空指针:链里存「指针 + 64 位实例 id(+0x08)」。每帧用 gatherUnitsInRadius 收一次玩家 40 米内活着的敌对怪(unitType 27 = Monster;玩家 +0x31C == 2 时敌方阵营取 1,否则取 2;输出 vector 布局照原版调用方 tickDamageByMonsterCount 写成 {data, size, cap, 20},用完调 0x6F4470 释放),链上成员不在这张表里就踢掉,只解引用表里的指针。
导出的几个函数是上清各模块的公共件:五鬼搬运借充能条和收怪,九老仙都印借充能条、收怪和 SnapFrom(纸人倒下的地方弹链)。日志标签 modou。源码只给了零售版坐标,没注明跨 build 核验,Steam / 1.26 待验。
无开关(数据侧驱动:MODOU_LV = 0 且没有残留链时,每帧读完等级和充能条就返回)getMaxCharges / gatherUnitsInRadius / vector 释放须各唯一命中,getLevel 命中 1~2 处,findStat / isLocallyAuthoritative / 连坐回调(要求 modifyHealth 的 Detour 已建起来)齐全,否则不装、记 [W],导出函数全部返回失败:五鬼、仙都印随之失效,LinkFrozen 恒为假。只有 updateChargeBar 命中数 ≠ 1 时墨斗照装,仅 SetCharge 失败(五鬼还炁、盖印扣炁、闹宅 +1 炁不生效)。
无 hook(只读不挂):EndScene 每帧
充能条与收怪借墨斗模块的导出函数
只读调用qlss_wugui.cpp
Tick;
调试入口 ArmNaoForTest(只有 debug 分支调试 HUD 的 nao 指令调用)
2_9 五鬼搬运:被动每级把 WUGUI_LV、WUGUI_CAP(鬼仓格数)、WUGUI_RANGE(搬符射程,分米)、WUGUI_TIER 写成玩家 STAT。学了之后每 9.7 秒补放一次 _GUI(绕身鬼火,数据侧 10 秒,首尾接上)。
搬炁:衰减只在暂停倒计时走完后发生、每帧掉一点,技能扣炁则是一下一格以上。所以只有「暂停倒计时 ≤ 0 且这一帧掉得不到 0.2 格」才当衰减,掉的部分记进鬼仓(封顶 CAP 格),每扛进半格放一下 _CANG。下次格数变多时 SetCharge(当前 + 鬼仓) 一并还回、放 _HUAN,鬼仓清零。换角色、读图、死了、没学,鬼仓清零。
搬符:每帧取玩家 30 米内的活怪,记下身上有共享符 CLASS_QLSS_FU(行号 1~5)的那些(最多 96 只)。记着的怪这一帧不见了、上一帧又在玩家 26 米内 → 当它死了:在它最后的位置 RANGE 内找最近一只没符的活怪,放 _BAN_N(N 是原来那一行);更远的当走出视野,不搬。II 阶:怪还活着、符没了,而且离第一次看到这张符已经 8 秒上下(提前 0.6 秒到晚 1.5 秒之间)→ 当自然到期,同样搬一次;被万符归宗引爆的符时间对不上,不算到期。小鬼搬过去的符到期时不再搬(每张符只搬一次;不记的话一张符会一直接力下去)。
III 阶五鬼闹宅:10 秒内五行都搬过 → 对玩家 15 米内每只带符的怪放 _NAO_N、+1 炁,之后冷却 10 秒。
日志借用 modou 标签,正文带 [五鬼] 前缀。
无开关(数据侧驱动)findStat / isLocallyAuthoritative 缺 → 不装,记 [W]。墨斗模块没装上时充能条和周围的怪都拿不到,只剩绕身鬼火。
CSkill::start sub_6C39E0
(与 §3.7 主动技能 Detour 同一函数;__thiscall(this, caster, a3, pos, a5, targetPos, target),retn 18h)
转调:正式版由 hooks.cpp Detour_SkillAct 开头调,debug 分支由 skill_trace.cpp Detour_Start 开头调;两者都没挂时自己 detourqlss_yin.cpp
PreStart / Detour_Start / Tick
2_10 九老仙都印:炁满 5 格时放上清主动(2_1 / 2_2 / 2_3 / 2_4 / 2_6 / 2_7),自动盖印:扣 YIN_COST 格炁,这一招变「印过」版,落点砸印。
为什么挂在 start 前面:原函数开头写施法者 +0x14、目标 +0x1C、目标点 +0x2C,随后派发 EVENT_START。在调原函数之前把 CLASS_QLSS_YIN_SEAL 写成技能号 k,这一招自己的 START 块(SELF YIN_SEAL == k → 印威词缀 + 印符窗口,全在数据侧)就读得到。调用方是 CSkillList::executeSkill 0x6D6FA0 与 executeByName_pickIdleOrClone,玩家按键、数据 EXECUTE 的子技能、runtime 施放的都经过这里。
认技能:CSkill+0x84 → 技能定义,定义 +0x11C 的 std::wstring 必须完全等于 CLASS_QLSS_SKILL_2_k(正好 20 个字,k 取 1/2/3/4/6/7;子技能名字更长,自然排除);施法者必须是本地玩家,且 YIN_LV > 0。
盖印:炁满(不少于满格减半格)且过了「印泥未干」冷却(盖完一次后 YIN_CD 秒,数据侧 8;不加冷却时上清主动自带 +1 炁、扣完又满,几乎每下都盖)→ 先 SetCharge 扣炁(updateChargeBar 会发 UNMAXEDCHARGE,由此嵌套进来的 start 名字不在表里,直接放行),再写 YIN_SEAL(1.5 秒后 Tick 清 0),记下目标点,等原函数返回后由 Tick 在那里放 _YA(砸印 + 印文)。III 阶「印连」:盖印后 3 秒内下一个上清主动也算印过、不扣炁,只连一次。同一技能指针在同一毫秒内被转调两次只认第一次,防的是两处 Detour 都转调时把印连吃掉。
Tick 里另做的:每 5.7 秒补放 _XUAN(手边玉印);炁满时每 1.9 秒放 _LIANG。印过的摄魂阵(玩家 YIN_S4 > 0):25 米内的怪被阵加了一层失魂,0.15 秒后(阵自带的互锁过了)再放一次 _ZHEN_JS 补一层,补的那层不再触发,等于每扫 +2。印过的纸人替死:盖过印的 2_6 之后 3 秒内,在 15 米内找显示名含「纸人」的同阵营单位,它从 40 米的同阵营表里消失(碎了或到期)时调 qlssmodou::SnapFrom 从它倒下的地方弹一条 5 只的墨斗链。印过的定身阵对 Boss 不减半由定身行的 Detour 读 YIN_S2 实现。
挂钩冲突(2026-10-01):这个函数早有人挂。正式版是 §3.7 的技能百分比冷却(InitThread 里比本模块先装),debug 分支是 skill_trace(经 hookmgr::InstallNormal,更早)。挂上后函数头成了跳转,本模块的签名(SIG_SKILLACT 的前缀)落空,MinHook 也不能对同一地址挂两次。所以签名没命中时本模块照样就绪,等先挂上的那个 Detour 开头转调 PreStart;MH_CreateHook 返回 MH_ERROR_ALREADY_CREATED 也按这样处理;只有谁都没挂、签名唯一命中时才自己挂 Detour_Start。当天正式版一次印都盖不了:原来只有 skill_trace 转调,正式版里签名落空后一直等不到转调。affa495 让 Detour_SkillAct 也转调后,用调试版加 mikuro_trace_nostart(§4.2)走正式版挂法复测:炁满放灵符盖印、印连都触发。
日志借用 modou 标签,正文带 [印] 前缀;上清主动放了却没盖印时记原因(炁不满 / 印泥未干,前 30 次)。
无开关(数据侧驱动)。debug 分支可放标记文件 mikuro_trace_nostart,让 skill_trace 不挂 start、改走正式版的转调路径(§4.2)findStat / isLocallyAuthoritative 缺 → 不装,记 [W]。§3.7 主动技能 Detour 没装上时,本模块的签名若唯一命中就自己挂;两边都没命中,模块照样就绪却永远等不到转调,只剩手边玉印和炁满亮光,盖不了印(日志「CSkill::start 签名命中 0 … 由它们转调 PreStart」即此情形)。墨斗模块没装上时 ChargeState 恒失败,同样盖不了印。

4. 运行期配置、系统标志与调试速查 ​

4.1 配置文件规范:mikuro_runtime.conf ​

文件位于游戏执行目录,由 Runtime 独占解析(游戏启动器仅负责调度 Direct3D 包装层及 dxvk.conf)。格式遵循 key = value,支持 # 与 ; 行注释。游戏内置面板点击「存为默认」时将原子化写回(先写入临时文件 .tmp 再完成覆盖替换)。列表型配置项支持多行重复指定。

配置键名默认值关联功能职责
saveFix1控制 §3.2 存盘/读档 OOM 崩溃修复逻辑(Detour 保持常驻挂载,该配置为函数体内的快速放行开关)。
dotShield1控制 §3.3 DoT 伤害护盾吸收与扣减机制修正。
clearOnTransition0控制 §3.6 跨关卡切图时不持久化地面掉落物。
wasd0控制 §3.10 WASD 平滑键盘移动模式(启动阶段读取并回放状态;运行期以模块内部状态机为准,保存配置时回写)。
socketGlow1控制 §3.9 宝石镶嵌发光及传奇装备边框流火光晕。
shieldHud1控制 §3.9 实时护盾吸收计量仪表盘。
[hooks] combat_meter1控制 §3.9 战斗统计面板的三处 Detour 是否安装(属 [hooks] 段,改动需重启游戏生效;面板窗口的显隐与统计区间的起止均为运行期操作,不进配置文件)。
[hooks] frame_sampler0控制 §3.11 帧耗时采样器是否安装(诊断用,只量不改;开着约 1% 主线程开销加每几秒一次的小段落盘,量完就关。重启生效)。
enabled / logOnly1 / 1掉落物生命周期过滤总开关 / 仅记录审计日志而不执行物理拦截。
blockNormal / blockMagic
blockUnique / blockLegendary
blockQuest
0基于道具稀有度过滤(原版绿色与蓝色道具在底层统一标识为 MAGIC,无法剥离;blockQuest 为屏蔽任务道具的底层唯一开关,UI 界面默认不暴露)。
blockTier(支持重复定义)空匹配并过滤 MOD 扩展稀有度前缀(例如 EPIC、RUNE 等);旧版配置项 blockEpic / blockRune 保持向前兼容映射。
useLevelFloor / levelFloor0 / 0丢弃需求等级低于指定绝对下限的道具实体。
blockUnittype / blockName(支持重复定义)空单位类型黑名单匹配 / 道具名称关键词黑名单(子串不区分大小写模糊匹配)。
allowUnittype(支持重复定义,整表覆写机制:首次定义将清空内置出厂名单)
allowName
SOCKETABLE
AMULET / 空
道具保留白名单。配置 allowUnittype = 空值可显式清空出厂预置白名单。

4.2 文件系统标记文件与环境变量(定位基准为当前进程工作目录) ​

启闭状态标记文件名环境变量键值关联目标模块
强制关闭mikuro_no_skill_cd_fix.txtMIKURO_SKILL_CD_FIX=0技能冷却词条生效修复模块。
强制关闭mikuro_no_cosmetic_mp.txtMIKURO_COSMETIC_MP=0外观槽多人同步兼容模块。
强制开启mikuro_grid_probe.txtMIKURO_GRID_PROBE=1地形可行走网格诊断探针。
强制开启mikuro_vfs_probe.txtMIKURO_VFS_PROBE=1VFS 虚拟文件系统耗时分析探针。
强制开启mikuro_loot_probe.txtMIKURO_LOOT_PROBE=1掉落生成决议探针(编译期宏默认关闭,此开关处于休眠状态)。
强制关闭
(只在 debug/skill-trace 分支)
mikuro_trace_nostart(无扩展名;按游戏 exe 所在目录找,GetModuleFileNameW(nullptr),不看工作目录)—调试用技能事件追踪 skill_trace 不挂 CSkill::start(START 事件不再记),把 start 让给正式版的挂法(§3.7 Detour_SkillAct → qlssyin::PreStart),用来在调试版里测正式版的九老仙都印盖印路径(§3.13)。main 分支没有 skill_trace,这个文件放了也不起作用。

4.3 编译期预处理宏定义 ​

预处理宏标识当前预置值宏定义所在文件与生效范围
LF_EN由构建命令行 /p:Lang=en 注入全局开关:UI 文案英文输出,不加载系统级中文字体,不执行单位类型的中文本地化映射。
CURSE_MUTATE_ENABLED1curse.cpp:置 0 时仅执行内存结构分析 Dump,不执行词缀数值随机变异。
ENH_INSIGHT_POLICY0(取较大值)curse.cpp:当启迪效果作用于已存在账本的装备时,上限数值的合并仲裁策略(0 取大值 / 1 累加 / 2 直接覆盖)。
TRANSFER_ENABLED
TRANSFER_DESTROY_SOURCE
TRANSFER_SEND_NPC_AWAY
XFER_CONV_PROBE
1
1
0
0
enchant_transfer.cpp:控制词缀迁移总开关、事务完成后是否销毁源装备、NPC 交互后是否离开当前城镇(固定配置为常驻 NPC)、对话状态机诊断转储。
MIKURO_LOOT_HOOK_ENABLED0hooks.cpp:掉落物生成决议底层探针开关。
MIKURO_GRID_PROBE_ENABLED
MIKURO_VFS_PROBE_ENABLED
1
1
各自探针源码实现文件。
prof::Install() 代码调用注释停用hooks.cpp 初始化尾部与 overlay.cpp 渲染管线。

4.4 运行期交互与调试快捷键 ​

物理键位核心操作职责响应前置约束条件
Home呼出或隐藏 ImGui 运行时主配置控制台Direct3D 覆盖层成功初始化。
End呼出或隐藏战斗统计面板(DPS meter)Direct3D 覆盖层成功初始化;面板内的「开始统计」按钮另需两条特征码命中。
F6快速切换 WASD 键盘平滑移动模式的启用与暂停当前运行会话中已显式在配置面板勾选激活过 WASD 功能。
F7WASD 移动步进标定模式(已被停用,进入等同于逻辑暂停)同上;Profiler 诊断面板的 F7 快捷键随其整体停用。
F8触发全场景所有带盾实体的护盾吸收状态 Dump 至日志文件护盾 HUD 布局的 5 项特征签名完全匹配命中。
F9 / F10导出整关网格 .mpp 镜像 / 切换场景地表贴地透视网格渲染走格探针已显式通过标记文件挂载启用。
[ / ]动态微调 WASD 合成目标点的步长半径(步长增减 20 像素)同 F6 约束条件。

4.5 运行期日志规范与格式定义 ​

运行时日志输出至游戏同级目录的 tl2_mikuro_runtime.log(单次进程启动时清空重构)。日志行严格遵循定长格式: [+ 1.234][3f88][I][findfile] 日志正文内容

  • + 1.234:相对于 Runtime 代理 DLL 执行 DLL_PROCESS_ATTACH 的浮点运行秒数;
  • 3f88:当前输出日志的操作线程 ID 低 16 位十六进制表示;
  • I:日志严重性级别;
    • E(Error):预期生效的核心拦截失败或遭遇未处理异常;
    • W(Warn):非致命性降级,特征未中跳过可选模块;
    • I(Info):系统初始化关键里程碑与显式未启用说明;
    • D(Debug):单项详细内存状态转储与内部调用追踪。
  • findfile:在 loot_filter.h 严格注册的模块标签白名单。规范约束:即便某个功能模块被显式禁用,初始化流程中亦必须输出一行 [I] 明确注明未装载,彻底消除「模块因代码缺陷未安装」与「运行时版本过旧未包含该模块」的日志混淆。

5. 维护约定与版本演进准则 ​

  1. 增补拦截点(Add Hook):
    • 必须在 §3 对应分类表格中录入标准条目(目标 IDA 坐标、拦截方式、源文件与 Detour 符号、技术职责、开关规范及回退策略);
    • 若引入新的死槽 131 效果,必须同步更新 §3.7 的 SUBTYPE 号段分配表;
    • 若引入新的配置项、标记文件、快捷键或编译宏,必须同步更新 §4 相关速查表;
    • 必须在 §7「更新记录」中按当前真实日期追加一条变更摘要。
  2. 废弃或停用拦截点(Deprecate Hook):
    • 严禁直接从表格中物理删除条目;必须将其维护状态显式标记为「已停用」或「已搁置」,并注明历史废除日期与架构归因(如 profiler 与掉落探针的处理规范),同时在 §7 留痕。
  3. 特征码重构(Signature Refactor):
    • 若仅重构 AOB 特征码而不改变拦截逻辑,§3 表格内容保持不动,但在 §7 记录一次特征演进(跨版本兼容性回退是极其关键的维护历史)。
  4. 实证性保障:
    • 表内所有标称「实证」的论断均须在源码注释中能索引到对应的断言或 Dump 记录;若源码中注为「待验」,文档必须严谨标注为「待验」。
  5. 多端技术文档同步:
    • 本文内容必须与源码仓库中的 loot_filter.h 模块标签定义及用户文档 README.zh-CN.md 保持完全一致;改动其中任一文档时,需联动复核其余两处。

6. 运行期状态快照(截至 2026-09-12 工程状态) ​

在默认配置且所有 AOB 特征码全量命中的基准环境下,Runtime 常驻活跃的 MinHook Detour 拦截点共计 36 项:

  • 渲染覆盖层与设备管理:2 项;
  • 掉落物落地拦截:1 项;
  • 跨关卡切图存盘序列化过滤:2 项;
  • 存盘/读档 OOM 崩溃修复:2 项;
  • 主动技能激活(冷却缩减与嘲讽):1 项;
  • 宝石镶嵌部位与落孔拦截:2 项;
  • 宝石发光与传奇装备流火光晕:4 项(懒装载:socketGlow 关闭时 0 项;含 CEGUI destroyWindow / destroyAllWindows 两处缓存卫生 detour);
  • 乱铸与强化卷轴应用逻辑:2 项;
  • 自定义打造与附魔通道状态机:4 项;
  • EFFECTSLIST 运行时汉化覆盖:1 项;
  • 暴击状态捕获与暴击吸血:2 项;
  • Tooltip 词缀子类型分流器:1 项;
  • 四大数值上限突破与面板同步:5 项;
  • DoT 伤害护盾吸收与轮询修正:2 项;
  • VFS 与单位扫描性能优化补丁:2 项;
  • 外观装备槽网络复制拦截:1 项;
  • 战斗统计面板取数(护甲减免结算 + INSTANT 效果执行器):2 项。

除上述 Detour 之外,系统常驻挂载 6 处内联补丁 / 调用重定向:

  • CEffect 属性缩放悬垂指针防护 Code Cave:1 处;
  • 技能冷却词条生效修复内联补丁桩:2 处;
  • 外观槽网络同步条件分支 call 重定向:3 处。

以及窗口消息过程子类化拦截 1 处。

按需与非活动模块:

  • 按需懒装载模块:WASD 移动控制(2 项 Detour)、宝石发光与传奇流火(4 项 Detour,默认开启故计入上表);
  • 调试探针模块:走格分析探针(2 项 Detour)、VFS 耗时探针(3~4 项 Detour);
  • 归档与停用模块:全链路性能剖析器(停用)、掉落生成决议探针(搁置)。

7. 历史演进与变更审计记录 ​

记录按时间倒序排列;仅收录拦截点级别的增删、架构重构与关键缺陷修复,不收录纯文本或 UI 注释类提交。

变更日期核心技术变更摘要
2026-10-01补记 2026-09-28 合进 main 的抵命符 deathward(上一条列的遗留之一):§3.7 消费清单加一行 —— 自己挂角色 / 怪物死亡 sub_54E3F0(虚表第 120 格)兜底 KILL 效果这类不经 modifyHealth 的死亡,主路搭 Detour_ModHP,血被打到 0 时当场回满、下一帧施放 300 万护盾并销毁那枚符;同时写明它导出的三个施放接口是 §3.13 各模块放子技能的唯一通道。§3.7 号段表补 15~19(空闲)、20(天尊请神暗场,tianzun_sky.cpp,2026-09-25;它不挂钩,只读玩家效果表画全屏暗场,本文仍未收录)、21(抵命符);Detour_BuildTip 行补 21 号文案;Detour_ModHP 行补上搭车的几个模块与「未命中则它们一起不装」;§2 第 17 步、§3.1 EndScene 行同步。
2026-10-01补记 2026-09-30 ~ 10-01 合进 main 的青莲术士模块,新增 §3.13(六行)。qlss_dingshen(2_2 定身阵真定身)是其中唯一自己挂 detour 的:挂怪物 AI 更新 sub_5B1E40,被定住的怪这一帧整段 AI 不跑;II 阶起 Boss 也能定、时长减半,九老仙都印盖过印的定身阵对 Boss 不减半。qlss_quze(3_2 曲则全)与 qlss_modou(2_8 墨斗弹线连坐)搭 modifyHealth 的既有 Detour;qlss_leichi(1_4 神霄雷池状态技能)与 qlss_wugui(2_9 五鬼搬运)不挂钩,只在 EndScene 里读写 STAT、调引擎函数。qlss_yin(2_10 九老仙都印)要在 CSkill::start(sub_6C39E0)派发 START 之前写盖印标记,而这个函数已被 §3.7 主动技能 Detour 挂住,改由 Detour_SkillAct 开头转调 qlssyin::PreStart(§3.7 该行同步补上)。这处冲突让正式版当天一次印都盖不了(原先只有 debug 分支的 skill_trace 转调),affa495 修好后用调试版加 mikuro_trace_nostart(§4.2 新增,只在 debug/skill-trace 分支)走正式版挂法复测通过。§2 第 12 / 17 步、§3.12 原语表同步补上。同日 runtime 另修:墨斗 / 五鬼里「没设过」的计时(0)在开机超过 24.8 天后被当成「还没到」。还没补的:这几个模块的日志标签 dingshen / quze / leichi / modou 没登记进 loot_filter.h 的标签白名单(§5 第 5 条);getMaxHealth / getMaxMana / modifyMana 只在零售版核过唯一,墨斗与九老仙都印的签名没注明跨 build 核验;同期的抵命符 deathward(2026-09-28,§3.13 各模块施放子技能都靠它)本文还没收录;§6 快照仍停在 2026-09-12。
2026-09-23撤下 §3.5 findfile 哈希索引(opt_findfile.cpp 编译期硬关 MIKURO_FINDFILE_INDEX_ENABLED 0,注册表默认改 false;conf 里写 true 也只得到 Unsupported,连 mountIndex 的 hook 都不装)。复核发现其安全论证前提不成立:文件头称 bucket 追加只发生在 mountIndex,故包住它即与建索引互斥;实际注册散装文件的 sub_6926E0 也会往已有 bucket 追加并 vec_grow(释放旧数组),并递归登记进父目录 bucket,不经过 mountIndex、不拿 g_cs、不增代数。调用方:findFile 自身冷路径、CMod__activate、sub_68DEB0(运行期「添加本地文件」,setGameState / reloadAffixes 等)。另一线程恰在给同一大目录建索引时触发扩容即读已释放内存 —— 与原版「扫描 vs 追加」同类,但建索引(全表遍历 + 排序)把窗口放大一个量级。收益只在启动(9 MOD 约 1.09 s/次),运行期过图 findFile 总计仅 29 ms,不值得背条件性 use-after-free。源码保留,重启用 = 常量翻 1 且须先补上 sub_6926E0 的互斥。unitscan 同时复核:与原函数 sub_65DC90 逐句等价(原函数唯二的副作用就是那两份从不读的 wstring 拷贝),1.26 实机 4/4 有效比对通过,保留。
2026-09-23findfile 在 1.26.0.1 上装不上索引的修正(seh_frame_probe.h)。诊断日志抓到原因:一个入口在 Torchlight2.exe 壳段(04CAF310)、EIP 停在 ntdll 等待的线程,对它 GetThreadSelectorEntry 恒失败(err=31),在途检查连续 5 s 全 -1 → 放弃装索引,InitThread 因此多拖约 5 s;技能冷却修复用同一探针,要等该线程消失(约 +6.5 s)才查通。该线程并非每次启动都出现。修法:选择子换不出 TEB 时改用 NtQueryInformationThread(ThreadBasicInformation) 取,两条路都须过 NT_TIB.Self == TEB 校验,判定规则不变;ntdll 函数在挂起任何线程之前解析。tools/test_seh_frame_probe.cpp 验证 WOW64 下两条路给出同一个 32 位 TEB、兜底地址走链能数到帧、错地址被 Self 挡住。探针同时加了查不了时的原因诊断(线程、步骤、错误码、入口与 EIP 所属模块),以及全进程兜底命中统计(InitThread 结尾一行 [sehprobe])。实机状态:修正后 1.26 两次启动 findfile 均装上、InitThread 12.4 s → 约 6 s,但这两次该线程都没出现,兜底尚未在实机被触发过;以后日志出现「TEB 兜底命中 N 次(链走通 N 次)」即为实机证据。
2026-09-23§3.4 新增父 MOD 数量上限门 ApplyModCapEarly,并修正其时序:1.26 加壳版的 10→100 poke 首版在 InitThread 后段(约 +6 s),晚于 loadAllData 前部唯一一次上限检查,玩家反馈勾了「允许 10+ MOD」不起作用;现改在 DllMain 同步执行、早于数据加载线程。启动器同步改为按版本分流:仅 1.26 设 MIKURO_ALLOW_OVER10 交给 runtime,其余版本恢复挂起启动时由启动器写入的老办法。1.26.0.1 实机已验:12 个 MOD 全部加载。
2026-09-23抛射物空实体防护改为走虚调用取模型,此前等于没装(hooks.cpp)。取 CGenericModel* 的那一槽(虚表 +0x110)是按类各自重载的:CCharacter 是 sub_555EE0(this+0x220),而 sub_583AD0 的 this 是 CEquipment,它是 sub_585700(this+0x2E0)。防护第一版硬读 self+0x220,判的对象与引擎用的不是同一个,恒判「不用拦」——2026-09-19 玩家转储 tid7744 仍崩在 sub_583AD0+0x141 即是此故。改走虚调用后引擎取哪个我们就取哪个。教训:凡引擎自己用虚调用拿到的东西,防护里就不能替它把偏移写死。同日由玩家报告「先祖的荣耀 6 件套普攻必崩」实证了触发数据:暗黑传奇 的 Ancile5 把武器模型指向不存在的 MODELS/WEAPONS/THROWINGGLAIVES/THROWINGGLAIVE1,且给近战单手剑挂了 MISSILE: —— 缺网格令实体为空、MISSILE: 把普攻拽上抛射物那条路,两者缺一不崩。
2026-09-20新增 §3.11 帧耗时采样器 frame_sampler(默认关;[hooks] frame_sampler):怪物 tick / 世界更新 / Ogre 渲染 / Present 四处包含式计时按帧落 CSV,外加约 1 kHz 的主线程 EIP 采样,每个样本带同刻区域标签;离线 tools/frame_sampler_report.py 按怪物数分桶、拟合 a+bN+cN²、按 IDA 函数表归因。起因是一次复核:2026-07-05《性能瓶颈分析报告_怪物密度掉帧》把 CLevel+0x88 当成「房间全局单位链表」并由此断言 O(N²),实际它是玩家链表(插入点 0x5E7B16 的守卫 hasUnitType(0x1C),UNITTYPES.HIE 里 28 = Player),单机下 sub_540140 / sub_51D6D0 / sub_542780 顶部那几处遍历都是 O(N);报告 §9.3 / §9.5 称「已实现」的并行 broadphase 与 steering 降频两块代码从未进过仓库(git log --all -S 零命中)。顺带核实:[client+0x1B60] 是游戏状态枚举(取值 0/1/7/9),不是报告所说的「做 steering」标志;CMonster+0x698 距离桶(阈值 10/15/20/25)目前只驱动动画思考间隔。超线性项待实测定位,本次只加量具不改行为。hookmgr::Count 10 → 11,tools/test_hook_manager.cpp 同步。
2026-09-15嘲讽「怪被拉过去之后再也不回头打玩家」一事的定性,以及临时可见位诊断的移除(hooks.cpp)。① 删掉 MIKURO_TAUNT_VISDIAG 那段临时诊断 —— 它要问的事已有答案:实机日志里宠物的 +0x69(有效可见位)恒为 1,「宠物隐身导致怪选不中它」这条假设被否掉。② 读完整条 AI 选目标链后确认:我们的 setTarget 在引擎层面完全不粘,窗口结束后的持续仇恨不是我们造成的。怪的追击态(CMonster 虚表 +0x248 = sub_5AE6D0)每次思考都可能掉回待机态(AI_CHANCETOATTACK 掷骰不过 / 打完一轮 / 离锚点超 35),而待机态的 sub_5AF830 调 CCharacter::findAITarget(sub_5223A0),该函数不读当前目标 unit+0x320,每次都从零重做一次锥形搜索;唯一护着当前目标的分支在 sub_5AE6D0 内、被 CAIManager::isFlagSet(1)(BERSERK)门住,普通怪走不到。真正造成持续的是 Scene__findSkillTargetInCone(sub_5FBA10)的合格性判定,两道门:候选落在近距半径 a4 内则不看朝向直接合格(a4 = unit+0x33C,sub_51D460 从 dat 键 MOTION_RADIUS 读入,引擎默认 4.5,再按难度档加 CGlobals+0xB0..0xBC;本数据包 2676 个怪实测中位 7.0 / p75 12.0,作对比 SIGHT_RADIUS 中位 14.0);落在 a4 之外则必须同时在怪自身朝向的锥形里且在视野内。选谁:a7!=0 取合格者中最近的,a7==0 从单位环的随机起点取第一个合格的,而 sub_5AF830 有 80% 的思考走前者。因此要分站位看:玩家在 7 米开外(远程/放风筝)时,嘲讽把怪转过身之后玩家既出 a4 又出锥形、彻底不合格,之后每次重挑都只剩贴脸的宠物 —— 这才是玩家反馈的「再也不打我了」;玩家和宠物一起贴脸时玩家始终在 a4 内、一直合格,只是 80% 那支因宠物更近而选宠物,20% 那支仍会时不时挑中玩家,并非被完全无视。结论:窗口 4s / 刷新 200ms 两个数维持不动 —— 调短调稀不改善该现象(现象不由窗口产生),只会削弱拉怪本身;该行为也正是嘲讽的设计意图。
2026-09-15抛射物空实体防护首次命中时,就地抄走 Ogre.log 里的缺文件行(新增 ogre_log_scan.h + tools/test_ogre_log_scan.cpp,run_hook_tests.ps1 加一步)。起因是时效窗口:缺失的网格文件名只有 Ogre.log 有,而它每次启动游戏被清空重写 —— 玩家崩完先重开一次游戏,证据就没了。抄进我们自己的日志后,靠 .prev.log 轮转多扛一次重开。解析抽成无游戏依赖的头文件是为了能真测:单测把读取块大小从 1 试到 32768 要求结果一致,并专测「末行没有换行」—— 第一版确实漏了它(累加器里的残行从不处理),而崩溃现场那行恰好是最后一行正是最该抓的情况。
2026-09-15sub_583AD0+0x141 那一族(武器抛射物生成,两名玩家、两个 build 同一处)全静态定性并加防护。此前只知道崩在哪条指令;这次把四层解引用逐层解开,定到空的是 CGenericModel+0x5C(该模型的 Ogre 实体),再顺着 sub_5A2440 → sub_674770 追到根因:单位的网格文件没打进包,PackageMgr__findFile 找不到 → 引擎往 Ogre.log 写 Unable to find file : <文件名> 并把实体置空,单位照常活动直到一次生成抛射物的攻击。机理、返回值安全性论证、调用约定(含「ebx 不是入参、Hex-Rays 标注是假的」)见 §3 对应行。连带把 Ogre.log 加进反馈包(game-logs/Ogre.log)—— 缺失的文件名只有它有,引擎把模型对象上的名字字段清成了空串,运行时事后取不到。
2026-09-15玩家 MIKURO-feedback-20260915-181732 两份游戏自带转储的定性,以及由此加的一道防护 + 一条新的诊断通路。① 两份是两族:06:15 那份 = CCharacter__strike_resolveTargets+0xB4 → sub_583AD0+0x141,与前一名玩家同一处同一条链(只是换到 Steam 1.25.5.6 上);07:50 那份是新的 —— sub_664CE0+0x3,__thiscall 的 this 进函数即 NULL,来自 sub_5C2530+0x4B6 的复活音效。定位手法:转储不含调用方那页代码,改从本机 Steam 副本按 RVA 取字节(先用两个已知崩溃点做同 build 自校验),再拿字节签名回零售 IDB 认函数;栈上的候选逐个验过前面是否紧跟 call 才认作真返回地址。② 新增 sub_664CE0 空 this 防护(§3 对应行),默认常开。③ crash_dump 新增 GuardDump:防护点首次命中写一份现场转储(§3.2 对应行),启动器分族收包,并在下次启动时用单独的措辞请玩家发回来 —— 不能说「上次崩溃了」,那次玩家根本没崩。
2026-09-15crash_dump 新增第三道入口:用 MinHook 挂 dbghelp!MiniDumpWriteDump(挂钩逻辑在 crash_dump.cpp 内,不经 hooks.cpp)。起因是玩家当天的 sub_583AD0 空指针崩溃 —— 游戏用帧内 __except 接住了它,顶层过滤器永不触发,我们一份转储也没有,只能从游戏自带的 80 KB 转储读出崩溃地址,读不出对象。机理与取舍(为何不顺手在 VEH 里认访问违例、为何必须在 detour 内拷贝异常记录、为何要把 g_write 指向跳板)见 §3.2 对应行。测试同步新增 g 用例专测这条路;run_hook_tests.ps1 的 crash_dump 与 objwindow 两条编译行相应带上 MinHook 源码与 include 目录。
2026-09-15宠物嘲讽不再使用引擎的目标锁(hooks.cpp PetTauntFor / 新增 MikuroTauntTick,overlay.cpp 每帧驱动):加宽采集窗口后的玩家转储终于能读出崩溃单位的状态字段,由此把这一族栈溢出定性为 setFlag(5, NEVER CHANGE TARGET) 导致的 AI 自递归 —— 锁使 setTarget 失效,「验证目标」与「持有目标」错开后,updateAI 状态 13 分支会把状态设回 2 并立即重入自身,无界递归。改为只下发 setTarget、由我们每 200ms 重按目标维持 4 秒窗口,setTarget 恢复生效后复验的就是刚验过的同一目标,循环当场终止。机理与证据见上方 §3.2 对应行。
2026-09-15crash_dump 的对象采集窗口加宽(CollectRanges + DumpCallback):玩家 2026-09-15 的两族栈溢出转储(零售 1.25.9.5 与 Steam 1.25.5.6 各一组)已能证实崩溃是单位 AI 的自递归——以 CMonster::updateAI_tickAndDispatch 内部返回地址为锚在 1246 层上逐槽采样,每个栈槽的取值数最多 4、绝大多数为 1,没有任何槽呈现上千个互不相同的指针,故递归反复进出的是同一批对象,与场上怪物数量无关(玩家反馈「有时一个怪也崩」由此得到解释;此前「怪群密集导致连锁踉跄」的推断作废)。但 WithIndirectlyReferencedMemory 的窗口只到 +0x2FF,读不到 +0x314 / +0x320 / +0x7E8,无法判断是否与嘲讽下发的 setFlag(5)(NEVER CHANGE TARGET)有关。现改为自己经 MemoryCallback 补内存段,每个栈上指针补 16 KB。A/B 实测(test_objwindow.cpp,同一负载分别编译改前 / 改后的 crash_dump.cpp,堆上 0x900 字节对象在 +0x314 / +0x320 / +0x7E8 埋哨兵):改前仅 +0x000 可读,改后三处哨兵全部读出且值正确;合成负载下转储体积 1.22 MB → 1.29 MB,真实游戏栈估在 2.6 MB → ~10 MB。随之把保留份数从 5 降到 2(kKeepDumps),启动器反馈包同样只收最新 2 份(MaxOurDumps);并修掉 RuntimeDirs 的去重漏洞 —— AppContext.BaseDirectory 带尾反斜杠而 Path.GetDirectoryName(gameExe) 不带,启动器装在游戏目录(正常部署)时两者比不相等,每份文件都被收进 zip 两次,包体白白翻倍。
2026-09-13玩家 dump 到手后的二次定性与两处修复(docs/player-crash-records-2026-09-13.md):① crash_dump 顶层过滤器槽位被游戏抢走的实证与改法——游戏在 WinMain 与每次 C++ 抛异常经 CRT(sub_84B1C1 __CxxUnhandledExceptionFilter)反复重设 SetUnhandledExceptionFilter,玩家一进本地图就把我们顶掉(装好了照样只出 0 字节 dump),故改用 AddVectoredExceptionHandler(1,…) 兜栈溢出(VEH 是独立链、抢不走),顶层过滤器降为兜其余;测试新增「装好后故意夺走槽位仍须写出 dump」用例。② 新增 §3.2 sub_527000 空 this 防护:玩家占多数的崩溃族并非栈溢出,而是技能动画名解析器被非 CCharacter 施法者以 self=NULL 调用;触发键精确定位为技能 .dat 的 <STRING>ANIMATION(= CSkillProperty+0x1A8)。同时更正本条早先的写法:一度把元凶归给「MOD 给怪物挂玩家技能(群魔堕落等 11,862 个非角色单位)」,已作废——RTTI 证实 CMonster 派生自 CCharacter,怪物/玩家/常规召唤物均安全。detour 在 self==NULL 时原样返回传入串、保留基础动画,并去重打印动画基名用于反查元凶单位;6 份实机 dump 证实、双 build AOB 唯一。
2026-09-13玩家闪退定性与崩溃转储(docs/player-crash-records-2026-09-13.md):① 39 次 ntdll.dll+0x53B02/0x53675 c0000005 全部是栈溢出被游戏自带 TopLevelExceptionFilter 的二次崩溃掩盖——该处理器在崩溃线程上 LoadLibrary(dbghelp)→MiniDumpWriteDump→加载 dbgcore 时栈见底;39/39 记录的最后加载模块都是 DBGHELP.DLL,其余 14 份 0/14;本机 x86 复现实验(stackovf_test.cpp)在 Win11 上得到同形 WER 记录(APPCRASH ntdll.dll c0000005)。撤回 09-12「skill_cd_fix 安装竞态为主因」的判断(7785665 含该修复仍原样复发)。② 新增 §3.2 crash_dump:先于游戏过滤器、在独立助手线程上写 minidump 到 tl2_mikuro_runtime.log 同目录 tl2_mikuro_crash_*.dmp,栈溢出时不再链给游戏过滤器;conf crashDump。③ 主面板与战斗统计面板折叠后标题条按标题文字宽收窄(ui_frame.cpp CollapsedTitleBegin/End,只在 ImGui::Begin 执行期间改 SizeFull.x,展开宽与 imgui.ini 不受影响)。
2026-09-13dotShield 复核修正(原生回放 docs/dotshield-audit-2026-09-13-evidence/):撤销「物理护盾兜底」第二趟(前提是把 applyDamage 的反伤调用误读为直伤兜底;且在非权威端对 ALL 盾池双重吸收);速率写回改为守恒式 rate − 已吸收/真实 Δt(原先长帧下伤害凭空消失);加同线程守卫与计数。§3.3 缺陷机理改为只保留「同属性盾永不扣减」一条。
2026-09-13二次复核(docs/review-7e8b8cf-2026-09-13.md)三条修正:F1 quiescent_write.h 每次 SuspendThread 后立即 GetThreadContext 确认目标真正停下再枚举(SuspendThread 是异步请求),压测新增「异步挂起模型」用例;F2 延迟拆除的身份字段自检不过则不入队(漏过滤而非绕过身份校验去拆);F3 socket_glow 回滚记账跟真实状态走:MH_DisableHook 失败保持 enabled=true,MH_ERROR_ENABLED 视为已开启。
2026-09-13按 docs/review-fixes-2026-09-13.md 的复核结论修正前一日提交:① quiescent_write.h 事务在全体挂起后再枚举直到一轮无新线程(第一次枚举之后、挂起之前出生的线程原本不在检查集合里),窗口内枚举改用 NtQuerySystemInformation 写入挂起前分配的缓冲(Toolhelp 内部用进程堆),压测新增「枚举后出生并进帧的线程」用例;② 延迟拆除的身份校验加入单位 64 位实例 id(unit+8/+12)、单位自认关卡(CUnit::getLevel = *(*(unit+0x1C4)+8))与 ≤ 2 s 年龄闸——vftable 只认类型不认实例;③ socket_glow hook 表只增不减:不再 MH_RemoveHook、原函数指针不再清空,失败只关回本次启用的;④ skill_cd_fix 两处同页补丁点按一段改一次页属性、恢复一次(原先按序两次恢复把页永久留在 RWX);⑤ DLL 指纹 SelfSha256Hex 改为栈缓冲 + RAII 句柄,不再有可抛 bad_alloc 的分配。同时按复核限定收回两处过满表述:skill_cd_fix 与玩家主崩溃簇的关联不能唯一归因(WER 缺寄存器与调用栈);旧否决路径的 CItem 泄漏是源码层推断,所有权链未完整追踪。
2026-09-12按 docs/player-crash-records-2026-09-12.md 的建议顺序处理玩家崩溃记录(保留 skill_cd_fix 与真过滤开启):① §3.2 skill_cd_fix 写入重做——以 executeInstantEffect 专属 SEH handler + 序言 EIP 窗口判定在途线程,MODIFY / SET 在一次全体挂起事务中同时写入,预算 20 s,耗尽两处都不写,取消 Partial;事务抽为 quiescent_write.h,新增压测 tools/test_quiescent_write.cpp(并入 run_hook_tests.ps1)。② §3.6 掉落过滤由「否决落地」改为「放行落地 → 下一帧 EndScene 用引擎 CLevel::removeAndDestroyUnit 拆除」,修复每个被过滤掉落泄漏一个 CItem 的问题;拆除前核对 vftable 与关卡物品链表成员资格;新签名 SIG_LEVEL_REMOVEUNIT 双 build 唯一命中。③ §3.9 socket_glow 改为懒装载(关闭时零 hook)、缓存钉 vftable、新增 CEGUI destroyWindow / destroyAllWindows 两处 detour 主动剔除缓存。④ hook_b 序列化签名命中数 ≠ 2 时整体不装(此前命中 1 或 3+ 会装一半或钩错函数),文件头改正为「角色存档也会经过」。
2026-09-12崩溃审计 P0-4:hooks.cpp 五处 VirtualProtect 失败即放弃对应补丁并记录(此前忽略返回值继续写入);日志启动行加入 DLL 自身 SHA-256 前缀与编译期 build id(build_id.h.in 由 vcxproj 的 StampBuildId 目标生成),用于把玩家日志与具体产物对上。
2026-09-11战斗统计面板统计口径修正:总伤害/DPS/最大单击/元素拆分由「生命条实际扣减量」改为「实际打出伤害」(modifyHealth 入参 delta)。动因为实机数据:CUnit+0x6AA 置位的单位(引擎级「不可死」标记,生命值低于 1 时抬回 1.0 并清除 POISON,练功假人靶子即属此类)会吞噬绝大部分伤害,旧口径下靶子测得 DPS 仅约为真实输出的一半。生命条未承受的部分单列为「溢出」列(原名「过量」),有效伤害 = 总伤害 − 溢出;各列均补充悬停说明。同期证伪:vt+0x1A8 的无敌回血分支对 CCharacter/CMonster 恒为假(xor al,al; retn)。
2026-09-11战斗统计面板修正两项取名缺陷:① 单位显示名改从 CUnit+0x5EC(引擎自身缓存、已完成本地化)读取,偏移由 CMonster_initFromUnitDef 的 lea 指令 disp32 运行期反解——此前误将 CUnit+0x8/+0xC 的实例句柄当作定义 GUID 去检索定义节点,检索必然落空并回落至内部代号(面板显示为 WB01ARBF 一类全大写标识);单位实体本身并不持有定义 GUID 字段。② 全部名称单元格改经 overlay::RenderColorTagged 渲染,修复 MOD 名称中的 |cAARRGGBB…|u 富文本标签被当作字面量显示的问题。同时将分桶键由 GUID 调整为名称字符串(同名怪物自然归并、精英单位独立成行),RTTI 非战斗单位判定的缓存键调整为 vftable 指针(规避实例地址复用导致的误判)。
2026-09-19§3.9 订正:删除 sub_5835B0 / sub_57D690 一对 Detour 与跨帧打标表(Detour 由 5 降回 3)。三轮实机(弓 / 无 MISSILE 键的弩 / 有 MISSILE 键的弩)中该对钩子一次都未被调用,探针打出的行为对象地址全为 CSkillEvent+0x0C、从无 CEquipment+0x268;回读 strike_resolveTargets 开头发现 553969 test bl,10h / jnz 跳过整段装备发射逻辑,而挥击执行体无条件 | 0x10 ⇒ 技能伤害永不可达该路径,故整体移除。同批:会话结束写入前 8 个技能的伤害/占比/命中数/来源;抛射物钩子到场探针(有上限);loot_filter.cpp 的日志改用 _wfsopen(_SH_DENYWR) 打开——此前的 _wfopen_s 按文档以独占方式打开,游戏运行期间任何人都无法读取本项目自己的日志。实机验证:四把武器五轮,武器系技能均正确归位,单轮占比 96.5%。
2026-09-17§3.9 战斗统计面板新增武器系技能的伤害归属(Detour 由 2 增至 5):新挂 CSkillEvent 挥击执行体 sub_6C9CC0(自 CSkillEvent+0x14 取来源 CSkill*,施法者取 +0x34/+0x2C 并处理 USE_MASTER_FOR_ATTACKS 换主人),以及 CMissile::addBehaviour sub_5835B0 与 CEquipment 的 iMissile 槽 4 sub_57D690 一对,用于把武器发射的抛射物(弓/弩/枪)在发射瞬间打标、命中时把技能上下文接回。动因:TL2 近战/远程技能的伤害主力是事件块上的 WEAPONDAMAGEPCT,该链不经 executeInstantEffect,出厂技能 dat 实测 3034 份出伤技能中 2012 份属此类,此前全部误计入「普通攻击」。三条新签名在零售 1.25.9.5 与 Steam 1.25.5.6 双 build 上经 tools/aob_check.py 实测唯一命中且落点为函数头(零售 0x6C9CC0 / 0x5835B0 / 0x57D690,Steam 0x6CC190 / 0x585080 / 0x57F160)。三者均为可选增强,未命中仅令对应类别退回「普通攻击」,不影响面板其余功能。已知不覆盖:反射回去的抛射物(CMissile+0x304 分支自行挥击,与效果 61 同源)。
2026-09-11新增 §3.9 战斗统计面板 combat_meter(DPS meter):新挂 applyArmorSoakAndAbsorb 与 executeInstantEffect 两项 Detour 取伤害类型与技能来源,并复用既有的 modifyHealth / consumeAbsorbBuffers / rollCritical 三处 Detour 取实扣伤害、持续伤害逐通道值与暴击标志;伤害按来源、技能、伤害类型、目标四维分桶,热键 End。两条新签名在零售 1.25.9.5 与 Steam 1.25.5.6 双 build 上经 tools/aob_check.py 实测唯一命中且落点为函数头。配套在 hooks.cpp 增补三个只读取名接口(单位显示名、技能 dat 节点、节点 DISPLAYNAME),均复用既有的 GUID→定义节点与内存翻译表基建,不新增 Hook。
2026-09-05本规范技术账本正式确立。新增 §3.2 技能冷却词条修复模块 skill_cd_fix(包含两处内联补丁桩与三项函数体全量自校验;此前以独立 Exe 补丁实测通过并移植),相关挂载在工作区处于就绪状态。
2026-09-04新增 §3.4 外观槽多人同步兼容模块 cosmetic_mp(包含 1 项入口 Detour 与 3 处条件分支 call 改向,原版数据环境下保持逐位等价)。
2026-09-01新增 §3.3 DoT 伤害护盾吸收结算修正机制(挂载 absorb 与 tick 两项 Detour,由配置项 dotShield 门控)。
2026-08-29存盘/读档 OOM 崩溃保护调整为默认开启且脱离对启动器的依赖;WASD 模式迁移至游戏内置面板控制,底层 Hook 重构为延迟懒装载;配置面板新增「存为默认」持久化功能。
2026-08-23打造系统扩展神明祝福与神明涤除两大菜单,接入 addRandomAffixes 与 addGold 接口;挂载 Tooltip 主区文本生成器 Detour(祝福词缀单独渲染为墨绿色);接入 endNPCConversation 解决镜头特写卡死;神明祝福调用链剥离冗余的 recomputeStats。
2026-08-22新增 §3.8 词缀迁移与词缀剥离模块(挂载 updateCanCombine、doCombine 与 setToggled 三项 Detour,接入十余项原生引擎原语)。
2026-08-19ImGui 面板重构接入游戏原生九宫格金边纹理(ui_frame);修复装备账本锻造次数在 Tooltip 中延迟刷新的显示缺陷。
2026-08-18VFS 耗时分析探针重构为纯按需启用机制(默认状态零 Hook 注入);日志管线规范化为定长前缀、严重性分级与模块标签体系(全量重构 219 处调用点)。
2026-08-17新增 §3.5 unittype 全量扫描无用拷贝剔除补丁(数据加载节省约 0.68 秒);修复 findFile 两项辅助特征在不同游戏版本上的重定位失效缺陷,改为从主函数体内动态解析相对偏移;性能优化模块彻底剥离运行期影子验证机制。
2026-08-16调整死槽 131 号段划分规范,将消耗品卷轴与装备持久化账本物理拆分为独立子类型(9 与 10 号分配给卷轴,11 与 12 号分配给装备账本)。
2026-08-14新增 §3.8 诅咒打造卷轴体系(挂载 ApplyDynamicEffect 与 loadFromSaveRecord)。
2026-08-12新增 §3.5 VFS 虚拟文件系统目录检索哈希二分索引补丁(MOD 密集环境下加载节省约 1.09 秒)。
2026-08-08新增 §3.11 地形可行走网格分析探针 gridprobe(集成 F9 数据导出与 F10 场景透视投影);彻底移除宠物嘲讽的 T 键物理快捷键,收拢技能施放为唯一权威入口;最近掉落列表中 UNITTYPE 字段恢复显示原始定义名称。
2026-07-30全链路帧耗时剖析器 profiler 归档入库(源码标记其于 07-25 结案停用、07-29 短暂开启后复原,当前维持停用状态);传奇装备边框流火动效正式生效。
2026-07-24掉落物生命周期过滤系统引入白名单机制(出厂预置 SOCKETABLE 与 AMULET)与分级过滤判定管线。
2026-07-23架构重大演进:全面废除硬编码静态 RVA,全量迁移至 AOB 动态字节特征码扫描,彻底修复跨版本兼容性缺陷(宠物嘲讽在 Steam 版本由必崩转为稳定可用)。
2026-07-22新增角色面板减伤上限数值同步 Detour;修复暴击伤害与闪避率上限 Hook 因特征码硬编码静态字符串地址在 ASLR 下从未成功装载的历史缺陷。
2026-07-21新增 §3.9 装备 Tooltip 宝石插孔发光动效(青莲法阵系列)。
2026-07-20新增 §3.10 WASD 平滑键盘移动操控系统。
2026-07-17新增减伤、暴击伤害、闪避率与格挡率四大核心数值上限突破机制(承载于 131/4~7),格挡率绝对上限纠正为 85%;引入格挡灵石部位限制;宠物嘲讽正式支持通过技能施放事件触发。
2026-07-16新增暴击吸血机制(死槽 131 的首个功能落地)、宠物嘲讽光环与 Effect 门控校验;确立死槽 131 + SUBTYPE 多路复用架构标准,冷却缩减由原 SOCKS(Type 133)收拢归入 131/3;实现 Tooltip 词缀按子类型分流渲染。
2026-07-12跨关卡切图掉落过滤补全 CTriggerUnit 与 CBreakable 实体排除规则(彻底根治任务门开启状态丢失被误锁的缺陷)。
2026-07-11掉落过滤系统支持 MOD 动态稀有度与单位类型的运行时自动发现(基于注册表 BST 检索)及任务道具全局豁免机制。
2026-07-04新增 §3.2 存盘/读档 OOM 致命内存溢出崩溃修复补丁;开源代码仓库正式由 tl2-loot-filter 战略更名为 tl2-mikuro-runtime。
2026-06-25项目首版正式发布:基于 d3d9.dll 动态库代理架构,实现道具落地拦截 Detour 与 ImGui 覆盖层管线(提供中英双语支持),集成 GitHub Actions 自动化发布流程。