分析对象:Torchlight2.exe(32-bit,静态基址 0x400000)
核心定位:梳理并界定直连 combatLog.Log 的外层调用者,明确其在伤害减免、网络同步、反射计算与效果派发管线中的真实上下文。

1. 结论速览 ​

在早期的调用点清单中,sub_52CAF0 曾被列为待分析项,经核对其已确认为 CUnit__applyNetworkHealthDelta。本篇重点收拢并确认另外四个常驻写入 combatLog 的外层函数:

原地址建议名称核心职责置信度
0x452CE0NetUnitfxBlocked__applyShieldDamage处理 UNetUnitfxBlockedMsg,扣减/摧毁可破坏盾效果高
0x523CF0CCharacter__rollMissileReflect汇总反射概率与比例,掷骰成功后返回反射倍率高
0x7B67F0CStat__logValue按 bool/int/float 类型把 CStat 当前值写入 combatLog高
0x7BFD10CTriggerableEffectAndAffixes__applyTo将 triggerable bundle 中的 affix 与 effect 派发到目标单位高

这四个函数并非单一伤害公式的连续流水线阶段,而是分别对应不同的观察与触发维度:

  • 0x452CE0 属于网络消息消费侧;
  • 0x523CF0 属于命中/投射物反射决策;
  • 0x7B67F0 是CStat 调试记录,并非战斗数值结算;
  • 0x7BFD10 是触发式效果和词缀派发器。

因此,combatLog 写入点应理解为引擎关键生命周期节点的“事件观察入口”,不可将所有调用者强行拼装为单一线性的计算管道。

2. 清单纠错与函数边界 ​

当前 IDA 边界为:

函数区间(尾地址不含)大小
0x452CE0[0x452CE0, 0x453422)1858 B (0x742)
0x523CF0[0x523CF0, 0x524027)823 B (0x337)
0x7B67F0[0x7B67F0, 0x7B6A1C)556 B (0x22C)
0x7BFD10[0x7BFD10, 0x7C054B)2107 B (0x83B)

旧 G_effects_affixes_stats.funcs.json 曾把前三个相关大小记为 2045、647、2192 B。那是旧边界快照,本轮已按当前 IDA 边界并用本地 x86 指令长度解码器逐条走到尾地址。

3. 0x452CE0:网络 blocked 消息与可破坏盾 ​

3.1 身份不是推测 ​

0x452CE0 只有一个调用者:0x4569D0 的 24 字节包装器。更关键的是,包装器地址 0x4569D0 存在于如下具名 RTTI 消息表项中:

text
MessageTableEntry<UNetUnitfxBlockedMsg, BlockedMessageHandler>

消息表位置是 0x2146E50,handler 指针位于 0x2146E58;包装器在 0x4569DD 用 E8 FE C2 FF FF 调用 0x452CE0。这使“网络侧 blocked 事件”成为符号支撑的结论,而不是由日志文字猜出的名字。

3.2 消息字段 ​

函数按以下固定偏移读取消息体:

消息偏移证据用途
+0x100x452D0B: 8B 4E 10查找 level/scene
+0x18,+0x1C8B 46 18 / 8B 56 1C第一组 64-bit 单位 GUID
+0x200x452EF0: DB 41 20作为整数伤害装入 x87,用于比较和扣盾
+0x28,+0x2C8B 56 28 / 8B 4E 2C第二组 64-bit 单位 GUID
+0x300x452F07: 80 79 30 00非零时强制进入破盾分支

这里只给两组 GUID 编号,不冒充已确认的“攻击者/受击者”字段名;函数内的使用关系支持受击对象与关联对象,但消息结构声明本身尚未恢复。

3.3 核心流程 ​

函数先解析 level 与两个单位,然后从装备容器取槽位 2 的物品。它要求物品通过 unit-type 0x15 检查,并遍历其效果,寻找:

text
effect.type == 163

FULL_LOGIC 类型表把 163 命名为原版拼写 DESTRUCTABLE SHIELD。命中该效果后,slot 1 是当前盾值:

cpp
void applyBlockedMessage(NetUnitfxBlockedMsg& msg) {
    Unit* unit = level.findUnit(msg.guidA);
    Unit* other = level.findUnit(msg.guidB);
    Item* shield = unit->inventory->getAtEquipLocation(2);

    Effect* e = findActiveEffect(shield, 163); // DESTRUCTABLE SHIELD
    float shield_hp = e->evalSlot(1);
    int damage = msg.damage_at_0x20;

    if ((double)damage > shield_hp || msg.force_break_at_0x30 != 0) {
        combatLog(unit->name + "(" + damage + ") Damage Blocked! Shield destroyed!");
        msg.damage_at_0x20 = 0;
        e->setSlotAndReroll(1, 0.0f);
        detachAndDestroyShield();
        play("HIT");
        play("Shattered");
    } else {
        e->setSlotAndReroll(1, shield_hp - damage);
        combatLog(unit->name + "(" + damage + ") Damage Blocked! "
                  + (shield_hp - damage) + " shield hp remaining.");
    }
}

