本文回答什么[EFFECT] 块写成某个样子,游戏里到底会发生什么。 239 个 TYPE 的数值槽对照表 解决的是「能填哪些键」,本文解决的是「填了会怎样」, 239 个 TYPE 的实例速查 则给出每个类型的真实数据样例。

分析依据:基于 IDA 对 32 位可执行文件 Torchlight2.exe(基址 0x400000)的逐指令反汇编分析。 两个分派器的字节索引表与跳转表已逐字节读出并交叉校验(表长与 case 数完全吻合), 所以「哪些类型有分支、哪些落空」可证。消费方一侧是逐调用点回读得到的,完整度见第 8 节。

1. 判定框架:六条路径

一个效果挂上去之后,引擎里只有六个地方会因为它的 TYPE 做出不同的事。

#路径入口覆盖
1数值查询CUnit__getStat (0x518850) → CEffectMgr__sumStatOfType_filtered (0x7AEA40)已定位 128 个类型
2直接扫表CEffectMgr__accumMissileReflect (0x7AE710)1 个类型
3布尔/查找查询CUnit__hasActiveEffectOfType (0x512C90)、CEffectMgr__findEffectByTypeAndName (0x7ADEE0) 等16 个类型
4INSTANT 执行CUnit__executeInstantEffect (0x559690)56 个有分支,175 个落空
5挂载时执行CUnit__applyEffectToSelf_dispatch (0x562590)28 个有分支,194 个落空
6每帧 tickCUnit__tickActiveEffects_dispatch (0x566DD0)45 处比较,45 个类型

IMPORTANT

路径归属就是「DURATION 该怎么填」的答案。

  • 只有路径 1/2/3 → 效果必须活着才有用:给持续时长,或挂在装备/词缀上当永久加成。写 INSTANT 等于没写。
  • 只有路径 4 → 写 INSTANT。给它时长不会让它反复触发。
  • 有路径 6 → 需要持续时长,存活期间按帧被检查。
  • 有路径 5 → 挂上去那一刻执行一次,与时长无关。

1.1 两个跳转表是怎么解出来的

两个分派器都编译成 MSVC 的「字节索引表 + 跳转表」双层结构,可以直接读表得到完整映射。

asm
; INSTANT —— CUnit__executeInstantEffect
0055971C  mov   eax, [edi+1Ch]              ; eax = eff->TYPE
0055971F  add   eax, 0FFFFFFFAh             ; -6        ← 覆盖 TYPE 6..236
00559722  cmp   eax, 0E6h
00559727  ja    def_559736
0055972D  movzx eax, ds:byte_55CB6C[eax]    ; 231 项字节索引表
00559736  jmp   ds:jpt_559736[eax*4]        ; 54 项跳转表,default 槽位 = 53

; 挂载 —— CUnit__applyEffectToSelf_dispatch
005626E2  mov   ecx, [edi+1Ch]
005626E5  add   ecx, 0FFFFFFF9h             ; -7        ← 覆盖 TYPE 7..228
005626E8  cmp   ecx, 0DDh
005626EE  ja    def_5626FB
005626F4  movzx ecx, ds:byte_564484[ecx]    ; 222 项字节索引表
005626FB  jmp   ds:jpt_5626FB[ecx*4]        ; 27 项跳转表,default 槽位 = 26

表长与 case 数完全对得上(231/54、222/27),无剩余、无越界。 tick 分派器没有跳转表,是一长串 cmp dword ptr [reg+1Ch], <类型号> 的 if 链。

2. 路径 1:数值查询

效果挂上时,数值被累加进 CEffectManager+0x60float statCache[239],下标就是类型号—— 这一步对所有类型无差别执行。真正决定行为的是有没有代码去查。

查询走 CUnit__getStat(类型号, 伤害类型, 是否向上取整),全二进制 371 个调用点。 调用约定是 push 取整标志; push 伤害类型; push 类型号; call——紧邻 callpush 就是类型号

CUnit__getStat 外还有三个包装器直接调 sumStatOfType_filteredCBaseUnit__getEffectStat_selfPlusInventory0x518930,16 个调用点,15 个在 CCharacter__resolveStrikeHit 里)、 sub_5188F0sub_512C40漏掉这几个包装器会让一整批类型看起来「没人用」—— 吸血、偷蓝、击退、腐蚀护甲全在那 16 个点里。

WARNING

sumStatOfType_filtered 有三条需要知道的行为:

  1. 伤害类型 == 7 是「不过滤」哨兵(合法值只有 0–6)。走这条直接返回 statCache[类型号]不扫表
  2. statCache[类型号] == 0 时直接返回 0,同样不扫表。缓存陈旧时带过滤的查询会返回 0。
  3. 只扫 PASSIVE 与 DYNAMIC 两张表,TRANSFER 不计入(见 CEffectManager)。

2.1 伤害加成是七个桶,不是一个数

10 DAMAGE BONUS25 PERCENT DAMAGE BONUS 按伤害类型 0–6 各记一份账, 效果的 DAMAGE_TYPE 决定进哪个桶,伤害结算逐桶取:

asm
0052DDC1  push 0 / push 6 / push 19h  → getStat(25, 6, 0)   ; 桶 6(通用)
0052DDDC  push 0 / push 0 / push 19h  → getStat(25, 0, 0)
0052DE17  push 0 / push 2 / push 19h  → getStat(25, 2, 0)
0052DE69  push 0 / push 3 / push 19h  → getStat(25, 3, 0)
0052DEBB  push 0 / push 4 / push 19h  → getStat(25, 4, 0)
0052DF0D  push 0 / push 5 / push 19h  → getStat(25, 5, 0)
0052DFAD  push 0 / push 2 / push 0Ah  → getStat(10, 2, 0)   ; DAMAGE BONUS 同理

DAMAGE_TYPE 写错等于把加成放进没人取的桶。26 PERCENT DAMAGE TAKEN 的读取方 CUnit__getDamageTakenPct_stat260x51ADF0) 是「按传入伤害类型查一次 + 桶 6 查一次」,桶 6 就是那个「对所有元素通用」的桶。

2.2 近战与远程是一对镜像函数

sub_52DA30sub_52E9D0 各 0xF95 字节,结构逐行同构,区别只在基础伤害类型: 近战读 getStat(0, …)MELEEDAMAGEBONUS),远程读 getStat(1, …)RANGEDDAMAGEBONUS0x52F53Dpush 0; push 7; push 1)。 sub_52BD90 / sub_52C170 是另一对同样的镜像。改一边不改另一边,近战和远程的行为就会不一致。

2.3 物品需求减免是按 UNITTYPE 选型的

sub_57D930(另有 0x57DA300x57DB300x57DC30 三份副本)先无条件取 93 REDUCED ITEM REQUIREMENTS,再按物品的 UNITTYPE 追加一个专用类型:

物品 UNITTYPE追加的类型
0xA2 (162)98 MARTIAL ITEM REQUIREMENTS
0xA3 (163)100 RANGED ITEM REQUIREMENTS
0xA4 (164)101 MAGIC ITEM REQUIREMENTS
0x0D (13)94 ARMOR ITEM REQUIREMENTS
0x81 (129)95 SHIELD ITEM REQUIREMENTS

最后 需求 = max(0, 原需求 − 减免合计)。 注意 34 PERCENT ITEM REQUIREMENTS 不在这条链里,它没有任何消费方(见第 8 节)。

2.4 抗性间址表把「全部抗性」错读成类型 0

CBaseUnit__getResistanceOfType(idx)0x520A40)按 idx 取一个类型号:

dword_216FD9C = { 35, 36, 37, 38, 39, 40, 0 }
                   ↑物理 ↑魔法 ↑火  ↑冰  ↑电  ↑毒  ↑ idx==6「全部」

前六项是对应的 * DEFENSE 类型,第七项是 0,也就是 MELEEDAMAGEBONUS。 所以查询「全部抗性」时,引擎把这个单位的近战伤害加成当成抗性加了进去。这是错位,不是设计。

另外这个函数里的 switch 映射也不按 idx 递增: case 2 → 219 INNATE FIREcase 3 → 218 INNATE ICEcase 4 → 220 INNATE ELECTRICcase 5 → 221 INNATE POISON

2.5 DAMAGE_TYPE 只有六个类型真的会读

DAMAGE_TYPE 是数值查询路径的过滤参数,存在 CEffect+0x14。 它不是每个效果都有用——绝大多数类型写了等于没写。先看这个键是怎么进来的。

七个合法值,运行时构造。 名字表在 byte_3B9CAB0,由静态构造函数 sub_1F876C0 填:

序号名字序号名字
0Physical4Electric
1Magical5Poison
2Fire6All
3Ice

7 不是合法值,是「不过滤」的哨兵。CEffectMgr__sumStatOfType_filtered0x7AEA40)第一行就是 if (damageType == 7 && !onlyLocalSource) return statCache[typeId];0x7AEA53)—— 直接返回 239 缓存、连表都不扫。绝大多数 getStat 调用传的就是 7

解析过程CEffect__ctorFromDat 0x7AB479 起):

默认值 "PHYSICAL" → getString("DAMAGE_TYPE") → toUpperW

if (var_1F9)  { +0x14 = 1 /*Magical*/; 跳过解析 }      ← 0x7AB4DB
else          逐项比 7 个名字(cmp ebp, 0C4h = 7 × 28)
                命中 → +0x14 = 序号                    ← 0x7AB545
                全不命中 → **不写 +0x14**              ← 落 0x7AB548

两处细节都要紧。先说第一处。

九个类型被强制成 Magical,键根本不读

var_1F90x7AAC03 的跳转表决定。这张表本身已经在 数值槽对照表 §7 的「特判」列里记过(代号 M, 那边引的是索引计算处 0x7AABEF)——本节补的是它的逐字节解码与数据侧后果

它是本系列解出的第四张跳转表(前三张是 INSTANT、挂载、施法模式): 索引表 idx_forceMagical_1170x7ABB54)共 117 项、索引式为 TYPE - 2(覆盖 TYPE 2..118), 跳转表 jpt_7ABB4C 两项:slot 0 = 0x7AAC0A(置位)、slot 1 = 0x7AAC0F(默认,不置位)。 逐字节解出落 slot 0 的九个类型,与 IDA 解析的 case 列表一致:

#TYPE数据里写了 DAMAGE_TYPE 的块数(原版 / MOD)
2DEFENSE0 / 0 ——(无人写)
17PERCENT DEFENSE0 / 6
35PHYSICAL DEFENSE0 / 0 ——(无人写)
37FIRE DEFENSE43 / 298
38ICE DEFENSE42 / 302
39ELECTRICAL DEFENSE43 / 314
40POISON DEFENSE38 / 349
86DEFENSE SPELL BONUS0 / 0 ——(无人写)
118PERMANENT DEFENSE0 / 0 ——(无人写)

九个全是 DEFENSE 家族。对它们,+0x14 被无条件写成 1(Magical), DAMAGE_TYPE: 那一行连读都不读。 原版自己在这九个类型上写了 166 处 DAMAGE_TYPE,MOD 库 1269 处,合计 1435 处全部无效

NOTE

这条也顺带解释了一处死代码。CUnit__getEffectiveVitality0x51B040)里有两次读:

v6 = getStat(17, 7, 0)   // 不过滤,拿走全部 PERCENT DEFENSE
v2 = getStat(17, 6, 0)   // 只要 DAMAGE_TYPE == 6(All)的那些
体力 = base + ceil((v2 + v6) * 0.01 * base)

类型 17 恰好在上面那九个里,所有 17 号效果的 +0x14 都被强制成 1。 于是 getStat(17, 6, 0) 永远匹配不到任何东西、恒返回 0。 这不是「写 DAMAGE_TYPE:ALL 会被算两遍」,而是第二次调用整个是空转。

值写错了,+0x14 干脆不被写入

匹配循环跑完七项都没命中时,0x7AB545mov [esi+14h], eax 被跳过。 基类构造 sub_4059100x405910)只设 vftable 和 this[1],也不碰 +0x14; 函数内对 [esi+14h] 的写入总共只有两处(0x7AB4DB0x7AB545)。 未追分配器,所以无匹配时该字段的实际取值取决于这块内存原本是什么。

实测数据里出现的非法值:

出现在原版MOD 库影响
MAGIC52 DAMAGE0168该类型真读 —— 合法名字是 MAGICAL
MAGIC26 PERCENT DAMAGE TAKEN08该类型真读
ELECTRICAL39 ELECTRICAL DEFENSE010该类型本就强制 Magical
ELECTRICAL102502该类型真读 —— 合法名字是 ELECTRIC
空值2255254225 那条该类型真读
POSION / POITION / POSITION52253804拼错 POISON

DAMAGE_TYPE:MAGIC 是这里面最值得说的:合法名字是 MAGICAL, 少一个 AL 就整条匹配不上。MOD 库里 168 个 52 DAMAGE 块这么写—— 而 52 是真的会按伤害类型分桶的类型(CEquipment__bakeDamageEffectsIntoTable)。

六个真读的类型

其余情况下 +0x14 只有在消费方传了非 7 的过滤值时才有意义。逐调用点核实后, 只有六个类型存在这样的消费方:

#TYPE读取点数据里的用量(原版 / MOD)
8ARMOR BONUSCBaseUnit__getResistanceOfType 0x520AF6(变量)/0x520B216);另 0x5216F653 / 1350
10DAMAGE BONUS近战/远程两对镜像函数共 8 处,如 0x52E0570x52E68A97 / 3489
23PERCENT ARMOR BONUSCBaseUnit__getResistanceOfType 0x520AB8(变量)/0x520ACC656 / 2050
25PERCENT DAMAGE BONUS两对镜像 + 挂载分派共 16 处,如 0x52BF4E0x563DD4327 / 6759
26PERCENT DAMAGE TAKEN0x51AD810x51AE100x522AF1325 / 5628
52DAMAGE不走 getStat —— CEquipment__bakeDamageEffectsIntoTable0x590BB0)直接读 +0x14999 / 17150