破盾分支会执行模型/装备解绑、受击表现、HIT 与 Shattered 标签以及清理;存活分支只更新效果 slot,并可发浮动 UI。

3.4 与 rollBlockParryShatter 的关系 ​

0x452CE0 和 CUnit__rollBlockParryShatter 会输出相同的两类字符串,但前者是 UNetUnitfxBlockedMsg 的接收与表现层。这也解释了为何表现相同的战斗日志存在两处独立的外层触发点:单机本地判定与网络同步重放分属不同执行上下文,在挂接 Hook 时不可混为一谈。

4. 0x523CF0:反射概率与反射比例是两列数据 ​

4.1 输入汇总 ​

函数维护两个浮点累计量,并先后调用:

text
0x523D3E  E8 CD A9 28 00  CEffectMgr__accumMissileReflect
0x523D57  E8 F4 46 2A 00  CInventory__accumMissileReflect

第一列是触发概率 chancePct,第二列是成功后的反射量 amountPct。这两个值不能合并成单一“反射百分比”。

4.2 掷骰与返回值 ​

chancePct <= 0 直接返回 0。否则:

cpp
float CCharacter::rollMissileReflect(/* context */) {
    float chancePct = 0.0f;
    float amountPct = 0.0f;

    effectMgr.accumMissileReflect(&chancePct, &amountPct);
    inventory.accumMissileReflect(&chancePct, &amountPct);

    if (chancePct <= 0.0f)
        return 0.0f;
    if (rand_float_between(0.0f, 100.0f) > chancePct)
        return 0.0f;

    combatLog(name + " Reflected!");
    showReflectedTextAndEffects();
    return amountPct * 0.01f;
}

关键证据:

  • 0x523D7D 调 rand_float_between;
  • 0x523D82 把随机值与 chancePct 比较;
  • 0x523DE8 写 Reflected!;
  • 0x523FF5 执行 fmul [0x2142654];该常量的原始位是 0x3C23D70A,即单精度 0.01。

所以函数返回的是 amountPct / 100,不是布尔值,也不是原始百分数。

4.3 两个调用者 ​

调用点所在路径返回值用途
0x5489BFCCharacter__resolveStrikeHit保存 x87 返回值并要求 >0 后继续反射分支
0x6E5882missile 碰撞/更新路径写入 missile +0x300 与 +0x2E4,并把 +0x354 设为 3

第二个调用者直接证明这不是泛化的“受到攻击时随机特效”;返回倍率会成为投射物后续状态的一部分。第一条调用路径仍需继续追到最终反伤公式,才能确认 strike 分支如何消费倍率。

5. 0x7B67F0:这是 CStat 日志,不是 effect dump ​

5.1 类身份交叉证据 ​

调用链 sub_7BD620 → sub_7BBBE0 → 0x7B67F0 中,sub_7BBBE0 会调用具名函数 CStatRegistry__findDef。若对象尚不存在,它调用 sub_7B6A20 构造对象;该构造器在 0x7B6A4C 写入:

text
C7 06 18 AC 19 02  mov dword ptr [esi], CStat::vftable

因此建议名称是 CStat__logValue。旧档案中的“效果调试转储”已纠正。

5.2 GUID 哨兵与类型分派 ​

函数先检查 COMBAT LOG,再用以下三条指令判 GUID:

text
0x7B6829  8B 4E 60  mov ecx,[esi+60h]
0x7B682C  23 4E 64  and ecx,[esi+64h]
0x7B682F  83 F9 FF  cmp ecx,FFFFFFFFh

若 (guid0 & guid1) == 0xFFFFFFFF,函数记录 INVALID_GUID。否则读取 this+0x90 的值种类:

kind读取格式化
0movzx 低字节sub_6863A0,已反编译为 true / false
1dwordintToWString
2floatsub_685FE0
其他不进入三条格式化支路保持空值文本

正常行在 0x7B698D 写入,形状是 <stat name> value: <formatted value>;无效 GUID 行在 0x7B6A00 写入。

这个函数属于调试/诊断面,不应作为伤害公式或 stat 更新钩点。若要拿原始 stat 值,应钩 sub_7B5F00 的返回或真正的写入路径,而不是解析这条日志文本。

6. 0x7BFD10:CTriggerableEffectAndAffixes 双队列派发器 ​

6.1 类身份与容器布局 ​

相邻的构造/拷贝路径 0x7C0590 在 0x7C05B9 写入具名虚表:

text
C7 06 DC B0 19 02  mov dword ptr [esi], CTriggerableEffectAndAffixes::vftable

同一路径还显式构造 CSkillAffixData 与 CSkillEffectData,与 0x7BFD10 的两段循环完全对应:

对象偏移容器
+0x0C/+0x10/+0x14affix-data vector:pointer / size / capacity
+0x1C/+0x20/+0x24effect-data vector:pointer / size / capacity

6.2 第一段:实例化并挂接 affix ​

每个 affix 条目先做目标/关系过滤和 CUnit__hasUnitType 门控。通过后:

  1. 0x7BFFD1:createAffixByNameAndLevel;
  2. 0x7BFFE6:CAffix__setSource;
  3. 0x7C001F:CAffix__setLevel;
  4. 0x7C002E:写入传入位置/上下文;
  5. 0x7C0070:若条目时长大于 0,CAffix__setDuration;
  6. 0x7C01D4:可选 combatLog,形状为 affix <name> -> <target>;
  7. 0x7C0243:CEquipment__attachAffix。

函数在至少一条 affix 成功挂接时设置返回标志。

6.3 第二段:直接应用 effect ​

每个 effect-data 条目可含多个 CEffect。流程如下:

cpp
for (effectData : effectVector) {
    if (!target->hasUnitType(effectData.requiredUnitType))
        continue;
    if (!bypassTargetSelector && !relationMatches(effectData.selector, ...))
        continue;

    for (Effect* e : effectData.effects) {
        if (position)
            e->position = *position;

        e->setSource(source, true);

        bool temporarilyFilledLevel = (e->level == 0);
        if (temporarilyFilledLevel) {
            e->level = (level >= 0 && level <= 1000) ? level : 0;
            e->rollAndCacheValue(0, 0);
        }

        if (target->vfunc_0x150(context, context, e, true))
            appliedAny = true;

        if (temporarilyFilledLevel) {
            e->level = 0;
            e->rollAndCacheValue(0, 0);
        }
    }
}
return appliedAny;

0x7C04A4..0x7C04C5 的无分支位运算把负数或大于 1000 的 level 变为 0;0x7C04F5 从目标单位虚表 +0x150 读取应用入口,0x7C0504 调用后以 AL 判断成功。临时补 level 的效果会在 0x7C0537 恢复为 0 并重新 roll/cache。

6.4 目标 selector ​

两段循环都处理 selector 0/1/2/4/5/6,比较传入的目标、source/owner 和单位 +0x71C 指向的关系对象;参数 a5 非零时会绕过 selector 过滤。

正式枚举标签尚未从 RTTI 或字符串表恢复,所以本报告不把这些数字强行命名为 SELF、OWNER、PET 等。重制实现可以先按现有比较关系复刻,但公开 API 最好等枚举名补齐后再定名。

7. 对重制与钩子的直接建议 ​

7.1 事件边界 ​

建议拆成四类事件,而不是统一成一条字符串日志:

text
ShieldBlocked/ShieldDestroyed
MissileReflectRolled(chancePct, amountPct, succeeded)
StatDebugValue(statGuid, kind, rawValue)
TriggerableBundleApplied(source, target, affix/effect, applied)

这能保留原版日志已经丢掉的原始值,也能明确区分网络表现、随机判定、调试转储与效果派发。

7.2 推荐钩点 ​

需求推荐点原因
观察网络破盾0x452CE0 入口及 0x452EF0/0x45321A可同时拿消息字段、旧盾值和分支结果
观察反射掷骰0x523D7D 前后与 0x523FF5能分开记录 chance、roll、amount 与最终倍率
观察 stat 原值sub_7B5F00 返回0x7B67F0 已把值格式化为文本
观察 effect/affix 派发0x7BFFD1、0x7C0243、0x7C0504分别覆盖创建、挂接、直接 effect 应用结果

所有硬编码地址都应先校验对应机器码。跨 Steam/非 Steam build 时优先使用经唯一性验证的 AOB,而不是直接复用 VA。

8. 尚未闭环的边界逻辑 ​

当前逆向分析明确保留三项待深入点:

  1. UNetUnitfxBlockedMsg +0x30 的生产端逻辑尚未完全覆盖;目前已确认为非零强制触发破盾分支。
  2. 0x523CF0 的倍率传入 CCharacter__resolveStrikeHit 后,其最终对伤害减免与反伤数值的具体折算尚待结合击中结算主流程进一步追踪。
  3. CTriggerableEffectAndAffixes selector 0/1/2/4/5/6 的底层字符串/枚举标识尚未完全对齐。

上述开放项不影响整体核心结论:四个 combatLog 外层函数的真实身份、主流程、调用链及关键数据字段均已闭合,并由客户端发行版的指令序列提供确定性保护。

9. 自动化验证覆盖 ​

配套回归测试覆盖以下核心事实:

  • 四个外层函数的真实边界与逐指令控制流;
  • 全部关键机器码特征锚点;
  • E8 rel32 的目标跳转地址;
  • UNetUnitfxBlockedMsg 消息处理虚表指针;
  • 反射换算常量单精度 0.01f;
  • CStat 与 CTriggerableEffectAndAffixes 的具名虚表结构。