823 的用法见 2.4 节的 getResistanceOfType——每种抗性都做两次读: 一次按具体伤害类型(变量),一次按 6(All),两者相加。 所以在护甲类效果上写 DAMAGE_TYPE:FIRE 只加到火抗,写 ALL 才是全抗。

汇总

两库合计 48263 个 [EFFECT]写了 DAMAGE_TYPE,分布在 101 个类型上:

分类类型数原版MOD 库合计
真的会读618573642638283
被强制成 Magical、键不读516612691435
键进了 +0x14,但没有消费方9037281738545
合计10123954586848263

也就是说 9980 个块的 DAMAGE_TYPE 是白写的, 其中原版自己占 538 处。最典型的是 37 FIRE DEFENSEDAMAGE_TYPE:FIRE—— 原版写了 43 次,一次都不生效。

WARNING

「没有消费方」这一列的完整度受扫描方法限制。判定依据是: 逐字节找出所有 push 6; push <类型> 形态的 getStat 调用并验证调用目标(这是「All 桶」的固定写法,过滤式读取必然成对出现其一), 加上直接读 +0x14CEquipment__bakeDamageEffectsIntoTable, 以及 0x7AAC03 那张强制表。 若某个类型的消费方用了别的写法(例如伤害类型全程只以寄存器传递、且不配 6 那一次), 本次可能漏掉。这一列应读作「本次未找到消费方」。

2.6 [EFFECT] 块里写了、但没人读的键

上一节只查了 DAMAGE_TYPE 一个键。同样的问题对所有键都成立,索性一次查完。

哪些键会被读,有两个来源:

  1. CEffect__ctorFromDat0x7AAA300x7ABB4B)里所有 push offset 的键名字面量,共 24 个
    TYPE ACTIVATION DURATION NAME LEVEL ICON LINK_NAME PARTICLE_FX UNITTHEME
    SAVE EXCLUSIVE NONMAGICAL REMOVEONOWNERDIE USEOWNERLEVEL DISPLAYPOSITIVE
    MODIFIERISBONUS DISPLAYMAXMODIFIER NOGRAPH GRAPHOVERRIDE SOAKSCALE
    DAMAGE_TYPE STATMODIFYNAME STATMODIFYPERCENT STAT_SOURCE_TYPE
  2. EFFECTSLIST.DAT 里那一行给出的五个槽位别名(239 行去重后共 62 个不同名字, 见 数值槽对照表)。

数据侧:两库的 [EFFECT] 块里一共出现过 98 种键名,其中 84 种能对上上面两个来源, 14 种对不上。逐个查证后分成四类:

甲、二进制里根本没有这个字符串 —— 必然无效

宽窄两种编码都搜过,全码段无此串,所以不可能有任何代码读它。

出现次数备注
EFFECTSHIDDEN32
DAMAGETYPE27少一个下划线。二进制里只有 PRIMARY_DAMAGETYPE0x218E57C 那处匹配是它的尾巴
EXCLUS0VE5EXCLUSIVE 把字母 I 打成了数字 0
DONTPLAYPARTICLES3
MINCHANCE2
MAXCHANCE2
空键名3<FLOAT>:值 这种写法

合计 74 处

CAUTION

DAMAGETYPE 这 27 处全部落在 52 DAMAGE 上、值全是 ELECTRIC,而且没有一个同时写了正确的 DAMAGE_TYPE。其中 2 处是原版SKILLS/CHAMPIONS/CHARGEDBOLT.DATSKILLS/PROCS/CHARGEDBOLT.DAT

后果是实打实的。键名不匹配 → getString("DAMAGE_TYPE", "PHYSICAL") 拿到默认值 → CEffect+0x14 被写成 0(Physical)。而 52 DAMAGE 的 INSTANT 分支在 0x55A957 读这个字段:

asm
.text:0055A957   mov  eax, [edi+14h]      ; 效果的 DAMAGE_TYPE
.text:0055A95E   cmp  eax, 6              ; 是 All 吗
.text:0055A961   jz   short loc_55A97D
.text:0055A965   push eax                 ; ← 不是 All,就拿它当过滤值
.text:0055A966   push 19h                 ;   类型 25 PERCENT DAMAGE BONUS
.text:0055A96A   call CUnit__getStat

也就是说这两个原版技能的伤害去查的是物理加成桶,而不是电击桶。

乙、字符串存在,但读它的函数不碰效果节点

出现次数谁在读结论
SOAKSCALEPCT30CEquipment__loadCombatStatsFromDatCSkillEvent__ctorFromDat装备与技能事件的键,效果解析器不读
VALUE319只被 EffectsList_dat_load_2390x40136C)读那是 EFFECTSLIST.DAT 的列名,不是效果块的键
VALUE12只被 EffectsList_dat_load_2390x4012BB)读同上

合计 51 处VALUE1VALUE3 这一类很好理解——把「表里那一行的列名」当成了「效果块里能写的键」。 正确写法是查该类型的槽位别名(MINMAXCHANCE/…),别名逐行不同。

丙、由 CEffect__ctorFromDat调用方在同一个节点上读 —— 有效

TARGET(848 处)不在效果解析器的 24 个键里,但它确实会被读: sub_7C0FA00x7C10B1)与 CSkillEffectAndAffixes__ctorFromDat0x6C6B8C 等三处) 都从同一个数据节点取这个键,而这两个函数正是 CEffect__ctorFromDat 的调用方。 键的归属不等于「谁 new 了这个对象」,这一类不算白写。

丁、未定论

MINLEVEL(254 处)与 MAXLEVEL(244 处)在二进制里存在, 但引用它们的只有 ItemDB__buildIndexFromDats0x65EBD10x65EC4F)与 sub_660A00, 都与效果节点无关。不过这两个键也可能是嵌套子块的键被本次扫描一并计入了 (扫描取的是 [EFFECT] 到最近一个 [/EFFECT] 之间的全部 <类型>键:值 行), 所以这里不下结论。AFFIX(1 处)同理。

NOTE

甲、乙两类合计 125 处是可以确定白写的。 相比 2.5 节 DAMAGE_TYPE 一个键就有 9980 处无效,键名写错反倒是小问题—— 真正的大头是键名写对了、但那个类型压根不读它

3. 路径 2:直接扫表

CEffectMgr__accumMissileReflect0x7AE710)不走 statCache, 自己扫 PASSIVE + DYNAMIC 两张表,只挑 TYPE == 60 MISSILE REFLECT

单条反射量 = (evalSlot(eff,2) + evalSlot(eff,3)) * 0.005     // (DMGPCTMIN+DMGPCTMAX)/2/100
合成概率   P = 1 - (1 - p_old) * (1 - p_new)                 // 独立事件叠加,永不超 100%
合成量     A = (a_old * p_old + 单条反射量 * p_new) / P       // 按概率加权的均值

NOTE

反射概率按独立事件合成,不是相加,所以堆多少件都到不了 100%。 反射量是按概率加权的平均值——低概率高反射的一件,会被高概率低反射的另一件稀释。

4. 路径 3:按类型的布尔/查找查询

这条路径只问「身上有没有这个类型的效果」,不关心数值。 入口是 CUnit__hasActiveEffectOfType0x512C90,20 个调用点)、 CEffectMgr__hasEffectOfType0x7AE100)、CEffectMgr__findEffectByTypeAndName0x7ADEE0)。

#TYPE查询点
25PERCENT DAMAGE BONUS0x5327380x5328ED0x5910600x7A64BA (CEffectMgr__findEffectByTypeAndName)
26PERCENT DAMAGE TAKEN0x5328DE
63VELOCITY MULT0x51A6310x542EC9 (CUnit__hasActiveEffectOfType)
79FLEE EFFECT0x5AFCD3
80TURN ALIGNMENT0x536B960x536C7C0x53C6BE0x53DD080x5506B8
82SKILL BONUS0x7ADC4D sub_7ADC100x7A7A90 CEffect__build_tooltip_desc
113WARP0x5C930D
122SHIELD BUFFER0x51FED8 CUnit__consumeAbsorbBuffers_type1220x525488 CUnit__canApplyEffectGate_vt83
128SILENCE0x6BFB9C CSkill__canUse —— 沉默时技能不可用
129DESUMMON ON DEATH0x53BBDC0x54E8CC CUnit__onDeath_vt120
161SPAWN UNIT ON DEATH0x53C8F7 CBaseUnit__execProcCastSkill
163DESTRUCTABLE SHIELD0x581715
166REMOVE TRIGGERABLE0x5642480x58343A
177EXPLODE ON DEATH0x54E568 CUnit__onDeath_vt120
196IMMOBILIZE0x7B9E13
238CHEATED0x5C114D0x5C119C0x5C11D70x5C122C0x5C12580x5C12AC —— 六处检查,CHEATED 并非惰性类型

NOTE

238 CHEATED 有六处检查点0x5C114D 起),它不是无用的开发期残留, 而是一个真正被读取的标记。同理 128 SILENCECSkill__canUse0x6BFB9C)里被查, 这就是沉默让技能不可用的实现。

5. 路径 4:INSTANT 执行

DURATION:INSTANT 的效果不进三张表、不参与 tick,执行一次就丢。 寄存器约定:esi = 被作用单位,edi = CEffect*56 个类型有真分支

处理地址类型关键调用做什么
0x55973D232 REMOVE EFFECTCEffectMgr__removeEffectsByName名字移除效果(不是按类型);先做两次 wstring 比较筛选
0x5597A3223 PULLCEffect__getSourceAsCharacter
CUnit__getMoveSpeedA/B
CUnit__datGetAttachesToMaster
CUnit__datDropToGround
CUnit__getStat
CUnit__applyDifficultyResistBoost
把目标朝来源拖拽;带抗性判定与移速取值
0x559ADF213 RETARGET、214 AGROCEffect__evalSlot×4
rand_float_between_seeded
CUnit__pickRandomTargetInRadius
CCharacter__setTarget
在半径内随机挑一个目标并强制换目标;213 与 214 共用同一段代码
0x559C18167 INTERRUPT抗性CCharacter__playReactionAnim×3打断:播放三种反应动画之一
0x559D3D143 RESPECCUnit__hasUnitType
__RTDynamicCast
CPlayer__respecSkills
洗点:必须能转型成 CPlayer 才生效
0x559D7F142 SWAP POS WITH PETCUnit__getMinionAt
CUnit__setPosition
与随从交换位置
0x559E45127 STOP SKILLCEffect__evalSlot
CSkillMgr__stopSkillByName
按名字停止一个技能
0x559EC274 CAST SKILLCUnit__hasUnitType
CBaseUnit__execProcCastSkill_petVariant
CBaseUnit__execProcCastSkill
施法:宠物走 petVariant,其余走主执行体
0x559F1A113 WARPgetGameClient
wstring::find/substr/length
传送:把目标串按分隔符切成关卡名与落点名
0x55A0D5114 PERMANENT STRENGTH永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
永久力量
0x55A103115 PERMANENT DEXTERITY永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
永久敏捷
0x55A131116 PERMANENT MAGIC永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
永久魔力
0x55A15F118 PERMANENT DEFENSE永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
永久防御
0x55A18D117 PERMANENT HEALTH永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
永久生命
0x55A1BB119 PERMANENT MANA永久属性CUnit__hasUnitType
CEffect__evalSlot
永久法力(少一个 ftol,直接用浮点
0x55A1EA120 AWARD STATPOINT永久属性CUnit__hasUnitType
CEffect__evalSlot
__ftol2_sse
奖励属性点
0x55A21865 LEARN SKILLCUnit__findOwningPlayer
CUnit__getOrCreateSkillManager
CSkillList__createSkill
CSkillMgr__setSkillEnabled
CUnit__canLearnSkill
CUnit__isLocallyAuthoritative
学技能:建技能对象→设为启用;受 canLearnSkill 与权威端双重把关
0x55A314222 SET SKILL LEVELCSkillMgr__findSkillByName
CSkillMgr__setSkillLevelByName
CUnit__getMinionAt×3
CUnit__datGetAttachesToMaster
设技能等级;同时遍历随从,对附属随从一并设置
0x55A486216 MODIFY SKILL COOLDOWN REMAININGCSkillMgr__findSkillByName
CSkill__setCooldownRemaining
修改冷却剩余(相对)
0x55A4E3217 SET SKILL COOLDOWN REMAININGCSkillMgr__findSkillByName
CSkill__setCooldownRemaining
设置冷却剩余(绝对);与 216 只差取值方式
0x55A53A111 FADE OUTCEffect__evalSlot
CUnit__setVisible×2
淡出:两次 setVisible
0x55A576112 SET VISIBLECEffect__evalSlot
CUnit__setVisible×2
设可见:与 111 调用序列完全相同
0x55A5B2110 KILLCUnit__isLocallyAuthoritative
CUnit__isPlayerPrimaryPet
CEffect__evalSlot
击杀:玩家主宠免疫,且只在权威端执行
0x55A621145 EXPLODE DEADCUnit__gibCorpse_DEATHEXPLODE尸爆
0x55A63145 OPEN WAYPOINT PORTALCUnit__isLocallyAuthoritative
CUnit__getLevel
CUnit__addPlayerStatistic
开传送点门户,并计入玩家统计
0x55A666141 OPEN PORTALCUnit__getLevel开门户
0x55A69657 TRANSFORMCEffect__getInternedString
CEffect__evalSlot
CUnit__doTransform
变身:目标模型名来自 intern 串表
0x55A6FB73 TRANSFORM PERMANENT永久变身:本段只做字符串准备,实体动作走共享尾部
0x55A72A67 DESUMMON MONSTERCUnit__doDesummon解除召唤
0x55A73B56 INTERRUPT CHANCE抗性CCharacter_isBossOrNoDestroy
CUnit__getStat
CUnit__applyDifficultyResistBoost
CCharacter__playReactionAnim
打断概率:Boss 与不可摧毁单位直接免疫
0x55A8566 MANA RECHARGE、123 MANA RECHARGE PLAYERCUnit__modifyMana回蓝:单调用;6 与 123 共用同一段代码
0x55A868156 DAMAGE CHANCECEffect__evalSlot几率伤害:掷概率后落到 52 的伤害路径
0x55A8BE52 DAMAGECUnit__hasStateNamed伤害入口:先查状态名,随后进入共享的暴击/伤害块
0x55A92B7 HP RECHARGE、124 HP RECHARGE PLAYERCUnit__getStat×2回血:先取两个属性,随后进入共享的暴击/结算块;7 与 124 共用
0x55BAB4146 ADD STAT属性矩阵CEffect__evalSlot
CStatManager__addToStat
加属性
0x55BB2C148 SET STAT属性矩阵CEffect__evalSlot
CStatManager__setStat
设属性
0x55BBA4150 CLEAR STAT属性矩阵CEffect__evalSlot
CStatManager__removeStat
清属性
0x55BC14147 ADD STAT TO LEVEL属性矩阵CEffect__evalSlot
CUnit__getLevel
CStatManager__addToStat
按等级加属性
0x55BC86149 SET STAT ON LEVEL属性矩阵CEffect__evalSlot
CUnit__getLevel
CStatManager__setStat
按等级设属性
0x55BCF8151 CLEAR STAT ON LEVEL属性矩阵CEffect__evalSlot
CUnit__getLevel
CStatManager__removeStat
按等级清属性
0x55BD62158 TELEPORT传送×4CUnit__isAlive
CUnit__getMoveSpeedA/B
CUnit__getStat
CEffect__evalSlot×3
CUnit__teleportToClampedPoint
定点传送
0x55BF07157 TELEPORT RANDOM传送×4CUnit__isAlive
CUnit__getMoveSpeedA/B
CUnit__getStat
CEffect__evalSlot×3
CUnit__teleportRandomNearby
随机传送
0x55C08C235 TELEPORT NOPARTICLE传送×4…同 158…
CUnit__teleportToClampedPoint
定点传送(无粒子)
0x55C231236 TELEPORT RANDOM NOPARTICLE传送×4…同 157…
CUnit__teleportRandomNearby
随机传送(无粒子)
0x55C3B6160 SPAWN UNITCEffect__evalSlot×4
CUnit__isLocallyAuthoritative
CUnit__spawnUnitByGuid
生成单位:按 GUID 生成,只在权威端
0x55C495215 SHIELD BREAK EFFECTCUnit__hasUnitType
vec_atClampedByCapacity×4
CEffect__isExpired
CEffect__setSlotAndReroll
破盾:遍历目标身上的效果并重掷某个槽
0x55C74F165 ADD TRIGGERABLECEffect__evalSlot
CUnit__findTriggerableByDef
添加触发器
0x55C7EA166 REMOVE TRIGGERABLECUnit__removeTriggerableByName按名字移除触发器
0x55C80D168 ADD CHARGE PERCENT充能CPlayer__addChargePercent加充能百分比
0x55C841169 SET CHARGE PERCENT充能CPlayer__updateChargeBar设充能百分比
0x55C873171 ADD CHARGES充能CPlayer__addCharges加充能点数
0x55C8A1181 CONVERT CHARGE TO STAT充能CPlayer__getMaxCharges
CUnit__getChargeFraction
CPlayer__addCharges
CStatManager__addToStat
把充能换算成属性(按点数)
0x55C959182 CONVERT CHARGE PERCENT TO STAT充能CUnit__getChargeFraction
CPlayer__addChargePercent
CStatManager__addToStat×2
CEffect__playParticleFx
把充能换算成属性(按百分比),并放粒子

两组共用槽位:6/1230x55A856)与 7/1240x55A92B)在 INSTANT 路径上行为完全相同, 区别只在数值查询侧的消费方不同;213 RETARGET214 AGRO 共用 0x559ADF

CAUTION

剩下 175 个类型在 INSTANT 路径上是彻底的空操作。 default 落点是 0x55CA77,紧接着就是 0x55CA79 起的函数尾声(popretn 8)—— 一条有效指令都没有。写 DURATION:INSTANT 加这些类型,效果会被正常构造、 跑一遍分派、然后什么都不做,不报错、不打日志。属性加成类型基本都在这 175 个里。

6. 路径 5:挂载时执行

28 个类型有真分支

处理地址类型关键调用做什么
0x5626C7227 SKILL ENABLE、228 SKILL DISABLECItem_AttachDynamicEffect技能启用/禁用:只做挂接;由函数开头的 CEffect+0x20 == 2 前置判定到达,不经 switch
0x562702144 PERCENT BLIND抗性CEffect__evalSlot×2
CUnit__getStat
CItem_AttachDynamicEffect
CCharacter__setTarget
CCharacter__playReactionAnim
CCharacter_isBossOrNoDestroy
百分比致盲:命中后强制换目标
0x562845202 BLIND抗性CUnit__getStat
CUnit__applyDifficultyResistBoost
CItem_AttachDynamicEffect
CCharacter__setTarget
致盲
0x5629C3173 CHARGE BAR DECAY DELAYCItem_AttachDynamicEffect
sub_5B4990
充能条衰减延迟
0x562A18167 INTERRUPT抗性CUnit__getStat
CUnit__applyDifficultyResistBoost
CCharacter__playReactionAnim×2
打断(与 INSTANT 版同形)
0x562B98196 IMMOBILIZE抗性CUnit__isAlive
CUnit__getStat
CUnit__applyDifficultyResistBoost
CUnit__applyStagger
CItem_AttachDynamicEffect
定身:经 applyStagger
0x562C72162 WARP TO POICLevel__findPOIByName
CLevel__findFreeSpotNear_r2
CLevel__snapToGround
CUnit__setPosition
传送到 POI:找点→找空位→贴地→设位置
0x562D1E146 ADD STATCEffect__evalSlot
CStatManager__addToStat
CStatManager__findStat
CStat__setDuration
加属性——比 INSTANT 版多了 findStat + setDuration,属性带时长
0x562DC5128 SILENCE抗性CUnit__isAlive
CUnit__getStat
CUnit__applyDifficultyResistBoost
CItem_AttachDynamicEffect
沉默
0x562E8780 TURN ALIGNMENT抗性CUnit__hasUnitType
CUnit__datGetAttachesToMaster
CUnit__getStat
CUnit__findOwningPlayer
CUnit__enforceCharmLimit4
CUnit__addPet
魅惑:命中后变成玩家的宠物,且受 4 个魅惑上限约束
0x562F9179 FLEE EFFECT抗性CUnit__isAlive
CUnit__hasUnitType
CUnit__getStat
sub_664F10
CCharacter__playReactionAnim
恐惧
0x5631D264 STUN抗性CEffect__evalSlot
CUnit__isAlive
CUnit__getStat×2
CUnit__applyDifficultyResistBoost
CCharacter__playReactionAnim
眩晕:两次 getStat(两级抗性)
0x56348E106 FREEZE元素×4rand_float_between
CUnit__isAlive
CUnit__getStat
CUnit__applyDifficultyResistBoost
CCharacter__playReactionAnim
CItem_AttachDynamicEffect
CEffect__getSourceAsCharacter
冰冻
0x563673107 BURN元素×4…与 106 逐条同构…燃烧
0x56383B108 POISON元素×4…与 106 逐条同构…中毒
0x563A07109 SHOCK元素×4…与 106 逐条同构…感电
0x563BD363 VELOCITY MULTCItem_AttachDynamicEffect
Ogre::Vector3::operator=
速度倍率:挂接后清一个向量
0x563C0056 INTERRUPT CHANCE抗性CEffect__evalSlot
CCharacter_isBossOrNoDestroy
CUnit__getStat
CUnit__applyDifficultyResistBoost
CCharacter__playReactionAnim
打断概率:Boss 免疫
0x563D0B156 DAMAGE CHANCECEffect__evalSlot几率伤害:掷概率后落到 52
0x563D5252 DAMAGECUnit__hasStateNamed
CItem_AttachDynamicEffect
CUnit__getStat×3
CUnit__getRootOwner×2
CUnit__magicBonusToSkillFrac
CUnit__findOwningPlayer×2
伤害:沿 getRootOwner 上溯到真正的归属者再取属性
0x563F3A7 HP RECHARGE、124 HP RECHARGE PLAYERCItem_AttachDynamicEffect
CUnit__getStat×2
CUnit__magicBonusToSkillFrac
CEffect__setSlotAndReroll×2
回血:挂载时就把两个槽重掷并写回
0x56400D110 KILLCUnit__isPlayerPrimaryPet
CUnit__isLocallyAuthoritative
CEffect__evalSlot
击杀:玩家主宠免疫(与 INSTANT 版同)
0x5640B7145 EXPLODE DEADCUnit__isAlive
CUnit__gibCorpse_DEATHEXPLODE
尸爆:比 INSTANT 版多一道 isAlive
0x5640D0160 SPAWN UNITCEffect__evalSlot
CItem_AttachDynamicEffect
CEffect__setSlotAndReroll
生成单位
0x564141165 ADD TRIGGERABLECEffect__evalSlot
CUnit__findTriggerableByDef
CItem_AttachDynamicEffect
添加触发器:先按名字比对去重
0x5642B5166 REMOVE TRIGGERABLECItem_AttachDynamicEffect
CUnit__removeTriggerableByName
移除触发器

227 SKILL ENABLE / 228 SKILL DISABLE 有前置特例:函数开头先判 CEffect+0x20 == 2, 命中就走 CItem_AttachDynamicEffect 直接返回,根本到不了 switch。

WARNING

挂载路径的 default 不是空操作,这一点和 INSTANT 完全不同。 default 落点 0x564308 是一段真代码:

asm
00564308  cmp  eax, 1            ; eax = CEffect+0x20(宿主种类)
0056430B  jz   loc_564316
0056430D  test eax, eax
0056430F  jz   loc_564316        ; (0, 1) → 执行挂接
00564311  cmp  eax, 2
00564314  jnz  loc_56432C
00564316  call CItem_AttachDynamicEffect
0056432C  call CUnit__rollSkillToggleDuration
00564344  call CCharacter__isHostileTo

也就是说,剩下 194 个类型没有类型专属分支,但通用动作照做: 该挂接的照样挂接,技能开关时长照样掷。 「落 default」只等于「没有为这个类型写特殊处理」,不等于「什么都没发生」。

6.1 分支里反复出现的几个族

拆完 56 + 28 个分支后,重复的结构比独特的逻辑多得多:

  • 状态抗性统一模式CUnit__getStat(<对应抗性类型>)CUnit__applyDifficultyResistBoost → 概率判定。难度加成在抗性之上二次放大,所以同一个概率在不同难度下的实际命中率不同。
  • 元素状态四兄弟 106/107/108/109:挂载路径与 tick 路径各有四份逐条同构的副本。
  • 传送四兄弟 157/158/235/236:INSTANT 路径四份同构,前缀完全一致,只在落位函数(teleportToClampedPoint vs teleportRandomNearby)与粒子标志上分叉。
  • 属性操作 3×2 矩阵 146–151:{addToStat, setStat, removeStat} × {普通, 按等级}。
  • 属性永久化七兄弟 114–120hasUnitTypeevalSlot__ftol2_sse 同一个三调用模式。
  • 充能族 168/169/171/181/182:前三个是单调用薄封装,后两个把充能换算成属性。

IMPORTANT

同一个 TYPE 在两条路径上可以做不同的事。 最清楚的例子是 146 ADD STAT: INSTANT 分支(0x55BAB4)只调 CStatManager__addToStat; 挂载分支(0x562D1E)多了 CStatManager__findStat + CStat__setDuration——属性带时长。 同一个效果块,写 DURATION:INSTANT 得到永久属性,写一个时长反而得到会过期的属性。

52 DAMAGE 也是:INSTANT 分支直接进伤害块,挂载分支先沿 CUnit__getRootOwner 上溯到 真正的归属者再取属性——宠物造成的伤害因此按主人的属性算。

7. 路径 6:每帧 tick

CUnit__tickActiveEffects_dispatch 没有跳转表,是一长串 cmp dword ptr [reg+1Ch], <类型号> 的 if 链, 共 45 处比较点。

IMPORTANT

这 45 处并不是 45 个独立分支。 其中 22 处组成两条 OR 链——每条链里所有比较都跳向同一个落点, 是「类型 ∈ 某个集合」的成员测试,不是逐类型分派。 真正逐类型的动作块只有 23 个

7.1 两条 OR 链

比较点命中类型共同落点这条链做什么
0x566F430x566F5247 SUMMON DURATION、110 KILL0x566F58`TYPE == 47
20 处
0x56701A0x5671B3
74 CAST SKILL、75 CAST SKILL ON TARGET、76 CAST SKILL AT TARGET、81 CAST SKILL ON STRUCK、105 CAST SKILL ON DEATH、152 CAST SKILL ON STRIKE、194 CAST SKILL ON RANGED STRIKE、195 CAST SKILL ON MELEE STRIKE、198 CAST SKILL FROM TARGET、199 CAST SKILL ON MELEE STRIKE FROM TARGET、200 CAST SKILL ON RANGED STRIKE FROM TARGET、201 CAST SKILL ON STRIKE FROM TARGET、203 CAST SKILL ON KILL、204 CAST SKILL ON KILL AT TARGET、206 CAST SKILL ON MELEE SKILL STRIKE、207 CAST SKILL ON MELEE SKILL STRIKE FROM TARGET、208 CAST SKILL ON RANGED SKILL STRIKE、209 CAST SKILL ON RANGED SKILL STRIKE FROM TARGET、210 CAST SKILL ON SKILL STRIKE、211 CAST SKILL ON SKILL STRIKE FROM TARGET0x5671B9完整的 CAST SKILL ON x 家族,20 个类型全在这一条 OR 链里,每条比较都跳同一个落点 loc_5671B9,那里只做一件事:if (unit->+0x1BC != 0) 跳过。这是一道门禁,不是分派。

第二条链值得看一眼形状——20 次比较,20 个 jz 指向同一个地址:

asm
0056701A  cmp  dword ptr [eax+1Ch], 4Ah   ; 74  CAST SKILL
0056701E  jz   loc_5671B9
00567030  cmp  dword ptr [ecx+1Ch], 4Ch   ; 76  CAST SKILL AT TARGET
00567034  jz   loc_5671B9
          …(中间 17 次同构比较)…
005671B3  cmp  dword ptr [eax+1Ch], 4Bh   ; 75  CAST SKILL ON TARGET
005671B7  jnz  short loc_5671C6
005671B9  mov  ecx, [esp+228h+var_218]    ; ← 20 条 jz 的共同落点
005671BD  cmp  dword ptr [ecx+1BCh], 0
005671C4  jnz  short loc_567205

命中的 20 个类型恰好等于 CAST SKILL ON x 家族的全部成员,一个不多一个不少。

7.1.1 这条链是「清理」,不是「门禁」

两处需要读准,否则会得出完全相反的结论。

第一,整段只在效果到期时才跑。 进入 OR 链前有一道 cmp var_1EC, 00x567005), 而 var_1EC 是「本帧到期」标志,在循环开头这样置位:

asm
00566EEF  fld  dword ptr [edx+24h]        ; eff->DURATION
00566EF2  fld  dword ptr [edx+18h]        ; eff->elapsed
00566EF5  fadd [ebp+arg_0]                ; + dt
00566EF8  fcompp                          ; DURATION vs (elapsed + dt)
00566F01  mov  byte ptr [...var_1EC], 1   ; 到期 → 置 1
          …或者…
00566F08  mov  edx, [edx+28h]             ; flags
00566F0B  and  dl, 20h
00566F11  setz byte ptr [...var_1EC]      ; flags & 0x20 == 0 → 也置 1

第二,CUnit+0x1BC 不是标志位,是 CSkillManager*CUnit__getOrCreateSkillManager0x513840)惰性创建它: 0x51384B 判空、0x513894 写回。所以 cmp [ecx+1BCh], 0 问的是 「这个单位有没有技能管理器」,不是「某个开关开没开」。

合起来,这段的真实语义是:

若 效果本帧到期:
    若 TYPE ∈ {20 个 CAST SKILL ON x} 且 自己有技能管理器
        → 调虚表槽 90,把技能卸掉
    否则若 TYPE == 183 CAST SKILL ON DEATH FROM EFFECT OWNER
        且 eff->来源单位 存在 且 来源单位有技能管理器      ← 查的是**来源**,不是自己
        → 同样调虚表槽 90

7.1.2 CAST SKILL ON x 的真实机制:装一个真技能上去

顺着虚表槽找下去,这一族的完整生命周期是:

阶段函数做什么
挂上CBaseUnit__registerCastSkillEffect_vt910x514AB0,虚表槽 91/+0x16CevalSlot 取技能等级 → CBaseUnit__getOrCreateSkill 在单位身上真的建出一个技能对象CSkillMgr__setSkillLevel
到期CUnit__unloadCastSkillEffect_vt900x51CE80,虚表槽 90/+0x168CSkillList__findSkillCSkillList__removeSkill

IMPORTANT

CAST SKILL ON x 不是「每帧检查有没有触发」。 它在挂上的那一刻往单位的技能管理器里安装一个真实的技能对象,到期时再卸掉; 触发判定由技能系统负责。这就是为什么这一族在 tick 分派器里只出现清理逻辑—— 平时根本不需要 tick 参与。

一个可直接用上的副产品:技能名写错时引擎会打日志0x514BC9Ogre::LogManager::logMessage)。 CAST SKILL ON x 是少数几个写错了会在日志里留痕的类型。

7.1.3 触发端:从事件到施法

装上技能之后,谁在什么时候把它放出来?整条链有三段。

第一段 —— 事件发生处直接 push 一个效果类型号。 入口是 CBaseUnit__fireProcEffects0x53D3C0):

fireProcEffects(this, chance@st0, eventType, other, bcast, srcSkill, strikeFlags)

IMPORTANT

eventType 参数就是效果类型号本身,被直接当事件 ID 用。 引擎里没有「按事件分类的注册表」,也没有位掩码——常量是调用方 push 进来的。 这就是为什么全库搜不到第二处 cmp [reg+1Ch], 152 之类的比较: 那个 152 是在 CCharacter__resolveStrikeHit 里当立即数推进栈的。

fireProcEffects 内部:

若 bcast 且本地权威 → 发 NetUnitProcMsg 广播        (0x53D4A5)
CUnit__hasActiveEffectOfType(eventType)  ← 快速门    (0x53D4B1)
扫两个桶,挑出 TYPE == eventType 的效果               (0x53D4F7)
  每个: CEffectMgr__evalEffectValue → execProcCastSkill,返回 false 就 break
若有背包 → sub_7C8130 逐件装备再触发一轮             (0x53D588)

最后那一步值得注意:装备上的 proc 是单独一轮,走 sub_7C8130sub_7CB870, 与身上效果的那一轮互不干扰。

第二段 —— 22 个触发点,逐个核实过的事件类型:

触发时机函数事件类型(= 效果类型号)
挥击选定目标0x553900 CCharacter__strike_resolveTargets740x553989
750x554E89
760x554EA7
1980x554EC8
命中结算0x548850 CCharacter__resolveStrikeHit1520x54E21D
2010x54E237
1940x54E266
1950x54E295
199/2000x54E2AF(近战/远程两个常量共用同一个 call,0x54E279 push 200 后 jmp 过来)
2100x54E2D6
2110x54E2F0
2080x54E31F
2090x54E339
2060x54E355
2070x54E36F
受到伤害0x53D5A0 CBaseUnit__applyDamage810x53DAD4(且受击者 +0x7DC <= 0)
死亡0x54E3F0 CUnit__onDeath_vt1201050x54F55D
1830x54F573
1610x54F591
2030x54F5A7
2040x54F5B9

199200 共用 0x54E2AF 这一个 call——0x54E279 推入 200 之后 jmp 过去, 近战/远程只是选了不同的常量。

第三段 —— execProcCastSkill0x53C7E0)真正施放。

CEffect__evalSlot + rand_int_between            掷概率      (0x53C8EF / 0x53C916)
161 SPAWN UNIT ON DEATH 特判                                (0x53C8F7)
183 特判 → 查**来源单位**的技能管理器                        (0x53CA47 / 0x53CA66)
CSkillList__findSkill / CSkillMgr__findSkillByName  找已安装的技能
CSkillList__isSkillOnCooldown                    查冷却      (0x53CAD9 / 0x53CC8F / 0x53CD37)
第三张跳转表 → 决定施法模式                                  (0x53CDA5)
CSkillList__executeSkill                         施放        (0x53CB92 / 0x53CE0F / 0x53CE69 …)

WARNING

proc 受技能自身冷却约束。 isSkillOnCooldown 在这个函数里出现三次。 给 CAST SKILL ON x 指定的技能如果自带冷却,proc 不会无视它—— 想要高频触发,技能本身的冷却也得配合。

第三张跳转表同样是「字节索引表 + 跳转表」结构,可以直接读表:

asm
0053CD8D  mov   esi, [esi+1Ch]           ; eventType
0053CD90  lea   eax, [esi-4Ah]           ; -74     ← 覆盖 TYPE 74..211
0053CD93  cmp   eax, 89h                 ; 137
0053CD98  ja    def_53CDA5
0053CD9E  movzx eax, ds:byte_53D32C[eax] ; 138 项字节索引表
0053CDA5  jmp   ds:jpt_53CDA5[eax*4]     ; 6 项跳转表,default 槽位 = 5

解出来的真分支又是同样那 20 个类型(118 个落 default),分成五种施法模式

处理地址施法模式类型
00x53CDAC自己施法74 CAST SKILL、75 CAST SKILL ON TARGET、81 CAST SKILL ON STRUCK、105 CAST SKILL ON DEATH、152 CAST SKILL ON STRIKE、194 CAST SKILL ON RANGED STRIKE、195 CAST SKILL ON MELEE STRIKE、206 CAST SKILL ON MELEE SKILL STRIKE、208 CAST SKILL ON RANGED SKILL STRIKE、210 CAST SKILL ON SKILL STRIKE
10x53CECC朝目标位置施法76 CAST SKILL AT TARGET
20x53CF58由目标施法 —— 恰好是全部名字带 FROM TARGET 的 7 个198 CAST SKILL FROM TARGET、199 CAST SKILL ON MELEE STRIKE FROM TARGET、200 CAST SKILL ON RANGED STRIKE FROM TARGET、201 CAST SKILL ON STRIKE FROM TARGET、207 CAST SKILL ON MELEE SKILL STRIKE FROM TARGET、209 CAST SKILL ON RANGED SKILL STRIKE FROM TARGET、211 CAST SKILL ON SKILL STRIKE FROM TARGET
30x53CE19击杀触发203 CAST SKILL ON KILL
40x53CE73击杀且朝目标204 CAST SKILL ON KILL AT TARGET

NOTE

槽 2 的 7 个类型恰好是全部名字带 FROM TARGET 的那批,一个不多一个不少—— 命名和代码分组完全对应。

183 CAST SKILL ON DEATH FROM EFFECT OWNER 在这张表里落 default, 它靠 0x53CA47 的前置特判处理,永远到不了跳转表。

四段合起来

阶段入口做什么
安装registerCastSkillEffect_vt91 0x514AB0在单位的技能管理器里建出技能对象
触发fireProcEffects 0x53D3C0事件处 push 类型号 → 扫效果 → 逐个执行;再扫一遍装备
执行execProcCastSkill 0x53C7E0掷概率 → 查冷却 → 按模式决定目标/来源 → executeSkill
卸载unloadCastSkillEffect_vt90 0x51CE80效果到期时把技能从管理器里摘掉(7.1.1 节)

7.1.4 五种施法模式各自做什么

五段的骨架一致——组装两个位置参数,然后 CSkillList__executeSkill。 位置靠 sub_61DCE0(unit, &vec3, 1) 取(把某个单位的位置写进栈缓冲)。区别全在取谁的位置

地址类型递归防护做什么
00x53CDAC74 CAST SKILL、75 CAST SKILL ON TARGET、81 CAST SKILL ON STRUCK、105 CAST SKILL ON DEATH、152 CAST SKILL ON STRIKE、194 CAST SKILL ON RANGED STRIKE、195 CAST SKILL ON MELEE STRIKE、206 CAST SKILL ON MELEE SKILL STRIKE、208 CAST SKILL ON RANGED SKILL STRIKE、210 CAST SKILL ON SKILL STRIKEother 就取 other 的位置作目标点,没有就取自己的;施法者取自己(ebp),技能来自自己的技能管理器([ebp+1BCh])。最后 CSkillList__executeSkill(0x53CE0F)。
10x53CECC76 CAST SKILL AT TARGET两个位置参数都取 other(有目标时),没有目标就都取自己。这是唯一一个「把落点整体搬到目标处」的模式,也是唯一不设递归防护的普通模式。
20x53CF58198 CAST SKILL FROM TARGET、199 CAST SKILL ON MELEE STRIKE FROM TARGET、200 CAST SKILL ON RANGED STRIKE FROM TARGET、201 CAST SKILL ON STRIKE FROM TARGET、207 CAST SKILL ON MELEE SKILL STRIKE FROM TARGET、209 CAST SKILL ON RANGED SKILL STRIKE FROM TARGET、211 CAST SKILL ON SKILL STRIKE FROM TARGET两道前置:other == 0 放弃、srcSkill == 0 放弃(0x53CF60 / 0x53CF6E)。随后取自身虚表 +0x88 的 Matrix4,乘上一个由 rand_float_between(0.0, 360.0) × π/180sub_676E70 + flt_2144DB8=360.0 + flt_216C944=0.0174533)算出的随机角度构成的旋转矩阵,再 setTransother(目标)的位置(0x53D011 / 0x53D01E)——锥形是从目标处、朝一个随机方向发出的。接着 CLevel__queryUnitsInCone(0x53D104);命中数 > 0 时用 rand_int_between + vec_atClampedByCapacity 再随机挑一个(0x53D114 / 0x53D121),否则置 0。最后 CSkillList__executeSkill(0x53D2E0)。中途还会 getGameClient + sub_6E18B0 走一次网络广播。
30x53CE19203 CAST SKILL ON KILL两个位置参数都取自己,不看 other
40x53CE73204 CAST SKILL ON KILL AT TARGETother == 0 直接放弃(0x53CE83);有目标时位置取自目标,随后 jmp 汇入模式 3 的尾部(0x53CE58)。

sub_61DCE0(unit, &vec3, 1) = 取某个单位的位置写进栈上缓冲,五种模式都靠它组装 executeSkill 的位置参数。

WARNING

FROM TARGET 那 7 个类型带两重随机。 这一段会取自身虚表 +0x88Matrix4,乘上一个 rand_float_between(0.0, 360.0) × π/180 算出的随机角度旋转矩阵, 再 setTrans目标的位置——锥形是从目标处、朝一个每次重掷的随机方向发出的。 然后 CLevel__queryUnitsInCone 查到的多个单位里,再用 rand_int_between 随机挑一个

两个常量已读出:flt_2144DB8 = 360.0flt_216C944 = 0.0174533(π/180)。

这一段还有两道会让效果完全不触发的前置:other == 0(无目标)、srcSkill == 0(无来源技能)。

7.1.5 锥形查询的实参与判定公式

CLevel__queryUnitsInCone0x5FC670)的签名:

queryUnitsInCone(this, out1, out2, Matrix4* xform, alignment,
                 float minDist, float halfAngleDeg, float maxDist, exclude, skill, …)

模式 2 传进去的三个浮点:

形参实参来源
minDist硬编码 0.0fldz @0x53D0DD)—— 近身豁免这条等于不生效
halfAngleDegCSkill__getLevelConeHalfAngleDeg0x6BE3A0)= skill->levelData->+0xA8 = FINDTARGETANGLE,缺省 0
maxDistCSkill__getLevelRange0x6BE3C0)= skill->levelData->+0x94 = RANGE,缺省 1.0

两个 DAT 键已确认到写入指令:

DAT 键字段加载点
FINDTARGETANGLECSkillProperty+0xA8CSkillProperty__ctorFromDat 0x6DDFAB 推键名 → 0x6DDFD6 CDataGroup__getFloat0x6DDFDB fstp [esi+0A8h]。原版仅 4 处使用。
RANGECSkillProperty+0x94同一函数 0x6DE0080x6DE0330x6DE038 fstp [esi+94h]。原版 141 处使用。

IMPORTANT

两处的 getFloat 默认实参都取自 上一级 [LEVELn-1] 的同名字段mov eax, [esi+34h];无上一级时 FINDTARGETANGLE 用 0、RANGE 用本级当前值)。所以 [LEVELn] 里不写这些键就继承上一级,不是回到默认值。

判定公式0x5FC8B10x5FC988):

  1. 局部坐标 = 目标位置 × Matrix4::inverse(xform)
  2. 把 Y 压平(v43 = 0.0),长度即距离
  3. atan2(x, z) × 57.295826角度(度)
  4. 命中 = 距离 ≤ minDist 或 ((|角度| ≤ 半角 或 (半角>0 且 距离<3.0 且 |角度|<30°)) 且 距离 ≤ maxDist) 或 (半角>20 且 (距离>1.5 或 |角度|>180))

WARNING

「距离 < 3.0 时把判定放宽到 30°」和「半角 > 20 时的旁路」是两条硬编码的贴脸补偿,写 MOD 时按纯锥形算会对不上近身的结果。

7.1.6 CUnit+0x7DC:proc 的 1 秒内置冷却

内容
字段CUnit+0x7DC
性质倒计时,不是布尔
置位execProcCastSkill 里 fld1; fstp [ebp+7DCh] 置 1.0 —— 只在模式 0(0x53CDB6)、模式 3(0x53CE24)、模式 4(0x53CE7D)
读取CBaseUnit__applyDamage 0x53DAB6 fcomp [esi+7DCh],就在触发 81 CAST SKILL ON STRUCK 之前(0x53DAD4)
衰减CMonster_tick_main 0x56B8E8–0x56B902:若 > 0 则每帧 -= dt
初始化CCharacter_ctor 0x555C1B
含义proc 施法后给施法者上 1 秒的内置冷却,防止「受击 → 施法 → 打到自己 → 再受击」无限递归。模式 1(76)与模式 2(FROM TARGET 七个)不设这个标记。

IMPORTANT

这个字段解释了一个实践中常见的困惑:81 CAST SKILL ON STRUCK 不会每次受击都触发。CBaseUnit__applyDamage0x53DAB6 比较受击者的 +0x7DC,非零就不触发; 而 proc 一旦施法就把它置成 1.0,再由 CMonster_tick_main 每帧 -= dt 减回去。 效果上就是每秒最多触发一次

需要注意的不对称:模式 1(76 CAST SKILL AT TARGET)与模式 2(FROM TARGET 七个) 不设这个标记,所以它们不给施法者上这道闸。

7.2 23 个逐类型动作块

比较点类型做什么
0x5671D2183 CAST SKILL ON DEATH FROM EFFECT OWNERCAST SKILL ON DEATH FROM EFFECT OWNER 紧跟在 OR 链之后单独判,走虚表调用
0x56729D61 MISSILE REFLECT VISUAL__RTDynamicCastCCharacter__setTarget:弹射反射的视觉版会改目标
0x5673B467 DESUMMON MONSTERCUnit__doDesummon
0x567472227 SKILL ENABLE与 228 共用 CSkillMgr__setSkillEnabledByName0x5674CC
0x567486228 SKILL DISABLE同上,两行只是传不同的布尔值
0x56752F65 LEARN SKILLfindOwningPlayergetOrCreateSkillManagerCSkillList__createSkillsetSkillEnabled每帧都重新确保技能存在
0x5676F3229 DRAW HEALTH与 230 一起转调 CUnit__tickDrawHealthMana0x56959E
0x56770B230 DRAW MANA同上
0x567723185 HP MOD OVER TIMEsub_51EFC0 + CEffect__evalSlot×5:五个槽全读
0x567E9280 TURN ALIGNMENTCEffect__isExpiredCUnit__isAlivedatGetAttachesToMasterhasStateNamed:魅惑的存续维护
0x56803064 STUNsub_6D7B90 / sub_6D9C80 / CDataGroup__getInt64:眩晕每帧去读一次 dat 键
0x568330107 BURN燃烧
0x568615106 FREEZE冰冻
0x5688FA109 SHOCK感电
0x568BDF108 POISON中毒
0x568F0559 SCALECEffect__evalSlot×4 —— 正好对应 59 的四个槽 STARTSCALE/ENDSCALE/TIMETOSCALE/TIMETOSCALEBACK
0x569274184 SCALE BY HPevalSlot×2 → CUnit__getMaxHealth_CIpow:按生命比例做幂曲线缩放
0x569360160 SPAWN UNITevalSlot×2 → CUnit__isLocallyAuthoritative
0x569541231 PERCENT DAMAGE TAKEN BY MONSTER COUNT转调 CUnit__tickDamageByMonsterCount0x569580
0x569554234 PERCENT DAMAGE BONUS BY MONSTER COUNT同上,与 231 共用同一个实现
0x5696A543 KNOCK BACK EFFECTCUnit__getStatCEffect__recacheValue → Vector3 累加:击退每帧重算数值
0x569824225 PULL EFFECTVector3 相减 → lengthnormalise → 两次 operator*=:拉拽向量的完整计算
0x569AD1166 REMOVE TRIGGERABLECUnit__removeTriggerableByName,随后回调 CUnit__executeInstantEffect0x569B31

NOTE

106/107/108/109 四段的调用序列逐条一致:vec_atClampedByCapacityCEffect__isExpiredsub_518790vec_at×2 → 虚表调用 → hasUnitTypeCEffectMgr__findAffixByNamehasUnitType。四份副本,改一处不改其余三处就会让四种元素状态行为不一致。

7.3 三处共享实现与一处跨路径回调

  • 227 SKILL ENABLE228 SKILL DISABLE 共用 CSkillMgr__setSkillEnabledByName0x5674CC)。

CUnit__tickDrawHealthMana0x53F6A0)—— 229 / 230 的全部实现

槽位:MIN_PER_HEALTH/MAX_PER_HEALTH(或 MANA)、PULSE_RATE、RADIUS、MAX_TARGETS

  1. 类型门:cmp eax, 0E5h(229) / cmp eax, 0E6h(230)(0x53F6ED / 0x53F6F4)
  2. CEffect__evalSlot ×3 取 PULSE_RATE/RADIUS/MAX_TARGETS(0x53F703 / 710 / 71D)
  3. CLevel__gatherUnitsInRadius(0x53F7BB)—— 范围抽取,不是单体
  4. 229 分支(0x53F815):floor(目标当前血)getMaxHealth 就跳过 —— 满血目标不被抽
  5. CUnit__isLocallyAuthoritative 后再比一次满血
  6. 目标状态 +0x314 ∈ {12, 50, 51} 时追加一道 +0x284 判定(0x53F87E–0x53F89E)
  7. hasUnitType(0xA7)hasStateNamed 两道排除(0x53F8A4 / 0x53F8D7)
  8. sub_535570 + 虚表调用完成实际抽取;CEffect__playParticleFx 放特效

NOTE

分派器那两行只是转调,不带 isExpired 前置,每帧无条件调。

CUnit__tickDamageByMonsterCount0x532660)—— 231 / 234 的全部实现

槽位:MIN_PER/MAX_PER、RANGE(槽 2)、MAX_COUNT(槽 3)、槽 4 被引擎占用

  1. 只认 231 / 234,其余立即返回(0x5326C2)
  2. 刷新周期 0.2 秒硬编码当前时间 - evalSlot(eff,4) < 0.2 就跳过(0x532701)
  3. CEffect__setSlotAndReroll(eff, 4, 当前时间) —— 槽 4 被当刷新时间戳写入(0x532712)
  4. findEffectByTypeAndName(26 或 25, eff->NAME)同名伙伴效果(0x532727 / 0x53273C)
  5. CLevel__gatherUnitsInRadius 半径 = 槽 2 数到周围单位数
  6. 值 = eff->+0xD4 × min(数量, MAX_COUNT),写进伙伴效果的槽 0 与槽 1(0x5327F4 / 0x532817)

CAUTION

它自己不算伤害,而是改写一条伙伴被动的数值让它进 239 属性缓存:231 → 伙伴 26 PERCENT DAMAGE TAKEN;234 → 伙伴 25 PERCENT DAMAGE BONUS数据里必须成对写:一条 231/234 + 一条同名的 26/25 被动,否则找不到伙伴,整条失效。另外 MOD 不要在 231/234 上写第 5 个参数,槽 4 是引擎的时间戳。

WARNING

166 REMOVE TRIGGERABLE 在 tick 路径里会回调 INSTANT 执行器0x569B31CUnit__executeInstantEffect)。 这是六条路径之间唯一一处互相调用——一个持续效果每帧都可能触发一次 INSTANT 语义。

8. 239 行路径归属总表

列的读法

  • 被动/扫表/布尔 —— 该类型是否被路径 1/2/3 消费。
  • INSTANT 分支 / 挂载分支 —— 处理代码地址;空白表示落 default,即无行为。这两列完整。
  • tick 比较点 —— if 链里比较该类型的指令地址。
  • 生效方式 —— 由前几列推出的结论。
#TYPE槽 0槽 1槽 2槽 3槽 4被动扫表布尔INSTANT 分支挂载分支tick 比较点生效方式
0MELEEDAMAGEBONUSMINMAXMINFORCEMAXFORCE·被动数值
1RANGEDDAMAGEBONUSMINMAXMINFORCEMAXFORCE·被动数值
2DEFENSEMINMAX···被动数值
3MAGICMINMAX···被动数值
4MAX MANAMINMAX···被动数值
5MAX HPMINMAX···被动数值
6MANA RECHARGEMINMAX···0x55A856被动/INSTANT
7HP RECHARGEMINMAXMINFORCEMAXFORCE·0x55A92B0x563F3A被动/INSTANT/挂载
8ARMOR BONUSMINMAX···被动数值
9TO HIT BONUSMINMAX···无消费方
10DAMAGE BONUSMINMAX···被动数值
11DAMAGE TAKENMINMAX···无消费方
12KNOCK BACKFORCE····被动数值
13SIGHT BONUSMINMAX···被动数值
14VIEW ANGLE BONUSMINMAX···被动数值
15PERCENT MELEEDAMAGEMINMAX···被动数值
16PERCENT RANGEDDAMAGEMINMAX···被动数值
17PERCENT DEFENSEMINMAX···被动数值
18PERCENT MAGICMINMAX···被动数值
19PERCENT MANAMINMAX···被动数值
20PERCENT HPMINMAX···被动数值
21PERCENT SPEEDMINMAX···被动数值
22PERCENT ATTACK SPEEDMINMAX···被动数值
23PERCENT ARMOR BONUSMINMAX···被动数值
24PERCENT TO HIT BONUSMINMAX···无消费方
25PERCENT DAMAGE BONUSMINMAX···被动/布尔
26PERCENT DAMAGE TAKENMINMAX···被动/布尔
27PERCENT MAGICAL DROPMINMAX···被动数值
28PERCENT GOLD DROPMINMAX···被动数值
29PERCENT CAST SPEEDMINMAX···被动数值
30PERCENT LIFE STOLENMINMAX···被动数值
31PERCENT MANA STOLENMINMAX···被动数值
32PERCENT DAMAGE REFLECTEDMINMAX···被动数值
33PERCENT BLOCK CHANCEMINMAX···被动数值
34PERCENT ITEM REQUIREMENTSMINMAX···无消费方
35PHYSICAL DEFENSEMINMAX···被动数值
36MAGICAL DEFENSEMINMAX···被动数值
37FIRE DEFENSEMINMAX···被动数值
38ICE DEFENSEMINMAX···被动数值
39ELECTRICAL DEFENSEMINMAX···被动数值
40POISON DEFENSEMINMAX···被动数值
41PERCENT SIGHT BONUSMINMAX···被动数值
42PERCENT VIEW ANGLE BONUSMINMAX···被动数值
43KNOCK BACK EFFECTFORCE····0x5696A5周期执行
44PERCENT ACTIVE DISTANCE BONUSMINMAX···被动数值
45OPEN WAYPOINT PORTALMINMAX···0x55A631仅 INSTANT
46IDENTIFYMINMAX···效果本身无行为
47SUMMON DURATIONMINMAX···0x566F43被动/tick
48DAMAGE REFLECTIONMINMAX···被动数值
49LIFE STEALMINMAX···被动数值
50MANA STEALMINMAX···被动数值
51GOLD DROPMINMAX···被动数值
52DAMAGEMINMAXMINFORCEMAXFORCE·0x55A8BE0x563D52被动/INSTANT/挂载
53PERCENT KNOCK BACK RESISTANCERESISTANCE····被动数值
54DEGRADE ARMORMINMAX···被动数值
55CRITICAL CHANCEMINMAX···被动数值
56INTERRUPT CHANCEMINMAX···0x55A73B0x563C00被动/INSTANT/挂载
57TRANSFORMMINMAX···0x55A696仅 INSTANT
58UNIT THEME·····机制不看 TYPE
59SCALESTARTSCALEENDSCALETIMETOSCALETIMETOSCALEBACK·0x568F05周期执行
60MISSILE REFLECTMINMAXDMGPCTMINDMGPCTMAX·扫表
61MISSILE REFLECT VISUALMINMAXDMGPCT··0x56729D周期执行
62PERCENT BLOCK CHANCE BASEMINMAX···被动数值
63VELOCITY MULTMINMAX···0x563BD3被动/布尔/挂载
64STUNMINMAXIGNORE_RESISTANCE··0x5631D20x568030被动/挂载/tick
65LEARN SKILLMINMAX···0x55A2180x56752FINSTANT/tick
66DEGRADE ARMOR EFFECTMINMAX···被动数值
67DESUMMON MONSTERMINMAXNO_LOOTNO_DIE_ANIMDESTROY_BODY0x55A72A0x5673B4INSTANT/tick
68XP GAIN BONUSMINMAX···被动数值
69STRENGTH BONUSMINMAX···被动数值
70DEXTERITY BONUSMINMAX···被动数值
71PERCENT STRENGTH BONUSMINMAX···被动数值
72PERCENT DEXTERITY BONUSMINMAX···被动数值
73TRANSFORM PERMANENTMINMAX···0x55A6FB仅 INSTANT
74CAST SKILLMINMAX···0x559EC20x56701AINSTANT/tick
75CAST SKILL ON TARGETMINMAX···0x5671B3周期执行
76CAST SKILL AT TARGETMINMAX···0x567030周期执行
77FAME GAIN BONUSMINMAX···被动数值
78MISSILE RANGE BONUSMINMAX···被动数值
79FLEE EFFECTMINMAX···0x562F91布尔/挂载
80TURN ALIGNMENTMINMAX···0x562E870x567E92布尔/挂载/tick
81CAST SKILL ON STRUCKMINMAX···0x56705F周期执行
82SKILL BONUSMINMAX···周期执行
83PRICE REDUCTIONMINMAX···被动数值
84FISHING LUCKMINMAX···被动数值
85ATTACK SPELL BONUSMINMAX···被动数值
86DEFENSE SPELL BONUSMINMAX···被动数值
87CHARM SPELL BONUSMINMAX···被动数值
88DUAL WIELDING BONUSMINMAX···被动数值
89PERCENT CRITICAL DAMAGEMINMAX···被动数值
90POTION EFFICIENCYMINMAX···被动数值
91PERCENT PET DAMAGEMINMAX···被动数值
92PERCENT PET VELOCITYMINMAX···被动数值
93REDUCED ITEM REQUIREMENTSMINMAX···被动数值
94ARMOR ITEM REQUIREMENTSMINMAX···被动数值
95SPELL REQUIREMENTSMINMAX···被动数值
96PET DEPARTURE TIMEMINMAX···被动数值
97PERCENT PET ARMORMINMAX···被动数值
98MARTIAL ITEM REQUIREMENTSMINMAX···被动数值
99PERCENT MARTIAL ITEM DAMAGE BONUSMINMAX···被动数值
100RANGED ITEM REQUIREMENTSMINMAX···被动数值
101MAGIC ITEM REQUIREMENTSMINMAX···被动数值
102PERCENT RANGED ITEM DAMAGE BONUSMINMAX···被动数值
103PERCENT MAGIC ITEM DAMAGE BONUSMINMAX···被动数值
104PERCENT PET HEALTHMINMAX···被动数值
105CAST SKILL ON DEATHMINMAX···0x56717D周期执行
106FREEZEMINMAX···0x56348E0x568615挂载/tick
107BURNMINMAX···0x5636730x568330挂载/tick
108POISONMINMAX···0x56383B0x568BDF挂载/tick
109SHOCKMINMAX···0x563A070x5688FA挂载/tick
110KILLMINMAXEXPLODENO_LOOT·0x55A5B20x56400D0x566F52INSTANT/挂载/tick
111FADE OUTMINMAX···0x55A53A仅 INSTANT
112SET VISIBLEMINMAX···0x55A576仅 INSTANT
113WARPMIN_DEPTHMAX_DEPTH···0x559F1A布尔/INSTANT
114PERMANENT STRENGTHMINMAX···0x55A0D5仅 INSTANT
115PERMANENT DEXTERITYMINMAX···0x55A103仅 INSTANT
116PERMANENT MAGICMINMAX···0x55A131仅 INSTANT
117PERMANENT HEALTHMINMAX···0x55A18D仅 INSTANT
118PERMANENT DEFENSEMINMAX···0x55A15F仅 INSTANT
119PERMANENT MANAMINMAX···0x55A1BB仅 INSTANT
120AWARD STATPOINTMINMAX···0x55A1EA仅 INSTANT
121FLEE RESISTRESISTANCE····被动数值
122SHIELD BUFFERMINMAX···周期执行
123MANA RECHARGE PLAYERMINMAX···0x55A856被动/INSTANT
124HP RECHARGE PLAYERMINMAX···0x55A92B0x563F3A被动/INSTANT/挂载
125LIFE STEAL MASTERMINMAX···被动数值
126PERCENT LIFE STOLEN MASTERMINMAX···被动数值
127STOP SKILLMINMAX···0x559E45仅 INSTANT
128SILENCEMINMAX···0x562DC5布尔/挂载
129DESUMMON ON DEATHMINMAX···周期执行
130EMOTIONAL DAMAGEMINMAX···开发期占位
131FUTURISTICNESSMINMAX···开发期占位
132JAUNTINESSMINMAX···开发期占位
133SOCKSMINMAX···开发期占位
134LOGIC RESISTANCEMINMAX···开发期占位
135NOODLE ARMSMINMAX···开发期占位
136INTERRUPT RESISTANCEMINMAX···被动数值
137CHARM RESISTANCEMINMAX···被动数值
138SILENCE_RESISTANCEMINMAX···被动数值
139STUN RESISTANCEMINMAX···被动数值
140SLOW RESISTANCEMINMAX···被动数值
141OPEN PORTALMINMAX···0x55A666仅 INSTANT
142SWAP POS WITH PETPETINDEX····0x559D7F仅 INSTANT
143RESPECMINMAX···0x559D3D仅 INSTANT
144PERCENT BLINDMINMAXCHANCE··0x562702被动/挂载
145EXPLODE DEADMINMAX···0x55A6210x5640B7INSTANT/挂载
146ADD STATMINMAXCHANCE··0x55BAB40x562D1EINSTANT/挂载
147ADD STAT TO LEVELMINMAXCHANCE··0x55BC14仅 INSTANT
148SET STATMINMAXCHANCE··0x55BB2C仅 INSTANT
149SET STAT ON LEVELMINMAXCHANCE··0x55BC86仅 INSTANT
150CLEAR STATMINMAXCHANCE··0x55BBA4仅 INSTANT
151CLEAR STAT ON LEVELMINMAXCHANCE··0x55BCF8仅 INSTANT
152CAST SKILL ON STRIKEMINMAX···0x567075周期执行
153PERCENT DUAL WIELDING ATTACKMINMAX···被动数值
154DAMAGE BONUS SECONDARYMINMAX···被动数值
155SET MESH INVISIBLEINVISIBLE····被动数值
156DAMAGE CHANCEMINMAXMINFORCEMAXFORCECHANCE0x55A8680x563D0BINSTANT/挂载
157TELEPORT RANDOMCHANCE·MINRANGEMAXRANGECOOLDOWN0x55BF07仅 INSTANT
158TELEPORTCHANCE·MINRANGEMAXRANGECOOLDOWN0x55BD62仅 INSTANT
159TELEPORT RESISTANCEMINMAX···被动数值
160SPAWN UNITCOUNT·CHANCEMINRANGEMAXRANGE0x55C3B60x5640D00x569360INSTANT/挂载/tick
161SPAWN UNIT ON DEATHCOUNTHPPCTCHANCEMINRANGEMAXRANGE周期执行
162WARP TO POIPOI····0x562C72挂载
163DESTRUCTABLE SHIELDHP%HPOVERRIDEANGLEOFFSETDESTROY_WEAPONS周期执行
164DISENCHANTGOLD%CHANCE%···效果本身无行为
165ADD TRIGGERABLE··CHANCE··0x55C74F0x564141INSTANT/挂载
166REMOVE TRIGGERABLEMINMAX···0x55C7EA0x5642B50x569AD1布尔/INSTANT/挂载/tick
167INTERRUPTMINMAX···0x559C180x562A18INSTANT/挂载
168ADD CHARGE PERCENTMINMAX···0x55C80D被动/INSTANT
169SET CHARGE PERCENTMINMAX···0x55C841被动/INSTANT
170MAX CHARGESMINMAX···被动数值
171ADD CHARGESMINMAX···0x55C873仅 INSTANT
172PERCENT CHARGE BAR DECAY RATEMINMAX···被动数值
173CHARGE BAR DECAY DELAYMINMAX···0x5629C3被动/挂载
174PERCENT CHARGING BONUSMINMAX···被动数值
175PERCENT MANA COST BONUSMINMAX···被动数值
176CHARGE BAR DECAY RATEMINMAX···被动数值
177EXPLODE ON DEATHMINMAX···周期执行
178DODGE CHANCE BONUSMINMAX···被动数值
179FUMBLE CHANCE REDUCTIONMINMAX···被动数值
180FUMBLE PENALTY REDUCTIONMINMAX···被动数值
181CONVERT CHARGE TO STAT··VALUE··0x55C8A1仅 INSTANT
182CONVERT CHARGE PERCENT TO STAT··VALUE··0x55C959仅 INSTANT
183CAST SKILL ON DEATH FROM EFFECT OWNERMINMAX···0x5671D2周期执行
184SCALE BY HPMAXSCALEMINSCALE···0x569274周期执行
185HP MOD OVER TIMEPERCENT_TO_HEALTIME_TO_HEALTIME_TO_HEAL_MAXMIN_STOP_ATMAX_STOP_AT0x567723周期执行
186TRANSFORMATION TIMEPERCENT_TO_100····被动数值
187TRAP DISARM CHANCECHANCE_TO_100····被动数值
188ELEMENTAL EFFECT DURATION BONUSMINMAX···被动数值
189BURN DURATION BONUSMINMAX···被动数值
190FREEZE DURATION BONUSMINMAX···被动数值
191SHOCK DURATION BONUSMINMAX···被动数值
192POISON DURATION BONUSMINMAX···被动数值
193NONE·····空操作哨兵
194CAST SKILL ON RANGED STRIKEMINMAX···0x5670D5周期执行
195CAST SKILL ON MELEE STRIKEMINMAX···0x5670A7周期执行
196IMMOBILIZEMINMAX···0x562B98被动/布尔/挂载
197IMMOBILIZE RESISTANCEMINMAX···被动数值
198CAST SKILL FROM TARGETMINMAX···0x567046周期执行
199CAST SKILL ON MELEE STRIKE FROM TARGETMINMAX···0x5670BE周期执行
200CAST SKILL ON RANGED STRIKE FROM TARGETMINMAX···0x5670EC周期执行
201CAST SKILL ON STRIKE FROM TARGETMINMAX···0x56708E周期执行
202BLINDMINMAX···0x562845被动/挂载
203CAST SKILL ON KILLMINMAX···0x56718D周期执行
204CAST SKILL ON KILL AT TARGETMINMAX···0x5671A0周期执行
205SHIELD BREAKMINMAX···被动数值
206CAST SKILL ON MELEE SKILL STRIKEMINMAX···0x567131周期执行
207CAST SKILL ON MELEE SKILL STRIKE FROM TARGETMINMAX···0x567144周期执行
208CAST SKILL ON RANGED SKILL STRIKEMINMAX···0x567157周期执行
209CAST SKILL ON RANGED SKILL STRIKE FROM TARGETMINMAX···0x56716A周期执行
210CAST SKILL ON SKILL STRIKEMINMAX···0x567103周期执行
211CAST SKILL ON SKILL STRIKE FROM TARGETMINMAX···0x56711A周期执行
212TARGET LOCKED·····纯显示
213RETARGETCHANCERANGETARGET_SKILL_TARGET··0x559ADF仅 INSTANT
214AGROCHANCE····0x559ADF仅 INSTANT
215SHIELD BREAK EFFECTMINMAX···0x55C495仅 INSTANT
216MODIFY SKILL COOLDOWN REMAININGMINMAX···0x55A486仅 INSTANT
217SET SKILL COOLDOWN REMAININGMINMAX···0x55A4E3仅 INSTANT
218INNATE ICE DEFENSEMINMAX···被动数值
219INNATE FIRE DEFENSEMINMAX···被动数值
220INNATE ELECTRIC DEFENSEMINMAX···被动数值
221INNATE POISON DEFENSEMINMAX···被动数值
222SET SKILL LEVELMINMAX···0x55A314仅 INSTANT
223PULLTIMEEFFECT_DURATION···0x5597A3仅 INSTANT
224PULL RESISTANCEMINMAX···被动数值
225PULL EFFECTTIME····0x569824被动/tick
226MISS CHANCEMINMAX···被动数值
227SKILL ENABLEMINTIMEMAXTIME···0x5626C70x567472挂载/tick
228SKILL DISABLEMINTIMEMAXTIME···0x5626C70x567486挂载/tick
229DRAW HEALTHMIN_PER_HEALTHMAX_PER_HEALTHPULSE_RATERADIUSMAX_TARGETS0x5676F3周期执行
230DRAW MANAMIN_PER_MANAMAX_PER_MANAPULSE_RATERADIUSMAX_TARGETS0x56770B周期执行
231PERCENT DAMAGE TAKEN BY MONSTER COUNTMIN_PERMAX_PERRANGEMAX_COUNT·0x569541周期执行
232REMOVE EFFECTMINMAX···0x55973D仅 INSTANT
233BLIND RESISTANCEMINMAX···被动数值
234PERCENT DAMAGE BONUS BY MONSTER COUNTMIN_PERMAX_PERRANGEMAX_COUNT·0x569554周期执行
235TELEPORT NOPARTICLECHANCE·MINRANGEMAXRANGECOOLDOWN0x55C08C仅 INSTANT
236TELEPORT RANDOM NOPARTICLECHANCE·MINRANGEMAXRANGECOOLDOWN0x55C231仅 INSTANT
237MINIONDAMAGEMINMAXMINFORCEMAXFORCE·纯显示
238CHEATEDMINMAX···周期执行

9. 收敛结果:239 个类型的定性

分类数量含义
已定位到至少一条路径223有明确的消费方或分派分支
机制不看 TYPE158 UNIT THEME,靠 UNITTHEME 键驱动
效果本身无行为246 IDENTIFY164 DISENCHANT,实际功能由别的机制实现
纯显示2212 TARGET LOCKED237 MINIONDAMAGE,只有描述串,无执行路径。237 在 MOD 库里被误用 801 次,见 9.2 节
无消费方4六条路径均无,且原版用量为 0
开发期占位6名字即说明问题
空操作哨兵1193 NONE,按设计什么都不做
仍未定位0239 个类型已全部定性

9.1 逐个交代

#TYPE结论依据
9TO HIT BONUS无消费方原版与 MOD 库用量均为 0,很可能是死类型
11DAMAGE TAKEN无消费方原版用量 0
24PERCENT TO HIT BONUS无消费方原版用量 0
34PERCENT ITEM REQUIREMENTS无消费方原版与 MOD 库用量均为 0;且明确不在 sub_57D930 的需求减免链里
46IDENTIFY效果本身无行为原版 SKILLS/SHARED/IDENTIFY/IDENTIFY.DAT 写 DURATION:INSTANT,但类型 46 在 INSTANT 跳表里落 default(0x55CA77 = 纯 retn)。鉴定功能由技能层实现,这个效果只是标记
58UNIT THEME机制不看 TYPECEffect__ctorFromDat 0x7AAD8C 起无条件读 UNITTHEME 键 → toUpperW → sub_661830 查主题 → 存入 CEffect+0x128(0x7AADDC,无 TYPE 守卫)。消费点在 CUnit__tickActiveEffects_dispatch 0x568184:只判 +0x128 != 0,取 theme+0x18 主题名大写化后配合 CUnit__hasStateNamed。所以 TYPE:UNIT THEME 是装饰性的,真正起作用的是 UNITTHEME 键
164DISENCHANT效果本身无行为原版 UNITS/ITEMS/SCROLLS/DISENCHANT.DAT 写 DURATION:INSTANT,落 INSTANT default。去附魔实际由标志位 0x400 驱动:唯一写入点 CEquipment__markNewEffectsAsEnchanted 0x73E20E,清除方 CEffectMgr__removeEnchantments_disenchant 0x7AF480 按 flags&0x400 与 CAffix__isEnchantment 过滤,与类型 164 无关
212TARGET LOCKED纯增益条标记五个槽全是 NA没有任何数值参数),但四条描述串齐全(Target Locked / Target Locked for [DURATION] seconds)。原版唯一用例 SKILLS/MONSTERS/GRANDREGENT/REGENT_MARK_PULSE_ON_PLAYER.DAT 里,它与一个 TYPE:AGRO 效果成对出现——真正的强制换目标由 AGRO 做(INSTANT 分支 0x559ADF → CCharacter__setTarget),212 只负责在增益条上显示。六条路径均无消费方,cmp [reg+1Ch], 0D4h 全码段无命中。
237MINIONDAMAGE纯工具提示文案 ⚠️超出两个分派器的类型范围(INSTANT ≤236、挂载 ≤228),不可能有分支;无 getStat 消费方;CEquipment__bakeDamageEffectsIntoTable0x590BB0)只认 52 与 10,不认 237。但它有完整描述串(Minions Deal [VALUE] [DMGTYPE] Damage)。原版 9 个效果块分布在 8 个文件,全部是 DUMMY_* 前缀的占位词缀DUMMY_BERSERKER_WOLFSHADE_DAMAGE 等)——这些词缀存在的意义就是让物品提示里出现一行「随从造成 X 伤害」,实际伤害来自随从自己的数据。

9.2 四个值得单说的

58 UNIT THEME 的机制根本不看 TYPECEffect__ctorFromDat0x7AAD8C无条件UNITTHEME 键: 大写化 → sub_661830 按名字查主题 → 结果存进 CEffect+0x1280x7AADDC), 这一路没有任何 TYPE 守卫。消费点在 CUnit__tickActiveEffects_dispatch0x568184: 只判 +0x128 != 0,取 主题对象+0x18 的名字大写化后配合 CUnit__hasStateNamed。 所以真正驱动主题的是 UNITTHEME:TYPE:UNIT THEME 只是习惯写法—— 换成别的 TYPE、只要留着 UNITTHEME 键,主题照样生效。

46 IDENTIFY164 DISENCHANT 的效果本身什么都不做。 两者原版都写 DURATION:INSTANT,但在 INSTANT 跳表里都落 default, 而 default 是纯 retn(第 5 节)。鉴定由技能层实现; 去附魔实际由标志位 0x400 驱动——唯一写入点是 CEquipment__markNewEffectsAsEnchanted0x73E20E), 清除方 CEffectMgr__removeEnchantments_disenchant0x7AF480)按 flags & 0x400CAffix__isEnchantment 过滤,全程与类型 164 无关。

237 MINIONDAMAGE 是纯工具提示文案。 它落在两个分派器的射程之外——INSTANT 的范围是 TYPE 6..236cmp eax, 0E6h), 挂载是 TYPE 7..228cmp ecx, 0DDh),所以 229–238 不可能有挂载分支, 237–238 连 INSTANT 分支也不可能有。它也没有任何 getStat 消费方, CEquipment__bakeDamageEffectsIntoTable0x590BB0)只认 52 与 10。 但它的描述串是完整的:Minions Deal [VALUE] [DMGTYPE] Damage。 原版 9 个效果块散在 8 个文件里,全部是 DUMMY_* 前缀的占位词缀DUMMY_BERSERKER_WOLFSHADE_DAMAGEDUMMY_EMBERMAGE_AVATAR_DAMAGE …)。 它存在的意义就是让物品提示里出现一行「随从造成 X 伤害」,实际伤害来自随从自己的数据。

CAUTION

这是整份数据里代价最大的一处误解。237 在 MOD 库里被写了 801 次(原版只有 9 次,且全是 DUMMY_* 占位词缀)。 照抄的人显然以为它真的会给随从加伤害——它不会,一个数值都不会进任何计算。

想给随从加伤害,要用的是 91 PERCENT PET DAMAGE, 描述串 +[VALUE]% pet and minion Damage。它的读取方式很特别:

asm
.text:0052BF69   call CUnit__getRootOwner   ; ← 随从反向找主人
.text:0052BF6E   test eax, eax
.text:0052BF70   jz   short loc_52BF92      ;   没有主人就整段跳过
.text:0052BF72   push 0
.text:0052BF74   push 7
.text:0052BF76   push 5Bh                   ; ← 类型 91
.text:0052BF7A   call CUnit__getRootOwner
.text:0052BF7F   mov  ecx, eax
.text:0052BF81   call CUnit__getStat        ;   问主人要这个属性
.text:0052BF86   fmul ds:flt_2142654        ;   × 0.01
.text:0052BF8C   fadd [esp+14h+arg_0]       ;   累加进伤害百分比

注意方向:91 要挂在主人身上(玩家的装备/词缀),随从结算伤害时自己回头去取。 挂在随从身上不起作用——getRootOwner 找的是它的主人,不是它自己。

还有一点与 2.1 节相反:91 不按伤害类型分桶。 七处调用传给 CUnit__getStat 的第二个参数恒为 7,而在 CEffectMgr__sumStatOfType_filtered0x7AEA40)里 7 是「不过滤」的哨兵 (合法伤害类型只有 0..6),命中 0x7AEA53 直接返回 239 缓存、连表都不扫。 所以在 91 的效果块上写 DAMAGE_TYPE:FIRE 不会把加成限制成火焰——它照样全额计入。 原版用 19 次、MOD 库 422 次,是有实测支撑的正确写法。

全码段共 7 处读取点,全部核过是指令头,且都紧跟着 CUnit__getRootOwner

函数位置
sub_52BD90(近战)0x52BF760x52C0F4
sub_52C170(远程)0x52C3660x52C4E4
sub_52DA30(近战,第二对镜像)0x52E8C3
sub_52E9D0(远程,第二对镜像)0x52F860
CUnit__applyEffectToSelf_dispatch0x563E20

两对镜像函数的关系见 2.2 节。

212 TARGET LOCKED 是纯增益条标记。 五个槽全是 NA——一个数值参数都没有,但四条描述串齐全 (Target Locked / Target Locked for [DURATION] seconds)。 原版唯一用例 SKILLS/MONSTERS/GRANDREGENT/REGENT_MARK_PULSE_ON_PLAYER.DAT 里, 它和一个 TYPE:AGRO 效果写在同一个 [EFFECTS] 块中—— 真正的强制换目标由 AGRO 完成(INSTANT 分支 0x559ADFCCharacter__setTarget), 212 只负责在玩家的增益条上挂图标和文字。

TIP

这两个类型可以当成**「只写文案、不产生效果」的展示位**来用: 挂上去会正常进增益条/物品提示,[VALUE][DURATION] 占位符照常渲染, 但引擎里没有任何代码会因为它们改变游戏状态。 212 尤其干净——零参数、可设时长、可 EXCLUSIVE 刷新。 237 不建议这么用:它的文案写死了「随从造成 X 伤害」,已经有 801 个 MOD 块把它当真属性写,再加只会加深误解。

反过来也要知道:引擎自己同样读不到它们CUnit__hasActiveEffectOfType 对这两个类型没有调用点, 所以不能指望拿它们去当别的原版机制的触发条件。

WARNING

扫描范围的诚实说明。 消费方是这样定位的:枚举 CUnit__getStat 的 371 个调用点以及三个额外包装器 (CBaseUnit__getEffectStat_selfPlusInventorysub_5188F0sub_512C40)的调用点, 逐点回读紧邻的 push 立即数;再加上按类型的布尔查询入口、直接扫表函数, 以及从数据侧反查原版用法定位子系统。 用 cmp 立即数做的补充搜索受工具单次 20 万条指令上限限制,分段跑过 0x4010000x7D0000, 不能保证 100% 无遗漏。 表中「被动/扫表/布尔」三列的空白应读作**「本次未定位」**,而不是「一定没有消费方」。 INSTANT 与挂载两列是读表得到的完整映射,可当作确定结论。

9.3 十六个零用量类型的死因

上一节回答的是「这个类型有没有代码」,这一节回答「为什么没人用」。 用量是数出来的:原版 E:/Torchlight 2/MEDIA 下 16084 个 .DAT、共 9984 个 [EFFECT] 块;MOD 库 D:/WizardProject/TL2 下 157950 个 .DAT、共 200352 个块。下面 16 个类型在两边都是 0

IMPORTANT

「零用量」和「没实现」是两件事,这 16 个里只有 6 个真的没有代码。 另外 7 个实装完整,剩下 3 个是实现留了洞。

死因数量类型
被后来的分类版取代134 PERCENT ITEM REQUIREMENTS
整个子系统没做19 TO HIT BONUS
开发期玩笑4130 EMOTIONAL DAMAGE132 JAUNTINESS133 SOCKS134 LOGIC RESISTANCE
实现有洞3147 ADD STAT TO LEVEL149 SET STAT ON LEVEL151 CLEAR STAT ON LEVEL
实装完整、原版没用642 PERCENT VIEW ANGLE BONUS63 VELOCITY MULT160 SPAWN UNIT186 TRANSFORMATION TIME187 TRAP DISARM CHANCE208 CAST SKILL ON RANGED SKILL STRIKE
引擎只读不写1238 CHEATED

逐个交代:

#TYPE死因依据
34PERCENT ITEM REQUIREMENTS被后来的分类版取代93/94/95/98/100/101 六个按 UNITTYPE 分类的减需求类型接管了它的功能;34 这个通用百分比版没人再读,描述串却留在表里
9TO HIT BONUS整个子系统没做命中率系统在成品里不存在(闪避/格挡/失手取而代之)。9 的四条描述串也全是空的,所以它既不显示也不生效——在 6 个「真的没有代码」的零用量类型里只有它是这样
130EMOTIONAL DAMAGE开发期玩笑[VALUE] Emotional Damage
132JAUNTINESS开发期玩笑+[VALUE] Jauntiness
133SOCKS开发期玩笑[VALUE] Socks
134LOGIC RESISTANCE开发期玩笑+[VALUE] Resistance to Logic
147ADD STAT TO LEVEL实现有洞写关卡自己的 CStatManagerCLevel+0x350),但缺了 146/148/150 都有的空指针门
149SET STAT ON LEVEL实现有洞同上,0x55BC86
151CLEAR STAT ON LEVEL实现有洞同上,0x55BCF8
42PERCENT VIEW ANGLE BONUS实装完整、原版没用CUnit__getViewAngleBonus0x51B540)真读,结果流进 AI 索敌
63VELOCITY MULT实装完整、原版没用同时占三条路径:数值查询、布尔查询、挂载分支
160SPAWN UNIT实装完整、原版没用三条路径都有分支:INSTANT 0x55C3B6、挂载 0x5640D0、tick 0x569360
186TRANSFORMATION TIME实装完整、原版没用CMonster_tick_main 里除变身倒计时的每帧减量
187TRAP DISARM CHANCE实装完整、原版没用陷阱触发前掷骰,掷赢就不触发
208CAST SKILL ON RANGED SKILL STRIKE实装完整、原版没用安装/触发/执行/卸载四段链齐全
238CHEATED引擎只读不写6 处 hasActiveEffectOfType(0xEE),命中就置全局标志位;但二进制里根本没有 CHEATED 这个字符串,没有任何代码路径能造出它

「被取代」只有一个,但很典型。
34 PERCENT ITEM REQUIREMENTS 想做的事——按百分比减物品需求—— 最后是由六个按装备类别拆开的类型完成的: 93 REDUCED / 94 ARMOR / 95 SPELL / 98 MARTIAL / 100 RANGED / 101 MAGIC, 六个都有消费方(2.3 节的 sub_57D930UNITTYPE 选型)。 34 被留在表里没删,描述串还是完整的——这正是下一节要说的问题。

「实现有洞」是这次新查出来的一类。
146/148/150(ADD/SET/CLEAR STAT)与 147/149/151(…ON LEVEL)是同一段后端的两套入口, 唯一区别是后者把 CStatManager 换成关卡自己的那个

asm
mov ecx, esi
call CUnit__getLevel        ; ← 可能返回 0
mov ecx, [eax+350h]         ; ← 没有 test eax, eax
call <与 146/148/150 相同的后端>

146/148/150 在掷骰之后有一道 cmp dword ptr [esi+114h], 0 / jz 空指针门, 147/149/151 一道都没有。承受者当前没有有效 CLevel 的任何时机 (过图、单位刚构造、存档回放、远端单位)踩到这三个 case 就是空指针解引用。 所以它们不是「没启用的特性」,是能用但会崩

TIP

代价是这三个类型本来很有价值:CLevel+0x350 确实是个 CStatManager, 而且随关卡存档持久化DungeonMgr_SaveLevelState 会遍历它、 对 def+0x98 为真的条目序列化)。想做「这一层地图记住玩家做过什么」 这类玩法,这是引擎里现成的唯一入口。要用就得自己保证承受者当时在关卡里。

9.4 「只有 UI 显示、没有实际效果」——对其中 5 个成立

这个猜想是对的,而且可以直接证明。工具提示的生成在 CEffect__build_tooltip_desc0x7A70B0),取描述串那一段是:

asm
.text:007A7371   cmp     [esp+160h+var_14A], 0   ; 是否「持续型」
.text:007A7376   mov     eax, [esi+1Ch]          ; ← CEffect+0x1C,就是 TYPE
.text:007A7382   jz      short loc_7A73AF
.text:007A7384   cmp     [esp+160h+var_10C], 0   ; GOOD 还是 BAD
.text:007A738B   lea     ecx, ds:0[eax*8]
.text:007A7392   sub     ecx, eax                ; ecx = TYPE * 7
.text:007A7394   lea     eax, g_EffectGoodDesOT_239[ecx*4]  ; *4 → TYPE * 28

28 是这个构建里 std::wstring 的大小,所以整段就是 desc = 数组[TYPE]。 四张表按 2×2 选:

普通持续型(OT)
GOODg_EffectGoodDes_239 0x28A4300g_EffectGoodDesOT_239 0x28A5D28
BADg_EffectBadDes_239 0x28A7750g_EffectBadDesOT_239 0x28A9178

四张表都在 EffectsList_dat_load_2390x401000)里按行填充(GOODDES 0x401157GOODDESOT 0x4011B0BADDES 0x401209BADDESOT 0x401262), 读取端只有 CEffect__build_tooltip_desc 一处。

关键在于:这段代码里没有任何「该类型有没有消费方」的检查。 只要 EFFECTSLIST.DAT 那一行写了描述串,提示就照常渲染、[VALUE] 照常替换。 于是「有文案、没代码」的类型在游戏里表现为——装备栏文字照显示,数值毫无作用

#TYPE会显示的文案
34PERCENT ITEM REQUIREMENTS[VALUE]% reduced item level requirements
130EMOTIONAL DAMAGE[VALUE] Emotional Damage
132JAUNTINESS+[VALUE] Jauntiness
133SOCKS[VALUE] Socks
134LOGIC RESISTANCE+[VALUE] Resistance to Logic

剩下那个 9 TO HIT BONUS 的四条描述串也全是空的,所以它既不显示也不生效—— 上面 6 个「没有代码」的零用量类型里,只有它是两头皆空。

NOTE

描述串全空本身不说明问题。239 行里有 28 个类型四条描述串都是空的, 其中 25 个有正常的消费方——158 TELEPORT141 OPEN PORTAL146 ADD STAT232 REMOVE EFFECT 这类动作型效果本来就不需要在提示里占一行。 只有当「无描述串」与「无消费方」同时成立时,才说明这一行是彻底的死条目; 符合的只有 9(另外 58UNITTHEME: 键生效、193 是设计上的空操作,都不算)。

9.5 真正的陷阱:有人在写、引擎不处理

零用量类型至少没人踩。用量不为零、却同样查不到消费方的有 10 个,按两库合计用量排序如下。 这 10 个不是一回事,看最后一列区分:58 靠同块的另一个键真的生效、193 是设计上的空操作, 这两个不算 bug;212237 是有意为之的展示位;剩下 6 个 (112446131135164)写了就是白写。

#TYPE原版MOD 库为什么不生效
58UNIT THEME2163178机制由同块的 UNITTHEME: 键驱动,与 TYPE 无关(9.2 节)
237MINIONDAMAGE9801纯工具提示文案;原版 9 处全是 DUMMY_* 占位词缀(9.2 节)
193NONE19275拼错 TYPE 的落点,按设计什么都不做
11DAMAGE TAKEN070百分比版 26 PERCENT DAMAGE TAKEN 有消费方(0x51ADF0,原版用 389 次),这个绝对值版没有
131FUTURISTICNESS021开发期玩笑(+[VALUE] Futuristicness),却被 MOD 写了 21 次
212TARGET LOCKED117纯增益条标记,真正换目标的是同块的 AGRO(9.2 节)
24PERCENT TO HIT BONUS015与 9 同族,命中率系统不存在
164DISENCHANT15落 INSTANT default;去附魔由标志位 0x400 驱动(9.2 节)
46IDENTIFY22落 INSTANT default(纯 retn);鉴定由技能层做
135NOODLE ARMS10开发期玩笑([VALUE] Noodle Arms)——原版自己也写了 1 次

237 MINIONDAMAGE 是这里面代价最大的一个:MOD 库里 801 个块。 原版那 9 处全都叫 DUMMY_*,本来就只是为了让提示里多一行字(9.2 节); 照抄的人多半以为它真的会给随从加伤害。

131 FUTURISTICNESS 被 MOD 写了 21 次,135 NOODLE ARMS 原版自己也写了 1 次—— 开发期玩笑类型混进正式数据,这件事双方都发生过。

拼错 TYPE 的情况也数了。 写了表里不存在的名字, 构造函数的初值 0xC10x7AAA7E)不会被覆盖,整个块静默变成 193 NONE

  • 原版:1 处CAST SKILL AFTER TIME,在 SKILLS/MONSTERS/FUNGUS/FUNGUS_ELECTRIC_EXPLODE/FUNGUS_ELECTRIC_GIVE_EXPLODE.DAT
  • MOD 库:206 处,20 种不同的错误写法,CAST SKILL ON KILL AT TARGE(66)、PERCENT DAMAGE(49)、PERCENT DEGRADE ARMOR(30)、PHYSICAL(28) 居前。

原版那处值得看一眼——电击真菌的延迟爆炸:

[EFFECT]
	<STRING>NAME:Fungus_electric_explode2
	<STRING>ACTIVATION:DYNAMIC
	<STRING>TYPE:CAST SKILL AFTER TIME
	<BOOL>SAVE:true
	<BOOL>EXCLUSIVE:true
	<FLOAT>MINTIME:2
	<FLOAT>MAXTIME:3
	<FLOAT>MINCHANCE:100
	<FLOAT>MAXCHANCE:100
[/EFFECT]

CAST SKILL AFTER TIME 不在 239 行里——21 个 CAST SKILL ON x 没有一个是按定时器触发的。 MINTIME/MAXTIME 这两个键说明作者要的是「2–3 秒后放技能」, 而引擎从来没提供过这个类型。这个块实际变成 193 NONE,什么都不做。

9.6 七个实装完整、可以直接用的

这批是 16 个里最有价值的部分——代码齐全,只是原版一次没用。

42 PERCENT VIEW ANGLE BONUS(绝对值版 14 VIEW ANGLE BONUS 原版用 1 次)
CUnit__getViewAngleBonus0x51B540):

视野角度 = (getStat(42) + 1.0) × (CUnit+0x338 + getStat(14))

唯一调用方是 CCharacter__findAITarget0x52243C),也就是 AI 索敌的视锥。 注意函数里还有一道短路:AI 管理器 flag 0 置位时直接返回 360.00x51B592), 即「全向感知」,这时 42 不起作用。

186 TRANSFORMATION TIME
CMonster_tick_main0x56AEAB / 0x56AED2

[CUnit+0x76C] -= dt / (1.0 + (自身的186 + 随从[0]的186) / 100)

CUnit+0x76C 是变身剩余时间,哨兵值 -999.0flt_213C534)表示「没在变身」。 除数在分母上,所以值越大倒计时走得越慢——与描述串 Transformation time is increased by [VALUE]% 一致。 它会把自己和第一个随从的值加起来,这是 186/187 共用的一个惯用法。

187 TRAP DISARM CHANCE
CUnit__triggerTrapSkill_rollDisarm0x5C8930,本次命名):

chance = getStat(受害者, 187) + getStat(受害者的随从[0], 187)
if rand_float(0, 100) > chance:  → 陷阱照常触发 CSkillList__executeSkill
else:                            → 什么都不发生(拆掉了)

前置是技能对象 +0x1FC 的标志位为真(0x5C8979),也就是「这个技能是陷阱」。 触发成功且受害者是玩家时还会 CUnit__addPlayerStatistic(10, 1) 记一笔。 原版没有任何词缀提供这个属性——「拆陷阱几率」是引擎做好了但没上架的一整条属性线

208 CAST SKILL ON RANGED SKILL STRIKE
21 个 CAST SKILL ON x唯一一个两库合计用量为 0 的(兄弟类型 209 …FROM TARGET MOD 库还有 3 次)。四段链一段不缺:

环节位置
安装CBaseUnit__registerCastSkillEffect_vt91(虚表槽 91)真的建出技能对象
触发CCharacter__resolveStrikeHit0x54E31F
执行施法模式 0(自己施法,带 CUnit+0x7DC 一秒递归防护)
卸载tick OR 链 0x5671570x5671B9

63 VELOCITY MULT:数值查询(CUnit__integrateMovement 0x542780)、 布尔查询(0x51A6310x542EC9)、挂载分支(0x563BD3)三条路径都占。

160 SPAWN UNIT:INSTANT(0x55C3B6)、挂载(0x5640D0)、tick(0x569360)三处都有分支。 INSTANT 分支的前置是 [CEffect+0x30] & [CEffect+0x34] != -1(要有可解析的单位 GUID) 加 CUnit__isLocallyAuthoritative,然后读槽 4、3、0。

238 CHEATED 不在此列。 它是完整实现的,但方向相反—— 引擎只不写:sub_5C0B30 里 6 处 hasActiveEffectOfType(0xEE)0x5C114D0x5C119C0x5C11D70x5C122C0x5C12580x5C12AC)逐件清点装备, 命中就 dword_331B3A0 |= 0x20 置全局标志。 而整个二进制里没有 CHEATED 这个字符串(宽窄两种编码都搜过), 说明没有任何代码路径能构造出这个效果——检测逻辑随成品发布了,触发它的数据没有。

10. 排错

「加了一个属性加成效果,游戏里没反应。」 查第 8 节该类型的前三列。若三列全空且三个分支列也空,先看第 9 节—— 它可能是开发期占位,也可能只是本次未定位。

「写了 DURATION:INSTANT,效果不触发。」 查「INSTANT 分支」列。空白就是空操作,175 个类型都是这样。

DAMAGE_TYPE 写了但伤害加成没生效。」1025 按伤害类型分七个桶记账(2.1 节)。写错桶等于加成落在没人取的地方。

「近战有效,远程没效(或反过来)。」 两条结算路径是镜像的独立函数(2.2 节),基础伤害类型分别读类型 0 和类型 1。

「堆了很多弹射反射,还是被打中。」 反射概率按独立事件合成,永远到不了 100%(第 3 节)。

「同样的效果挂装备上有用、放技能里没用。」 挂载路径与数值查询路径是两条独立的路。只有挂载分支的类型在挂上瞬间执行一次,与时长无关。

11. 边界与未完成的部分

11.1 数据快照

文中所有「用量」「块数」都是数出来的,不是估计。两个来源的规模不一样:

来源.DAT 文件可解析 [EFFECT]
原版 E:/Torchlight 2/MEDIA160849984
MOD 库 D:/WizardProject/TL2157950200352

IMPORTANT

原版侧在整个分析期间保持不变;MOD 库是会变的。 本系列早期的几次扫描曾读到 165715 个 MOD 文件 / 201923 个块, 与现在相差 7765 个文件 / 1571 个块,82 个类型的计数因此变动过。 三篇文章的用量数字已全部重算并统一到上表这一次扫描, 但如果你在自己的库上复现,数字不会完全一致——结论不依赖具体数值, 依赖的是「是否为 0」和数量级。

受这次重算影响最大的一条:135 NOODLE ARMS 的 MOD 用量从 1 变成 0, 现在只剩原版自己那 1 处误用。

11.2 已经做到的层次

  • 239 个类型全部定性(第 9 节),无未定位项。
  • 四张跳转表逐字节解出:INSTANT(0x559736)、挂载(0x562CE0)、 施法模式(0x53CDA5)、强制 Magical(0x7AAC03)。
  • 分支语义给到「关键调用序列 + 一句话」这一层,没有逐条还原完整控制流与数值公式。 判断「这个类型做什么、怎么写才生效」够用;要精确复刻公式仍需逐段细读。

11.3 明确未解决的

  • CEffect 的分配器没追。 DAMAGE_TYPE 名字匹配全不中时(2.5 节), 0x7AB545 的写入被跳过,+0x14 保持分配时的内容。基类构造 sub_405910 不碰这个字段,函数内也只有两处写入点——但这块内存原本是什么,本次没查。
  • MINLEVELMAXLEVELAFFIX 在效果块里的归属未定论(2.6 节)。 它们可能来自嵌套子块被扫描一并计入,也可能确实是有人误写。
  • CSkillEvent__ctorFromDat0x6CEE60)也读 DAMAGE_TYPE0x6CF619), 技能事件那一侧的伤害类型语义本文没有展开。
  • 「本次未找到消费方」不等于「一定没有」。 这个限定同时适用于 第 8 节的三列空白、2.5 节的 90 个类型、2.6 节的丁类。 扫描方法与其边界写在各节末尾的提示框里。

本文覆盖的范围:六条路径的分派映射、79 个分派分支、tick 的两条 OR 链与 23 个动作块、 CAST SKILL ON x 的安装/触发/执行/卸载四段链、五种施法模式、锥形查询的实参与公式、 两个 tick 实现体、DAMAGE_TYPE 的解析与消费、[EFFECT] 块全部键名的对账——都已成表。