本文回答什么:
[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 个类型 |
| 4 | INSTANT 执行 | CUnit__executeInstantEffect (0x559690) | 56 个有分支,175 个落空 |
| 5 | 挂载时执行 | CUnit__applyEffectToSelf_dispatch (0x562590) | 28 个有分支,194 个落空 |
| 6 | 每帧 tick | CUnit__tickActiveEffects_dispatch (0x566DD0) | 45 处比较,45 个类型 |
IMPORTANT
路径归属就是「DURATION 该怎么填」的答案。
- 只有路径 1/2/3 → 效果必须活着才有用:给持续时长,或挂在装备/词缀上当永久加成。写
INSTANT等于没写。 - 只有路径 4 → 写
INSTANT。给它时长不会让它反复触发。 - 有路径 6 → 需要持续时长,存活期间按帧被检查。
- 有路径 5 → 挂上去那一刻执行一次,与时长无关。
1.1 两个跳转表是怎么解出来的
两个分派器都编译成 MSVC 的「字节索引表 + 跳转表」双层结构,可以直接读表得到完整映射。
; 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+0x60 的 float statCache[239],下标就是类型号—— 这一步对所有类型无差别执行。真正决定行为的是有没有代码去查。
查询走 CUnit__getStat(类型号, 伤害类型, 是否向上取整),全二进制 371 个调用点。 调用约定是 push 取整标志; push 伤害类型; push 类型号; call——紧邻 call 的 push 就是类型号。
除 CUnit__getStat 外还有三个包装器直接调 sumStatOfType_filtered: CBaseUnit__getEffectStat_selfPlusInventory(0x518930,16 个调用点,15 个在 CCharacter__resolveStrikeHit 里)、 sub_5188F0、sub_512C40。漏掉这几个包装器会让一整批类型看起来「没人用」—— 吸血、偷蓝、击退、腐蚀护甲全在那 16 个点里。
WARNING
sumStatOfType_filtered 有三条需要知道的行为:
伤害类型 == 7是「不过滤」哨兵(合法值只有 0–6)。走这条直接返回statCache[类型号],不扫表。statCache[类型号] == 0时直接返回 0,同样不扫表。缓存陈旧时带过滤的查询会返回 0。- 只扫 PASSIVE 与 DYNAMIC 两张表,TRANSFER 不计入(见 CEffectManager)。
2.1 伤害加成是七个桶,不是一个数
10 DAMAGE BONUS 与 25 PERCENT DAMAGE BONUS 按伤害类型 0–6 各记一份账, 效果的 DAMAGE_TYPE 决定进哪个桶,伤害结算逐桶取:
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_stat26(0x51ADF0) 是「按传入伤害类型查一次 + 桶 6 查一次」,桶 6 就是那个「对所有元素通用」的桶。
2.2 近战与远程是一对镜像函数
sub_52DA30 与 sub_52E9D0 各 0xF95 字节,结构逐行同构,区别只在基础伤害类型: 近战读 getStat(0, …)(MELEEDAMAGEBONUS),远程读 getStat(1, …)(RANGEDDAMAGEBONUS) (0x52F53D 处 push 0; push 7; push 1)。 sub_52BD90 / sub_52C170 是另一对同样的镜像。改一边不改另一边,近战和远程的行为就会不一致。
2.3 物品需求减免是按 UNITTYPE 选型的
sub_57D930(另有 0x57DA30/0x57DB30/0x57DC30 三份副本)先无条件取 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 FIRE、case 3 → 218 INNATE ICE、case 4 → 220 INNATE ELECTRIC、case 5 → 221 INNATE POISON。
2.5 DAMAGE_TYPE 只有六个类型真的会读
DAMAGE_TYPE 是数值查询路径的过滤参数,存在 CEffect+0x14。 它不是每个效果都有用——绝大多数类型写了等于没写。先看这个键是怎么进来的。
七个合法值,运行时构造。 名字表在 byte_3B9CAB0,由静态构造函数 sub_1F876C0 填:
| 序号 | 名字 | 序号 | 名字 | |
|---|---|---|---|---|
| 0 | Physical | 4 | Electric | |
| 1 | Magical | 5 | Poison | |
| 2 | Fire | 6 | All | |
| 3 | Ice |
7 不是合法值,是「不过滤」的哨兵。CEffectMgr__sumStatOfType_filtered(0x7AEA40)第一行就是 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_1F9 由 0x7AAC03 的跳转表决定。这张表本身已经在 数值槽对照表 §7 的「特判」列里记过(代号 M, 那边引的是索引计算处 0x7AABEF)——本节补的是它的逐字节解码与数据侧后果。
它是本系列解出的第四张跳转表(前三张是 INSTANT、挂载、施法模式): 索引表 idx_forceMagical_117(0x7ABB54)共 117 项、索引式为 TYPE - 2(覆盖 TYPE 2..118), 跳转表 jpt_7ABB4C 两项:slot 0 = 0x7AAC0A(置位)、slot 1 = 0x7AAC0F(默认,不置位)。 逐字节解出落 slot 0 的九个类型,与 IDA 解析的 case 列表一致:
| # | TYPE 名 | 数据里写了 DAMAGE_TYPE 的块数(原版 / MOD) |
|---|---|---|
| 2 | DEFENSE | 0 / 0 ——(无人写) |
| 17 | PERCENT DEFENSE | 0 / 6 |
| 35 | PHYSICAL DEFENSE | 0 / 0 ——(无人写) |
| 37 | FIRE DEFENSE | 43 / 298 |
| 38 | ICE DEFENSE | 42 / 302 |
| 39 | ELECTRICAL DEFENSE | 43 / 314 |
| 40 | POISON DEFENSE | 38 / 349 |
| 86 | DEFENSE SPELL BONUS | 0 / 0 ——(无人写) |
| 118 | PERMANENT DEFENSE | 0 / 0 ——(无人写) |
九个全是 DEFENSE 家族。对它们,+0x14 被无条件写成 1(Magical), DAMAGE_TYPE: 那一行连读都不读。 原版自己在这九个类型上写了 166 处 DAMAGE_TYPE,MOD 库 1269 处,合计 1435 处全部无效。
NOTE
这条也顺带解释了一处死代码。CUnit__getEffectiveVitality(0x51B040)里有两次读:
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 干脆不被写入
匹配循环跑完七项都没命中时,0x7AB545 的 mov [esi+14h], eax 被跳过。 基类构造 sub_405910(0x405910)只设 vftable 和 this[1],也不碰 +0x14; 函数内对 [esi+14h] 的写入总共只有两处(0x7AB4DB 与 0x7AB545)。 未追分配器,所以无匹配时该字段的实际取值取决于这块内存原本是什么。
实测数据里出现的非法值:
| 值 | 出现在 | 原版 | MOD 库 | 影响 |
|---|---|---|---|---|
MAGIC | 52 DAMAGE | 0 | 168 | 该类型真读 —— 合法名字是 MAGICAL |
MAGIC | 26 PERCENT DAMAGE TAKEN | 0 | 8 | 该类型真读 |
ELECTRICAL | 39 ELECTRICAL DEFENSE | 0 | 10 | 该类型本就强制 Magical |
ELECTRICAL | 10、25 | 0 | 2 | 该类型真读 —— 合法名字是 ELECTRIC |
| 空值 | 22、55、25 | 4 | 2 | 25 那条该类型真读 |
POSION / POITION / POSITION | 52、25、38 | 0 | 4 | 拼错 POISON |
DAMAGE_TYPE:MAGIC 是这里面最值得说的:合法名字是 MAGICAL, 少一个 AL 就整条匹配不上。MOD 库里 168 个 52 DAMAGE 块这么写—— 而 52 是真的会按伤害类型分桶的类型(CEquipment__bakeDamageEffectsIntoTable)。
六个真读的类型
其余情况下 +0x14 只有在消费方传了非 7 的过滤值时才有意义。逐调用点核实后, 只有六个类型存在这样的消费方:
| # | TYPE 名 | 读取点 | 数据里的用量(原版 / MOD) |
|---|---|---|---|
| 8 | ARMOR BONUS | CBaseUnit__getResistanceOfType 0x520AF6(变量)/0x520B21(6);另 0x5216F6 | 53 / 1350 |
| 10 | DAMAGE BONUS | 近战/远程两对镜像函数共 8 处,如 0x52E057、0x52E68A | 97 / 3489 |
| 23 | PERCENT ARMOR BONUS | CBaseUnit__getResistanceOfType 0x520AB8(变量)/0x520ACC(6) | 56 / 2050 |
| 25 | PERCENT DAMAGE BONUS | 两对镜像 + 挂载分派共 16 处,如 0x52BF4E、0x563DD4 | 327 / 6759 |
| 26 | PERCENT DAMAGE TAKEN | 0x51AD81、0x51AE10、0x522AF1 | 325 / 5628 |
| 52 | DAMAGE | 不走 getStat —— CEquipment__bakeDamageEffectsIntoTable(0x590BB0)直接读 +0x14 | 999 / 17150 |
8/23 的用法见 2.4 节的 getResistanceOfType——每种抗性都做两次读: 一次按具体伤害类型(变量),一次按 6(All),两者相加。 所以在护甲类效果上写 DAMAGE_TYPE:FIRE 只加到火抗,写 ALL 才是全抗。
汇总
两库合计 48263 个 [EFFECT] 块写了 DAMAGE_TYPE,分布在 101 个类型上:
| 分类 | 类型数 | 原版 | MOD 库 | 合计 |
|---|---|---|---|---|
| 真的会读 | 6 | 1857 | 36426 | 38283 |
| 被强制成 Magical、键不读 | 5 | 166 | 1269 | 1435 |
键进了 +0x14,但没有消费方 | 90 | 372 | 8173 | 8545 |
| 合计 | 101 | 2395 | 45868 | 48263 |
也就是说 9980 个块的 DAMAGE_TYPE 是白写的, 其中原版自己占 538 处。最典型的是 37 FIRE DEFENSE 写 DAMAGE_TYPE:FIRE—— 原版写了 43 次,一次都不生效。
WARNING
「没有消费方」这一列的完整度受扫描方法限制。判定依据是: 逐字节找出所有 push 6; push <类型> 形态的 getStat 调用并验证调用目标(这是「All 桶」的固定写法,过滤式读取必然成对出现其一), 加上直接读 +0x14 的 CEquipment__bakeDamageEffectsIntoTable, 以及 0x7AAC03 那张强制表。 若某个类型的消费方用了别的写法(例如伤害类型全程只以寄存器传递、且不配 6 那一次), 本次可能漏掉。这一列应读作「本次未找到消费方」。
2.6 [EFFECT] 块里写了、但没人读的键
上一节只查了 DAMAGE_TYPE 一个键。同样的问题对所有键都成立,索性一次查完。
哪些键会被读,有两个来源:
CEffect__ctorFromDat(0x7AAA30–0x7ABB4B)里所有push offset的键名字面量,共 24 个:TYPEACTIVATIONDURATIONNAMELEVELICONLINK_NAMEPARTICLE_FXUNITTHEMESAVEEXCLUSIVENONMAGICALREMOVEONOWNERDIEUSEOWNERLEVELDISPLAYPOSITIVEMODIFIERISBONUSDISPLAYMAXMODIFIERNOGRAPHGRAPHOVERRIDESOAKSCALEDAMAGE_TYPESTATMODIFYNAMESTATMODIFYPERCENTSTAT_SOURCE_TYPEEFFECTSLIST.DAT里那一行给出的五个槽位别名(239 行去重后共 62 个不同名字, 见 数值槽对照表)。
数据侧:两库的 [EFFECT] 块里一共出现过 98 种键名,其中 84 种能对上上面两个来源, 14 种对不上。逐个查证后分成四类:
甲、二进制里根本没有这个字符串 —— 必然无效
宽窄两种编码都搜过,全码段无此串,所以不可能有任何代码读它。
| 键 | 出现次数 | 备注 |
|---|---|---|
EFFECTSHIDDEN | 32 | |
DAMAGETYPE | 27 | 少一个下划线。二进制里只有 PRIMARY_DAMAGETYPE,0x218E57C 那处匹配是它的尾巴 |
EXCLUS0VE | 5 | EXCLUSIVE 把字母 I 打成了数字 0 |
DONTPLAYPARTICLES | 3 | |
MINCHANCE | 2 | |
MAXCHANCE | 2 | |
| 空键名 | 3 | <FLOAT>:值 这种写法 |
合计 74 处。
CAUTION
DAMAGETYPE 这 27 处全部落在 52 DAMAGE 上、值全是 ELECTRIC,而且没有一个同时写了正确的 DAMAGE_TYPE。其中 2 处是原版: SKILLS/CHAMPIONS/CHARGEDBOLT.DAT 与 SKILLS/PROCS/CHARGEDBOLT.DAT。
后果是实打实的。键名不匹配 → getString("DAMAGE_TYPE", "PHYSICAL") 拿到默认值 → CEffect+0x14 被写成 0(Physical)。而 52 DAMAGE 的 INSTANT 分支在 0x55A957 读这个字段:
.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也就是说这两个原版技能的伤害去查的是物理加成桶,而不是电击桶。
乙、字符串存在,但读它的函数不碰效果节点
| 键 | 出现次数 | 谁在读 | 结论 |
|---|---|---|---|
SOAKSCALEPCT | 30 | CEquipment__loadCombatStatsFromDat、CSkillEvent__ctorFromDat | 装备与技能事件的键,效果解析器不读 |
VALUE3 | 19 | 只被 EffectsList_dat_load_239(0x40136C)读 | 那是 EFFECTSLIST.DAT 的列名,不是效果块的键 |
VALUE1 | 2 | 只被 EffectsList_dat_load_239(0x4012BB)读 | 同上 |
合计 51 处。 VALUE1/VALUE3 这一类很好理解——把「表里那一行的列名」当成了「效果块里能写的键」。 正确写法是查该类型的槽位别名(MIN/MAX/CHANCE/…),别名逐行不同。
丙、由 CEffect__ctorFromDat 的调用方在同一个节点上读 —— 有效
TARGET(848 处)不在效果解析器的 24 个键里,但它确实会被读: sub_7C0FA0(0x7C10B1)与 CSkillEffectAndAffixes__ctorFromDat(0x6C6B8C 等三处) 都从同一个数据节点取这个键,而这两个函数正是 CEffect__ctorFromDat 的调用方。 键的归属不等于「谁 new 了这个对象」,这一类不算白写。
丁、未定论
MINLEVEL(254 处)与 MAXLEVEL(244 处)在二进制里存在, 但引用它们的只有 ItemDB__buildIndexFromDats(0x65EBD1/0x65EC4F)与 sub_660A00, 都与效果节点无关。不过这两个键也可能是嵌套子块的键被本次扫描一并计入了 (扫描取的是 [EFFECT] 到最近一个 [/EFFECT] 之间的全部 <类型>键:值 行), 所以这里不下结论。AFFIX(1 处)同理。
NOTE
甲、乙两类合计 125 处是可以确定白写的。 相比 2.5 节 DAMAGE_TYPE 一个键就有 9980 处无效,键名写错反倒是小问题—— 真正的大头是键名写对了、但那个类型压根不读它。
3. 路径 2:直接扫表
CEffectMgr__accumMissileReflect(0x7AE710)不走 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__hasActiveEffectOfType(0x512C90,20 个调用点)、 CEffectMgr__hasEffectOfType(0x7AE100)、CEffectMgr__findEffectByTypeAndName(0x7ADEE0)。
| # | TYPE 名 | 查询点 |
|---|---|---|
| 25 | PERCENT DAMAGE BONUS | 0x532738、0x5328ED、0x591060、0x7A64BA (CEffectMgr__findEffectByTypeAndName) |
| 26 | PERCENT DAMAGE TAKEN | 0x5328DE |
| 63 | VELOCITY MULT | 0x51A631、0x542EC9 (CUnit__hasActiveEffectOfType) |
| 79 | FLEE EFFECT | 0x5AFCD3 |
| 80 | TURN ALIGNMENT | 0x536B96、0x536C7C、0x53C6BE、0x53DD08、0x5506B8 |
| 82 | SKILL BONUS | 0x7ADC4D sub_7ADC10、0x7A7A90 CEffect__build_tooltip_desc |
| 113 | WARP | 0x5C930D |
| 122 | SHIELD BUFFER | 0x51FED8 CUnit__consumeAbsorbBuffers_type122、0x525488 CUnit__canApplyEffectGate_vt83 |
| 128 | SILENCE | 0x6BFB9C CSkill__canUse —— 沉默时技能不可用 |
| 129 | DESUMMON ON DEATH | 0x53BBDC、0x54E8CC CUnit__onDeath_vt120 |
| 161 | SPAWN UNIT ON DEATH | 0x53C8F7 CBaseUnit__execProcCastSkill |
| 163 | DESTRUCTABLE SHIELD | 0x581715 |
| 166 | REMOVE TRIGGERABLE | 0x564248、0x58343A |
| 177 | EXPLODE ON DEATH | 0x54E568 CUnit__onDeath_vt120 |
| 196 | IMMOBILIZE | 0x7B9E13 |
| 238 | CHEATED | 0x5C114D、0x5C119C、0x5C11D7、0x5C122C、0x5C1258、0x5C12AC —— 六处检查,CHEATED 并非惰性类型 |
NOTE
238 CHEATED 有六处检查点(0x5C114D 起),它不是无用的开发期残留, 而是一个真正被读取的标记。同理 128 SILENCE 在 CSkill__canUse(0x6BFB9C)里被查, 这就是沉默让技能不可用的实现。
5. 路径 4:INSTANT 执行
DURATION:INSTANT 的效果不进三张表、不参与 tick,执行一次就丢。 寄存器约定:esi = 被作用单位,edi = CEffect*。56 个类型有真分支:
| 处理地址 | 类型 | 族 | 关键调用 | 做什么 |
|---|---|---|---|---|
0x55973D | 232 REMOVE EFFECT | CEffectMgr__removeEffectsByName | 按名字移除效果(不是按类型);先做两次 wstring 比较筛选 | |
0x5597A3 | 223 PULL | CEffect__getSourceAsCharacterCUnit__getMoveSpeedA/BCUnit__datGetAttachesToMasterCUnit__datDropToGroundCUnit__getStatCUnit__applyDifficultyResistBoost | 把目标朝来源拖拽;带抗性判定与移速取值 | |
0x559ADF | 213 RETARGET、214 AGRO | CEffect__evalSlot×4rand_float_between_seededCUnit__pickRandomTargetInRadiusCCharacter__setTarget | 在半径内随机挑一个目标并强制换目标;213 与 214 共用同一段代码 | |
0x559C18 | 167 INTERRUPT | 抗性 | CCharacter__playReactionAnim×3 | 打断:播放三种反应动画之一 |
0x559D3D | 143 RESPEC | CUnit__hasUnitType__RTDynamicCastCPlayer__respecSkills | 洗点:必须能转型成 CPlayer 才生效 | |
0x559D7F | 142 SWAP POS WITH PET | CUnit__getMinionAtCUnit__setPosition | 与随从交换位置 | |
0x559E45 | 127 STOP SKILL | CEffect__evalSlotCSkillMgr__stopSkillByName | 按名字停止一个技能 | |
0x559EC2 | 74 CAST SKILL | CUnit__hasUnitTypeCBaseUnit__execProcCastSkill_petVariantCBaseUnit__execProcCastSkill | 施法:宠物走 petVariant,其余走主执行体 | |
0x559F1A | 113 WARP | getGameClientwstring::find/substr/length | 传送:把目标串按分隔符切成关卡名与落点名 | |
0x55A0D5 | 114 PERMANENT STRENGTH | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 永久力量 |
0x55A103 | 115 PERMANENT DEXTERITY | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 永久敏捷 |
0x55A131 | 116 PERMANENT MAGIC | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 永久魔力 |
0x55A15F | 118 PERMANENT DEFENSE | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 永久防御 |
0x55A18D | 117 PERMANENT HEALTH | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 永久生命 |
0x55A1BB | 119 PERMANENT MANA | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot | 永久法力(少一个 ftol,直接用浮点) |
0x55A1EA | 120 AWARD STATPOINT | 永久属性 | CUnit__hasUnitTypeCEffect__evalSlot__ftol2_sse | 奖励属性点 |
0x55A218 | 65 LEARN SKILL | CUnit__findOwningPlayerCUnit__getOrCreateSkillManagerCSkillList__createSkillCSkillMgr__setSkillEnabledCUnit__canLearnSkillCUnit__isLocallyAuthoritative | 学技能:建技能对象→设为启用;受 canLearnSkill 与权威端双重把关 | |
0x55A314 | 222 SET SKILL LEVEL | CSkillMgr__findSkillByNameCSkillMgr__setSkillLevelByNameCUnit__getMinionAt×3CUnit__datGetAttachesToMaster | 设技能等级;同时遍历随从,对附属随从一并设置 | |
0x55A486 | 216 MODIFY SKILL COOLDOWN REMAINING | CSkillMgr__findSkillByNameCSkill__setCooldownRemaining | 修改冷却剩余(相对) | |
0x55A4E3 | 217 SET SKILL COOLDOWN REMAINING | CSkillMgr__findSkillByNameCSkill__setCooldownRemaining | 设置冷却剩余(绝对);与 216 只差取值方式 | |
0x55A53A | 111 FADE OUT | CEffect__evalSlotCUnit__setVisible×2 | 淡出:两次 setVisible | |
0x55A576 | 112 SET VISIBLE | CEffect__evalSlotCUnit__setVisible×2 | 设可见:与 111 调用序列完全相同 | |
0x55A5B2 | 110 KILL | CUnit__isLocallyAuthoritativeCUnit__isPlayerPrimaryPetCEffect__evalSlot | 击杀:玩家主宠免疫,且只在权威端执行 | |
0x55A621 | 145 EXPLODE DEAD | CUnit__gibCorpse_DEATHEXPLODE | 尸爆 | |
0x55A631 | 45 OPEN WAYPOINT PORTAL | CUnit__isLocallyAuthoritativeCUnit__getLevelCUnit__addPlayerStatistic | 开传送点门户,并计入玩家统计 | |
0x55A666 | 141 OPEN PORTAL | CUnit__getLevel | 开门户 | |
0x55A696 | 57 TRANSFORM | CEffect__getInternedStringCEffect__evalSlotCUnit__doTransform | 变身:目标模型名来自 intern 串表 | |
0x55A6FB | 73 TRANSFORM PERMANENT | — | 永久变身:本段只做字符串准备,实体动作走共享尾部 | |
0x55A72A | 67 DESUMMON MONSTER | CUnit__doDesummon | 解除召唤 | |
0x55A73B | 56 INTERRUPT CHANCE | 抗性 | CCharacter_isBossOrNoDestroyCUnit__getStatCUnit__applyDifficultyResistBoostCCharacter__playReactionAnim | 打断概率:Boss 与不可摧毁单位直接免疫 |
0x55A856 | 6 MANA RECHARGE、123 MANA RECHARGE PLAYER | CUnit__modifyMana | 回蓝:单调用;6 与 123 共用同一段代码 | |
0x55A868 | 156 DAMAGE CHANCE | CEffect__evalSlot | 几率伤害:掷概率后落到 52 的伤害路径 | |
0x55A8BE | 52 DAMAGE | CUnit__hasStateNamed | 伤害入口:先查状态名,随后进入共享的暴击/伤害块 | |
0x55A92B | 7 HP RECHARGE、124 HP RECHARGE PLAYER | CUnit__getStat×2 | 回血:先取两个属性,随后进入共享的暴击/结算块;7 与 124 共用 | |
0x55BAB4 | 146 ADD STAT | 属性矩阵 | CEffect__evalSlotCStatManager__addToStat | 加属性 |
0x55BB2C | 148 SET STAT | 属性矩阵 | CEffect__evalSlotCStatManager__setStat | 设属性 |
0x55BBA4 | 150 CLEAR STAT | 属性矩阵 | CEffect__evalSlotCStatManager__removeStat | 清属性 |
0x55BC14 | 147 ADD STAT TO LEVEL | 属性矩阵 | CEffect__evalSlotCUnit__getLevelCStatManager__addToStat | 按等级加属性 |
0x55BC86 | 149 SET STAT ON LEVEL | 属性矩阵 | CEffect__evalSlotCUnit__getLevelCStatManager__setStat | 按等级设属性 |
0x55BCF8 | 151 CLEAR STAT ON LEVEL | 属性矩阵 | CEffect__evalSlotCUnit__getLevelCStatManager__removeStat | 按等级清属性 |
0x55BD62 | 158 TELEPORT | 传送×4 | CUnit__isAliveCUnit__getMoveSpeedA/BCUnit__getStatCEffect__evalSlot×3CUnit__teleportToClampedPoint | 定点传送 |
0x55BF07 | 157 TELEPORT RANDOM | 传送×4 | CUnit__isAliveCUnit__getMoveSpeedA/BCUnit__getStatCEffect__evalSlot×3CUnit__teleportRandomNearby | 随机传送 |
0x55C08C | 235 TELEPORT NOPARTICLE | 传送×4 | …同 158…CUnit__teleportToClampedPoint | 定点传送(无粒子) |
0x55C231 | 236 TELEPORT RANDOM NOPARTICLE | 传送×4 | …同 157…CUnit__teleportRandomNearby | 随机传送(无粒子) |
0x55C3B6 | 160 SPAWN UNIT | CEffect__evalSlot×4CUnit__isLocallyAuthoritativeCUnit__spawnUnitByGuid | 生成单位:按 GUID 生成,只在权威端 | |
0x55C495 | 215 SHIELD BREAK EFFECT | CUnit__hasUnitTypevec_atClampedByCapacity×4CEffect__isExpiredCEffect__setSlotAndReroll | 破盾:遍历目标身上的效果并重掷某个槽 | |
0x55C74F | 165 ADD TRIGGERABLE | CEffect__evalSlotCUnit__findTriggerableByDef | 添加触发器 | |
0x55C7EA | 166 REMOVE TRIGGERABLE | CUnit__removeTriggerableByName | 按名字移除触发器 | |
0x55C80D | 168 ADD CHARGE PERCENT | 充能 | CPlayer__addChargePercent | 加充能百分比 |
0x55C841 | 169 SET CHARGE PERCENT | 充能 | CPlayer__updateChargeBar | 设充能百分比 |
0x55C873 | 171 ADD CHARGES | 充能 | CPlayer__addCharges | 加充能点数 |
0x55C8A1 | 181 CONVERT CHARGE TO STAT | 充能 | CPlayer__getMaxChargesCUnit__getChargeFractionCPlayer__addChargesCStatManager__addToStat | 把充能换算成属性(按点数) |
0x55C959 | 182 CONVERT CHARGE PERCENT TO STAT | 充能 | CUnit__getChargeFractionCPlayer__addChargePercentCStatManager__addToStat×2CEffect__playParticleFx | 把充能换算成属性(按百分比),并放粒子 |
两组共用槽位:6/123(0x55A856)与 7/124(0x55A92B)在 INSTANT 路径上行为完全相同, 区别只在数值查询侧的消费方不同;213 RETARGET 与 214 AGRO 共用 0x559ADF。
CAUTION
剩下 175 个类型在 INSTANT 路径上是彻底的空操作。 default 落点是 0x55CA77,紧接着就是 0x55CA79 起的函数尾声(pop/retn 8)—— 一条有效指令都没有。写 DURATION:INSTANT 加这些类型,效果会被正常构造、 跑一遍分派、然后什么都不做,不报错、不打日志。属性加成类型基本都在这 175 个里。
6. 路径 5:挂载时执行
28 个类型有真分支:
| 处理地址 | 类型 | 族 | 关键调用 | 做什么 |
|---|---|---|---|---|
0x5626C7 | 227 SKILL ENABLE、228 SKILL DISABLE | CItem_AttachDynamicEffect | 技能启用/禁用:只做挂接;由函数开头的 CEffect+0x20 == 2 前置判定到达,不经 switch | |
0x562702 | 144 PERCENT BLIND | 抗性 | CEffect__evalSlot×2CUnit__getStatCItem_AttachDynamicEffectCCharacter__setTargetCCharacter__playReactionAnimCCharacter_isBossOrNoDestroy | 百分比致盲:命中后强制换目标 |
0x562845 | 202 BLIND | 抗性 | CUnit__getStatCUnit__applyDifficultyResistBoostCItem_AttachDynamicEffectCCharacter__setTarget | 致盲 |
0x5629C3 | 173 CHARGE BAR DECAY DELAY | CItem_AttachDynamicEffectsub_5B4990 | 充能条衰减延迟 | |
0x562A18 | 167 INTERRUPT | 抗性 | CUnit__getStatCUnit__applyDifficultyResistBoostCCharacter__playReactionAnim×2 | 打断(与 INSTANT 版同形) |
0x562B98 | 196 IMMOBILIZE | 抗性 | CUnit__isAliveCUnit__getStatCUnit__applyDifficultyResistBoostCUnit__applyStaggerCItem_AttachDynamicEffect | 定身:经 applyStagger |
0x562C72 | 162 WARP TO POI | CLevel__findPOIByNameCLevel__findFreeSpotNear_r2CLevel__snapToGroundCUnit__setPosition | 传送到 POI:找点→找空位→贴地→设位置 | |
0x562D1E | 146 ADD STAT | CEffect__evalSlotCStatManager__addToStatCStatManager__findStatCStat__setDuration | 加属性——比 INSTANT 版多了 findStat + setDuration,属性带时长 | |
0x562DC5 | 128 SILENCE | 抗性 | CUnit__isAliveCUnit__getStatCUnit__applyDifficultyResistBoostCItem_AttachDynamicEffect | 沉默 |
0x562E87 | 80 TURN ALIGNMENT | 抗性 | CUnit__hasUnitTypeCUnit__datGetAttachesToMasterCUnit__getStatCUnit__findOwningPlayerCUnit__enforceCharmLimit4CUnit__addPet | 魅惑:命中后变成玩家的宠物,且受 4 个魅惑上限约束 |
0x562F91 | 79 FLEE EFFECT | 抗性 | CUnit__isAliveCUnit__hasUnitTypeCUnit__getStatsub_664F10CCharacter__playReactionAnim | 恐惧 |
0x5631D2 | 64 STUN | 抗性 | CEffect__evalSlotCUnit__isAliveCUnit__getStat×2CUnit__applyDifficultyResistBoostCCharacter__playReactionAnim | 眩晕:两次 getStat(两级抗性) |
0x56348E | 106 FREEZE | 元素×4 | rand_float_betweenCUnit__isAliveCUnit__getStatCUnit__applyDifficultyResistBoostCCharacter__playReactionAnimCItem_AttachDynamicEffectCEffect__getSourceAsCharacter | 冰冻 |
0x563673 | 107 BURN | 元素×4 | …与 106 逐条同构… | 燃烧 |
0x56383B | 108 POISON | 元素×4 | …与 106 逐条同构… | 中毒 |
0x563A07 | 109 SHOCK | 元素×4 | …与 106 逐条同构… | 感电 |
0x563BD3 | 63 VELOCITY MULT | CItem_AttachDynamicEffectOgre::Vector3::operator= | 速度倍率:挂接后清一个向量 | |
0x563C00 | 56 INTERRUPT CHANCE | 抗性 | CEffect__evalSlotCCharacter_isBossOrNoDestroyCUnit__getStatCUnit__applyDifficultyResistBoostCCharacter__playReactionAnim | 打断概率:Boss 免疫 |
0x563D0B | 156 DAMAGE CHANCE | CEffect__evalSlot | 几率伤害:掷概率后落到 52 | |
0x563D52 | 52 DAMAGE | CUnit__hasStateNamedCItem_AttachDynamicEffectCUnit__getStat×3CUnit__getRootOwner×2CUnit__magicBonusToSkillFracCUnit__findOwningPlayer×2 | 伤害:沿 getRootOwner 上溯到真正的归属者再取属性 | |
0x563F3A | 7 HP RECHARGE、124 HP RECHARGE PLAYER | CItem_AttachDynamicEffectCUnit__getStat×2CUnit__magicBonusToSkillFracCEffect__setSlotAndReroll×2 | 回血:挂载时就把两个槽重掷并写回 | |
0x56400D | 110 KILL | CUnit__isPlayerPrimaryPetCUnit__isLocallyAuthoritativeCEffect__evalSlot | 击杀:玩家主宠免疫(与 INSTANT 版同) | |
0x5640B7 | 145 EXPLODE DEAD | CUnit__isAliveCUnit__gibCorpse_DEATHEXPLODE | 尸爆:比 INSTANT 版多一道 isAlive | |
0x5640D0 | 160 SPAWN UNIT | CEffect__evalSlotCItem_AttachDynamicEffectCEffect__setSlotAndReroll | 生成单位 | |
0x564141 | 165 ADD TRIGGERABLE | CEffect__evalSlotCUnit__findTriggerableByDefCItem_AttachDynamicEffect | 添加触发器:先按名字比对去重 | |
0x5642B5 | 166 REMOVE TRIGGERABLE | CItem_AttachDynamicEffectCUnit__removeTriggerableByName | 移除触发器 |
227 SKILL ENABLE / 228 SKILL DISABLE 有前置特例:函数开头先判 CEffect+0x20 == 2, 命中就走 CItem_AttachDynamicEffect 直接返回,根本到不了 switch。
WARNING
挂载路径的 default 不是空操作,这一点和 INSTANT 完全不同。 default 落点 0x564308 是一段真代码:
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 路径四份同构,前缀完全一致,只在落位函数(
teleportToClampedPointvsteleportRandomNearby)与粒子标志上分叉。 - 属性操作 3×2 矩阵 146–151:{addToStat, setStat, removeStat} × {普通, 按等级}。
- 属性永久化七兄弟 114–120:
hasUnitType→evalSlot→__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 链
| 比较点 | 命中类型 | 共同落点 | 这条链做什么 |
|---|---|---|---|
0x566F43、0x566F52 | 47 SUMMON DURATION、110 KILL | 0x566F58 | `TYPE == 47 |
20 处0x56701A–0x5671B3 | 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 TARGET | 0x5671B9 | 完整的 CAST SKILL ON x 家族,20 个类型全在这一条 OR 链里,每条比较都跳同一个落点 loc_5671B9,那里只做一件事:if (unit->+0x1BC != 0) 跳过。这是一道门禁,不是分派。 |
第二条链值得看一眼形状——20 次比较,20 个 jz 指向同一个地址:
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, 0(0x567005), 而 var_1EC 是「本帧到期」标志,在循环开头这样置位:
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__getOrCreateSkillManager(0x513840)惰性创建它: 0x51384B 判空、0x513894 写回。所以 cmp [ecx+1BCh], 0 问的是 「这个单位有没有技能管理器」,不是「某个开关开没开」。
合起来,这段的真实语义是:
若 效果本帧到期:
若 TYPE ∈ {20 个 CAST SKILL ON x} 且 自己有技能管理器
→ 调虚表槽 90,把技能卸掉
否则若 TYPE == 183 CAST SKILL ON DEATH FROM EFFECT OWNER
且 eff->来源单位 存在 且 来源单位有技能管理器 ← 查的是**来源**,不是自己
→ 同样调虚表槽 907.1.2 CAST SKILL ON x 的真实机制:装一个真技能上去
顺着虚表槽找下去,这一族的完整生命周期是:
| 阶段 | 函数 | 做什么 |
|---|---|---|
| 挂上 | CBaseUnit__registerCastSkillEffect_vt91(0x514AB0,虚表槽 91/+0x16C) | evalSlot 取技能等级 → CBaseUnit__getOrCreateSkill 在单位身上真的建出一个技能对象 → CSkillMgr__setSkillLevel |
| 到期 | CUnit__unloadCastSkillEffect_vt90(0x51CE80,虚表槽 90/+0x168) | CSkillList__findSkill → CSkillList__removeSkill |
IMPORTANT
CAST SKILL ON x 不是「每帧检查有没有触发」。 它在挂上的那一刻往单位的技能管理器里安装一个真实的技能对象,到期时再卸掉; 触发判定由技能系统负责。这就是为什么这一族在 tick 分派器里只出现清理逻辑—— 平时根本不需要 tick 参与。
一个可直接用上的副产品:技能名写错时引擎会打日志 (0x514BC9,Ogre::LogManager::logMessage)。 CAST SKILL ON x 是少数几个写错了会在日志里留痕的类型。
7.1.3 触发端:从事件到施法
装上技能之后,谁在什么时候把它放出来?整条链有三段。
第一段 —— 事件发生处直接 push 一个效果类型号。 入口是 CBaseUnit__fireProcEffects(0x53D3C0):
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_7C8130 → sub_7CB870, 与身上效果的那一轮互不干扰。
第二段 —— 22 个触发点,逐个核实过的事件类型:
| 触发时机 | 函数 | 事件类型(= 效果类型号) |
|---|---|---|
| 挥击选定目标 | 0x553900 CCharacter__strike_resolveTargets | 74 → 0x55398975 → 0x554E8976 → 0x554EA7198 → 0x554EC8 |
| 命中结算 | 0x548850 CCharacter__resolveStrikeHit | 152 → 0x54E21D201 → 0x54E237194 → 0x54E266195 → 0x54E295199/200 → 0x54E2AF(近战/远程两个常量共用同一个 call,0x54E279 push 200 后 jmp 过来)210 → 0x54E2D6211 → 0x54E2F0208 → 0x54E31F209 → 0x54E339206 → 0x54E355207 → 0x54E36F |
| 受到伤害 | 0x53D5A0 CBaseUnit__applyDamage | 81 → 0x53DAD4(且受击者 +0x7DC <= 0) |
| 死亡 | 0x54E3F0 CUnit__onDeath_vt120 | 105 → 0x54F55D183 → 0x54F573161 → 0x54F591203 → 0x54F5A7204 → 0x54F5B9 |
199 与 200 共用 0x54E2AF 这一个 call——0x54E279 推入 200 之后 jmp 过去, 近战/远程只是选了不同的常量。
第三段 —— execProcCastSkill(0x53C7E0)真正施放。
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 不会无视它—— 想要高频触发,技能本身的冷却也得配合。
第三张跳转表同样是「字节索引表 + 跳转表」结构,可以直接读表:
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),分成五种施法模式:
| 槽 | 处理地址 | 施法模式 | 类型 |
|---|---|---|---|
| 0 | 0x53CDAC | 自己施法 | 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 |
| 1 | 0x53CECC | 朝目标位置施法 | 76 CAST SKILL AT TARGET |
| 2 | 0x53CF58 | 由目标施法 —— 恰好是全部名字带 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 |
| 3 | 0x53CE19 | 击杀触发 | 203 CAST SKILL ON KILL |
| 4 | 0x53CE73 | 击杀且朝目标 | 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) 取(把某个单位的位置写进栈缓冲)。区别全在取谁的位置。
| 槽 | 地址 | 类型 | 递归防护 | 做什么 |
|---|---|---|---|---|
| 0 | 0x53CDAC | 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 | ✓ | 有 other 就取 other 的位置作目标点,没有就取自己的;施法者取自己(ebp),技能来自自己的技能管理器([ebp+1BCh])。最后 CSkillList__executeSkill(0x53CE0F)。 |
| 1 | 0x53CECC | 76 CAST SKILL AT TARGET | — | 两个位置参数都取 other(有目标时),没有目标就都取自己。这是唯一一个「把落点整体搬到目标处」的模式,也是唯一不设递归防护的普通模式。 |
| 2 | 0x53CF58 | 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 | — | 两道前置:other == 0 放弃、srcSkill == 0 放弃(0x53CF60 / 0x53CF6E)。随后取自身虚表 +0x88 的 Matrix4,乘上一个由 rand_float_between(0.0, 360.0) × π/180(sub_676E70 + flt_2144DB8=360.0 + flt_216C944=0.0174533)算出的随机角度构成的旋转矩阵,再 setTrans 到 other(目标)的位置(0x53D011 / 0x53D01E)——锥形是从目标处、朝一个随机方向发出的。接着 CLevel__queryUnitsInCone(0x53D104);命中数 > 0 时用 rand_int_between + vec_atClampedByCapacity 再随机挑一个(0x53D114 / 0x53D121),否则置 0。最后 CSkillList__executeSkill(0x53D2E0)。中途还会 getGameClient + sub_6E18B0 走一次网络广播。 |
| 3 | 0x53CE19 | 203 CAST SKILL ON KILL | ✓ | 两个位置参数都取自己,不看 other。 |
| 4 | 0x53CE73 | 204 CAST SKILL ON KILL AT TARGET | ✓ | other == 0 直接放弃(0x53CE83);有目标时位置取自目标,随后 jmp 汇入模式 3 的尾部(0x53CE58)。 |
sub_61DCE0(unit, &vec3, 1) = 取某个单位的位置写进栈上缓冲,五种模式都靠它组装 executeSkill 的位置参数。
WARNING
FROM TARGET 那 7 个类型带两重随机。 这一段会取自身虚表 +0x88 的 Matrix4,乘上一个 rand_float_between(0.0, 360.0) × π/180 算出的随机角度旋转矩阵, 再 setTrans 到 目标的位置——锥形是从目标处、朝一个每次重掷的随机方向发出的。 然后 CLevel__queryUnitsInCone 查到的多个单位里,再用 rand_int_between 随机挑一个。
两个常量已读出:flt_2144DB8 = 360.0,flt_216C944 = 0.0174533(π/180)。
这一段还有两道会让效果完全不触发的前置:other == 0(无目标)、srcSkill == 0(无来源技能)。
7.1.5 锥形查询的实参与判定公式
CLevel__queryUnitsInCone(0x5FC670)的签名:
queryUnitsInCone(this, out1, out2, Matrix4* xform, alignment,
float minDist, float halfAngleDeg, float maxDist, exclude, skill, …)模式 2 传进去的三个浮点:
| 形参 | 实参来源 |
|---|---|
minDist | 硬编码 0.0(fldz @0x53D0DD)—— 近身豁免这条等于不生效 |
halfAngleDeg | CSkill__getLevelConeHalfAngleDeg(0x6BE3A0)= skill->levelData->+0xA8 = FINDTARGETANGLE,缺省 0 |
maxDist | CSkill__getLevelRange(0x6BE3C0)= skill->levelData->+0x94 = RANGE,缺省 1.0 |
两个 DAT 键已确认到写入指令:
| DAT 键 | 字段 | 加载点 |
|---|---|---|
FINDTARGETANGLE | CSkillProperty+0xA8 | CSkillProperty__ctorFromDat 0x6DDFAB 推键名 → 0x6DDFD6 CDataGroup__getFloat → 0x6DDFDB fstp [esi+0A8h]。原版仅 4 处使用。 |
RANGE | CSkillProperty+0x94 | 同一函数 0x6DE008 → 0x6DE033 → 0x6DE038 fstp [esi+94h]。原版 141 处使用。 |
IMPORTANT
两处的 getFloat 默认实参都取自 上一级 [LEVELn-1] 的同名字段(mov eax, [esi+34h];无上一级时 FINDTARGETANGLE 用 0、RANGE 用本级当前值)。所以 [LEVELn] 里不写这些键就继承上一级,不是回到默认值。
判定公式(0x5FC8B1–0x5FC988):
- 局部坐标 = 目标位置 ×
Matrix4::inverse(xform) - 把 Y 压平(
v43 = 0.0),长度即距离 atan2(x, z) × 57.295826即角度(度)- 命中 = 距离 ≤ 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__applyDamage 在 0x53DAB6 比较受击者的 +0x7DC,非零就不触发; 而 proc 一旦施法就把它置成 1.0,再由 CMonster_tick_main 每帧 -= dt 减回去。 效果上就是每秒最多触发一次。
需要注意的不对称:模式 1(76 CAST SKILL AT TARGET)与模式 2(FROM TARGET 七个) 不设这个标记,所以它们不给施法者上这道闸。
7.2 23 个逐类型动作块
| 比较点 | 类型 | 做什么 |
|---|---|---|
0x5671D2 | 183 CAST SKILL ON DEATH FROM EFFECT OWNER | CAST SKILL ON DEATH FROM EFFECT OWNER 紧跟在 OR 链之后单独判,走虚表调用 |
0x56729D | 61 MISSILE REFLECT VISUAL | __RTDynamicCast → CCharacter__setTarget:弹射反射的视觉版会改目标 |
0x5673B4 | 67 DESUMMON MONSTER | CUnit__doDesummon |
0x567472 | 227 SKILL ENABLE | 与 228 共用 CSkillMgr__setSkillEnabledByName(0x5674CC) |
0x567486 | 228 SKILL DISABLE | 同上,两行只是传不同的布尔值 |
0x56752F | 65 LEARN SKILL | findOwningPlayer → getOrCreateSkillManager → CSkillList__createSkill → setSkillEnabled:每帧都重新确保技能存在 |
0x5676F3 | 229 DRAW HEALTH | 与 230 一起转调 CUnit__tickDrawHealthMana(0x56959E) |
0x56770B | 230 DRAW MANA | 同上 |
0x567723 | 185 HP MOD OVER TIME | sub_51EFC0 + CEffect__evalSlot×5:五个槽全读 |
0x567E92 | 80 TURN ALIGNMENT | CEffect__isExpired → CUnit__isAlive → datGetAttachesToMaster → hasStateNamed:魅惑的存续维护 |
0x568030 | 64 STUN | sub_6D7B90 / sub_6D9C80 / CDataGroup__getInt64:眩晕每帧去读一次 dat 键 |
0x568330 | 107 BURN | 燃烧 |
0x568615 | 106 FREEZE | 冰冻 |
0x5688FA | 109 SHOCK | 感电 |
0x568BDF | 108 POISON | 中毒 |
0x568F05 | 59 SCALE | CEffect__evalSlot×4 —— 正好对应 59 的四个槽 STARTSCALE/ENDSCALE/TIMETOSCALE/TIMETOSCALEBACK |
0x569274 | 184 SCALE BY HP | evalSlot×2 → CUnit__getMaxHealth → _CIpow:按生命比例做幂曲线缩放 |
0x569360 | 160 SPAWN UNIT | evalSlot×2 → CUnit__isLocallyAuthoritative |
0x569541 | 231 PERCENT DAMAGE TAKEN BY MONSTER COUNT | 转调 CUnit__tickDamageByMonsterCount(0x569580) |
0x569554 | 234 PERCENT DAMAGE BONUS BY MONSTER COUNT | 同上,与 231 共用同一个实现 |
0x5696A5 | 43 KNOCK BACK EFFECT | CUnit__getStat → CEffect__recacheValue → Vector3 累加:击退每帧重算数值 |
0x569824 | 225 PULL EFFECT | Vector3 相减 → length → normalise → 两次 operator*=:拉拽向量的完整计算 |
0x569AD1 | 166 REMOVE TRIGGERABLE | CUnit__removeTriggerableByName,随后回调 CUnit__executeInstantEffect(0x569B31) |
NOTE
106/107/108/109 四段的调用序列逐条一致:vec_atClampedByCapacity → CEffect__isExpired → sub_518790 → vec_at×2 → 虚表调用 → hasUnitType → CEffectMgr__findAffixByName → hasUnitType。四份副本,改一处不改其余三处就会让四种元素状态行为不一致。
7.3 三处共享实现与一处跨路径回调
227 SKILL ENABLE与228 SKILL DISABLE共用CSkillMgr__setSkillEnabledByName(0x5674CC)。
CUnit__tickDrawHealthMana(0x53F6A0)—— 229 / 230 的全部实现
槽位:MIN_PER_HEALTH/MAX_PER_HEALTH(或 MANA)、PULSE_RATE、RADIUS、MAX_TARGETS
- 类型门:
cmp eax, 0E5h(229) /cmp eax, 0E6h(230)(0x53F6ED / 0x53F6F4) CEffect__evalSlot×3 取 PULSE_RATE/RADIUS/MAX_TARGETS(0x53F703 / 710 / 71D)CLevel__gatherUnitsInRadius(0x53F7BB)—— 范围抽取,不是单体- 229 分支(0x53F815):
floor(目标当前血)≥getMaxHealth就跳过 —— 满血目标不被抽 CUnit__isLocallyAuthoritative后再比一次满血- 目标状态
+0x314∈ {12, 50, 51} 时追加一道+0x284判定(0x53F87E–0x53F89E) hasUnitType(0xA7)、hasStateNamed两道排除(0x53F8A4 / 0x53F8D7)sub_535570+ 虚表调用完成实际抽取;CEffect__playParticleFx放特效
NOTE
分派器那两行只是转调,不带 isExpired 前置,每帧无条件调。
CUnit__tickDamageByMonsterCount(0x532660)—— 231 / 234 的全部实现
槽位:MIN_PER/MAX_PER、RANGE(槽 2)、MAX_COUNT(槽 3)、槽 4 被引擎占用
- 只认 231 / 234,其余立即返回(0x5326C2)
- 刷新周期 0.2 秒硬编码:
当前时间 - evalSlot(eff,4) < 0.2就跳过(0x532701) CEffect__setSlotAndReroll(eff, 4, 当前时间)—— 槽 4 被当刷新时间戳写入(0x532712)- 按
findEffectByTypeAndName(26 或 25, eff->NAME)找同名伙伴效果(0x532727 / 0x53273C) CLevel__gatherUnitsInRadius半径 = 槽 2 数到周围单位数- 值 =
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 执行器 (0x569B31 → CUnit__executeInstantEffect)。 这是六条路径之间唯一一处互相调用——一个持续效果每帧都可能触发一次 INSTANT 语义。
8. 239 行路径归属总表
列的读法
- 被动/扫表/布尔 —— 该类型是否被路径 1/2/3 消费。
- INSTANT 分支 / 挂载分支 —— 处理代码地址;空白表示落 default,即无行为。这两列完整。
- tick 比较点 —— if 链里比较该类型的指令地址。
- 生效方式 —— 由前几列推出的结论。
| # | TYPE 名 | 槽 0 | 槽 1 | 槽 2 | 槽 3 | 槽 4 | 被动 | 扫表 | 布尔 | INSTANT 分支 | 挂载分支 | tick 比较点 | 生效方式 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MELEEDAMAGEBONUS | MIN | MAX | MINFORCE | MAXFORCE | · | ✓ | 被动数值 | |||||
| 1 | RANGEDDAMAGEBONUS | MIN | MAX | MINFORCE | MAXFORCE | · | ✓ | 被动数值 | |||||
| 2 | DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 3 | MAGIC | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 4 | MAX MANA | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 5 | MAX HP | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 6 | MANA RECHARGE | MIN | MAX | · | · | · | ✓ | 0x55A856 | 被动/INSTANT | ||||
| 7 | HP RECHARGE | MIN | MAX | MINFORCE | MAXFORCE | · | ✓ | 0x55A92B | 0x563F3A | 被动/INSTANT/挂载 | |||
| 8 | ARMOR BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 9 | TO HIT BONUS | MIN | MAX | · | · | · | 无消费方 | ||||||
| 10 | DAMAGE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 11 | DAMAGE TAKEN | MIN | MAX | · | · | · | 无消费方 | ||||||
| 12 | KNOCK BACK | FORCE | · | · | · | · | ✓ | 被动数值 | |||||
| 13 | SIGHT BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 14 | VIEW ANGLE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 15 | PERCENT MELEEDAMAGE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 16 | PERCENT RANGEDDAMAGE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 17 | PERCENT DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 18 | PERCENT MAGIC | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 19 | PERCENT MANA | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 20 | PERCENT HP | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 21 | PERCENT SPEED | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 22 | PERCENT ATTACK SPEED | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 23 | PERCENT ARMOR BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 24 | PERCENT TO HIT BONUS | MIN | MAX | · | · | · | 无消费方 | ||||||
| 25 | PERCENT DAMAGE BONUS | MIN | MAX | · | · | · | ✓ | ✓ | 被动/布尔 | ||||
| 26 | PERCENT DAMAGE TAKEN | MIN | MAX | · | · | · | ✓ | ✓ | 被动/布尔 | ||||
| 27 | PERCENT MAGICAL DROP | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 28 | PERCENT GOLD DROP | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 29 | PERCENT CAST SPEED | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 30 | PERCENT LIFE STOLEN | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 31 | PERCENT MANA STOLEN | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 32 | PERCENT DAMAGE REFLECTED | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 33 | PERCENT BLOCK CHANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 34 | PERCENT ITEM REQUIREMENTS | MIN | MAX | · | · | · | 无消费方 | ||||||
| 35 | PHYSICAL DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 36 | MAGICAL DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 37 | FIRE DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 38 | ICE DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 39 | ELECTRICAL DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 40 | POISON DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 41 | PERCENT SIGHT BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 42 | PERCENT VIEW ANGLE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 43 | KNOCK BACK EFFECT | FORCE | · | · | · | · | 0x5696A5 | 周期执行 | |||||
| 44 | PERCENT ACTIVE DISTANCE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 45 | OPEN WAYPOINT PORTAL | MIN | MAX | · | · | · | 0x55A631 | 仅 INSTANT | |||||
| 46 | IDENTIFY | MIN | MAX | · | · | · | 效果本身无行为 | ||||||
| 47 | SUMMON DURATION | MIN | MAX | · | · | · | ✓ | 0x566F43 | 被动/tick | ||||
| 48 | DAMAGE REFLECTION | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 49 | LIFE STEAL | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 50 | MANA STEAL | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 51 | GOLD DROP | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 52 | DAMAGE | MIN | MAX | MINFORCE | MAXFORCE | · | ✓ | 0x55A8BE | 0x563D52 | 被动/INSTANT/挂载 | |||
| 53 | PERCENT KNOCK BACK RESISTANCE | RESISTANCE | · | · | · | · | ✓ | 被动数值 | |||||
| 54 | DEGRADE ARMOR | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 55 | CRITICAL CHANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 56 | INTERRUPT CHANCE | MIN | MAX | · | · | · | ✓ | 0x55A73B | 0x563C00 | 被动/INSTANT/挂载 | |||
| 57 | TRANSFORM | MIN | MAX | · | · | · | 0x55A696 | 仅 INSTANT | |||||
| 58 | UNIT THEME | · | · | · | · | · | 机制不看 TYPE | ||||||
| 59 | SCALE | STARTSCALE | ENDSCALE | TIMETOSCALE | TIMETOSCALEBACK | · | 0x568F05 | 周期执行 | |||||
| 60 | MISSILE REFLECT | MIN | MAX | DMGPCTMIN | DMGPCTMAX | · | ✓ | 扫表 | |||||
| 61 | MISSILE REFLECT VISUAL | MIN | MAX | DMGPCT | · | · | 0x56729D | 周期执行 | |||||
| 62 | PERCENT BLOCK CHANCE BASE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 63 | VELOCITY MULT | MIN | MAX | · | · | · | ✓ | ✓ | 0x563BD3 | 被动/布尔/挂载 | |||
| 64 | STUN | MIN | MAX | IGNORE_RESISTANCE | · | · | ✓ | 0x5631D2 | 0x568030 | 被动/挂载/tick | |||
| 65 | LEARN SKILL | MIN | MAX | · | · | · | 0x55A218 | 0x56752F | INSTANT/tick | ||||
| 66 | DEGRADE ARMOR EFFECT | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 67 | DESUMMON MONSTER | MIN | MAX | NO_LOOT | NO_DIE_ANIM | DESTROY_BODY | 0x55A72A | 0x5673B4 | INSTANT/tick | ||||
| 68 | XP GAIN BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 69 | STRENGTH BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 70 | DEXTERITY BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 71 | PERCENT STRENGTH BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 72 | PERCENT DEXTERITY BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 73 | TRANSFORM PERMANENT | MIN | MAX | · | · | · | 0x55A6FB | 仅 INSTANT | |||||
| 74 | CAST SKILL | MIN | MAX | · | · | · | 0x559EC2 | 0x56701A | INSTANT/tick | ||||
| 75 | CAST SKILL ON TARGET | MIN | MAX | · | · | · | 0x5671B3 | 周期执行 | |||||
| 76 | CAST SKILL AT TARGET | MIN | MAX | · | · | · | 0x567030 | 周期执行 | |||||
| 77 | FAME GAIN BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 78 | MISSILE RANGE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 79 | FLEE EFFECT | MIN | MAX | · | · | · | ✓ | 0x562F91 | 布尔/挂载 | ||||
| 80 | TURN ALIGNMENT | MIN | MAX | · | · | · | ✓ | 0x562E87 | 0x567E92 | 布尔/挂载/tick | |||
| 81 | CAST SKILL ON STRUCK | MIN | MAX | · | · | · | 0x56705F | 周期执行 | |||||
| 82 | SKILL BONUS | MIN | MAX | · | · | · | ✓ | 周期执行 | |||||
| 83 | PRICE REDUCTION | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 84 | FISHING LUCK | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 85 | ATTACK SPELL BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 86 | DEFENSE SPELL BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 87 | CHARM SPELL BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 88 | DUAL WIELDING BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 89 | PERCENT CRITICAL DAMAGE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 90 | POTION EFFICIENCY | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 91 | PERCENT PET DAMAGE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 92 | PERCENT PET VELOCITY | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 93 | REDUCED ITEM REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 94 | ARMOR ITEM REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 95 | SPELL REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 96 | PET DEPARTURE TIME | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 97 | PERCENT PET ARMOR | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 98 | MARTIAL ITEM REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 99 | PERCENT MARTIAL ITEM DAMAGE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 100 | RANGED ITEM REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 101 | MAGIC ITEM REQUIREMENTS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 102 | PERCENT RANGED ITEM DAMAGE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 103 | PERCENT MAGIC ITEM DAMAGE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 104 | PERCENT PET HEALTH | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 105 | CAST SKILL ON DEATH | MIN | MAX | · | · | · | 0x56717D | 周期执行 | |||||
| 106 | FREEZE | MIN | MAX | · | · | · | 0x56348E | 0x568615 | 挂载/tick | ||||
| 107 | BURN | MIN | MAX | · | · | · | 0x563673 | 0x568330 | 挂载/tick | ||||
| 108 | POISON | MIN | MAX | · | · | · | 0x56383B | 0x568BDF | 挂载/tick | ||||
| 109 | SHOCK | MIN | MAX | · | · | · | 0x563A07 | 0x5688FA | 挂载/tick | ||||
| 110 | KILL | MIN | MAX | EXPLODE | NO_LOOT | · | 0x55A5B2 | 0x56400D | 0x566F52 | INSTANT/挂载/tick | |||
| 111 | FADE OUT | MIN | MAX | · | · | · | 0x55A53A | 仅 INSTANT | |||||
| 112 | SET VISIBLE | MIN | MAX | · | · | · | 0x55A576 | 仅 INSTANT | |||||
| 113 | WARP | MIN_DEPTH | MAX_DEPTH | · | · | · | ✓ | 0x559F1A | 布尔/INSTANT | ||||
| 114 | PERMANENT STRENGTH | MIN | MAX | · | · | · | 0x55A0D5 | 仅 INSTANT | |||||
| 115 | PERMANENT DEXTERITY | MIN | MAX | · | · | · | 0x55A103 | 仅 INSTANT | |||||
| 116 | PERMANENT MAGIC | MIN | MAX | · | · | · | 0x55A131 | 仅 INSTANT | |||||
| 117 | PERMANENT HEALTH | MIN | MAX | · | · | · | 0x55A18D | 仅 INSTANT | |||||
| 118 | PERMANENT DEFENSE | MIN | MAX | · | · | · | 0x55A15F | 仅 INSTANT | |||||
| 119 | PERMANENT MANA | MIN | MAX | · | · | · | 0x55A1BB | 仅 INSTANT | |||||
| 120 | AWARD STATPOINT | MIN | MAX | · | · | · | 0x55A1EA | 仅 INSTANT | |||||
| 121 | FLEE RESIST | RESISTANCE | · | · | · | · | ✓ | 被动数值 | |||||
| 122 | SHIELD BUFFER | MIN | MAX | · | · | · | ✓ | 周期执行 | |||||
| 123 | MANA RECHARGE PLAYER | MIN | MAX | · | · | · | ✓ | 0x55A856 | 被动/INSTANT | ||||
| 124 | HP RECHARGE PLAYER | MIN | MAX | · | · | · | ✓ | 0x55A92B | 0x563F3A | 被动/INSTANT/挂载 | |||
| 125 | LIFE STEAL MASTER | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 126 | PERCENT LIFE STOLEN MASTER | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 127 | STOP SKILL | MIN | MAX | · | · | · | 0x559E45 | 仅 INSTANT | |||||
| 128 | SILENCE | MIN | MAX | · | · | · | ✓ | 0x562DC5 | 布尔/挂载 | ||||
| 129 | DESUMMON ON DEATH | MIN | MAX | · | · | · | ✓ | 周期执行 | |||||
| 130 | EMOTIONAL DAMAGE | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 131 | FUTURISTICNESS | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 132 | JAUNTINESS | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 133 | SOCKS | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 134 | LOGIC RESISTANCE | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 135 | NOODLE ARMS | MIN | MAX | · | · | · | 开发期占位 | ||||||
| 136 | INTERRUPT RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 137 | CHARM RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 138 | SILENCE_RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 139 | STUN RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 140 | SLOW RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 141 | OPEN PORTAL | MIN | MAX | · | · | · | 0x55A666 | 仅 INSTANT | |||||
| 142 | SWAP POS WITH PET | PETINDEX | · | · | · | · | 0x559D7F | 仅 INSTANT | |||||
| 143 | RESPEC | MIN | MAX | · | · | · | 0x559D3D | 仅 INSTANT | |||||
| 144 | PERCENT BLIND | MIN | MAX | CHANCE | · | · | ✓ | 0x562702 | 被动/挂载 | ||||
| 145 | EXPLODE DEAD | MIN | MAX | · | · | · | 0x55A621 | 0x5640B7 | INSTANT/挂载 | ||||
| 146 | ADD STAT | MIN | MAX | CHANCE | · | · | 0x55BAB4 | 0x562D1E | INSTANT/挂载 | ||||
| 147 | ADD STAT TO LEVEL | MIN | MAX | CHANCE | · | · | 0x55BC14 | 仅 INSTANT | |||||
| 148 | SET STAT | MIN | MAX | CHANCE | · | · | 0x55BB2C | 仅 INSTANT | |||||
| 149 | SET STAT ON LEVEL | MIN | MAX | CHANCE | · | · | 0x55BC86 | 仅 INSTANT | |||||
| 150 | CLEAR STAT | MIN | MAX | CHANCE | · | · | 0x55BBA4 | 仅 INSTANT | |||||
| 151 | CLEAR STAT ON LEVEL | MIN | MAX | CHANCE | · | · | 0x55BCF8 | 仅 INSTANT | |||||
| 152 | CAST SKILL ON STRIKE | MIN | MAX | · | · | · | 0x567075 | 周期执行 | |||||
| 153 | PERCENT DUAL WIELDING ATTACK | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 154 | DAMAGE BONUS SECONDARY | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 155 | SET MESH INVISIBLE | INVISIBLE | · | · | · | · | ✓ | 被动数值 | |||||
| 156 | DAMAGE CHANCE | MIN | MAX | MINFORCE | MAXFORCE | CHANCE | 0x55A868 | 0x563D0B | INSTANT/挂载 | ||||
| 157 | TELEPORT RANDOM | CHANCE | · | MINRANGE | MAXRANGE | COOLDOWN | 0x55BF07 | 仅 INSTANT | |||||
| 158 | TELEPORT | CHANCE | · | MINRANGE | MAXRANGE | COOLDOWN | 0x55BD62 | 仅 INSTANT | |||||
| 159 | TELEPORT RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 160 | SPAWN UNIT | COUNT | · | CHANCE | MINRANGE | MAXRANGE | 0x55C3B6 | 0x5640D0 | 0x569360 | INSTANT/挂载/tick | |||
| 161 | SPAWN UNIT ON DEATH | COUNT | HPPCT | CHANCE | MINRANGE | MAXRANGE | ✓ | 周期执行 | |||||
| 162 | WARP TO POI | POI | · | · | · | · | 0x562C72 | 挂载 | |||||
| 163 | DESTRUCTABLE SHIELD | HP% | HPOVERRIDE | ANGLE | OFFSET | DESTROY_WEAPONS | ✓ | 周期执行 | |||||
| 164 | DISENCHANT | GOLD% | CHANCE% | · | · | · | 效果本身无行为 | ||||||
| 165 | ADD TRIGGERABLE | · | · | CHANCE | · | · | 0x55C74F | 0x564141 | INSTANT/挂载 | ||||
| 166 | REMOVE TRIGGERABLE | MIN | MAX | · | · | · | ✓ | 0x55C7EA | 0x5642B5 | 0x569AD1 | 布尔/INSTANT/挂载/tick | ||
| 167 | INTERRUPT | MIN | MAX | · | · | · | 0x559C18 | 0x562A18 | INSTANT/挂载 | ||||
| 168 | ADD CHARGE PERCENT | MIN | MAX | · | · | · | ✓ | 0x55C80D | 被动/INSTANT | ||||
| 169 | SET CHARGE PERCENT | MIN | MAX | · | · | · | ✓ | 0x55C841 | 被动/INSTANT | ||||
| 170 | MAX CHARGES | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 171 | ADD CHARGES | MIN | MAX | · | · | · | 0x55C873 | 仅 INSTANT | |||||
| 172 | PERCENT CHARGE BAR DECAY RATE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 173 | CHARGE BAR DECAY DELAY | MIN | MAX | · | · | · | ✓ | 0x5629C3 | 被动/挂载 | ||||
| 174 | PERCENT CHARGING BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 175 | PERCENT MANA COST BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 176 | CHARGE BAR DECAY RATE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 177 | EXPLODE ON DEATH | MIN | MAX | · | · | · | ✓ | 周期执行 | |||||
| 178 | DODGE CHANCE BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 179 | FUMBLE CHANCE REDUCTION | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 180 | FUMBLE PENALTY REDUCTION | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 181 | CONVERT CHARGE TO STAT | · | · | VALUE | · | · | 0x55C8A1 | 仅 INSTANT | |||||
| 182 | CONVERT CHARGE PERCENT TO STAT | · | · | VALUE | · | · | 0x55C959 | 仅 INSTANT | |||||
| 183 | CAST SKILL ON DEATH FROM EFFECT OWNER | MIN | MAX | · | · | · | 0x5671D2 | 周期执行 | |||||
| 184 | SCALE BY HP | MAXSCALE | MINSCALE | · | · | · | 0x569274 | 周期执行 | |||||
| 185 | HP MOD OVER TIME | PERCENT_TO_HEAL | TIME_TO_HEAL | TIME_TO_HEAL_MAX | MIN_STOP_AT | MAX_STOP_AT | 0x567723 | 周期执行 | |||||
| 186 | TRANSFORMATION TIME | PERCENT_TO_100 | · | · | · | · | ✓ | 被动数值 | |||||
| 187 | TRAP DISARM CHANCE | CHANCE_TO_100 | · | · | · | · | ✓ | 被动数值 | |||||
| 188 | ELEMENTAL EFFECT DURATION BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 189 | BURN DURATION BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 190 | FREEZE DURATION BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 191 | SHOCK DURATION BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 192 | POISON DURATION BONUS | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 193 | NONE | · | · | · | · | · | 空操作哨兵 | ||||||
| 194 | CAST SKILL ON RANGED STRIKE | MIN | MAX | · | · | · | 0x5670D5 | 周期执行 | |||||
| 195 | CAST SKILL ON MELEE STRIKE | MIN | MAX | · | · | · | 0x5670A7 | 周期执行 | |||||
| 196 | IMMOBILIZE | MIN | MAX | · | · | · | ✓ | ✓ | 0x562B98 | 被动/布尔/挂载 | |||
| 197 | IMMOBILIZE RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 198 | CAST SKILL FROM TARGET | MIN | MAX | · | · | · | 0x567046 | 周期执行 | |||||
| 199 | CAST SKILL ON MELEE STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x5670BE | 周期执行 | |||||
| 200 | CAST SKILL ON RANGED STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x5670EC | 周期执行 | |||||
| 201 | CAST SKILL ON STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x56708E | 周期执行 | |||||
| 202 | BLIND | MIN | MAX | · | · | · | ✓ | 0x562845 | 被动/挂载 | ||||
| 203 | CAST SKILL ON KILL | MIN | MAX | · | · | · | 0x56718D | 周期执行 | |||||
| 204 | CAST SKILL ON KILL AT TARGET | MIN | MAX | · | · | · | 0x5671A0 | 周期执行 | |||||
| 205 | SHIELD BREAK | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 206 | CAST SKILL ON MELEE SKILL STRIKE | MIN | MAX | · | · | · | 0x567131 | 周期执行 | |||||
| 207 | CAST SKILL ON MELEE SKILL STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x567144 | 周期执行 | |||||
| 208 | CAST SKILL ON RANGED SKILL STRIKE | MIN | MAX | · | · | · | 0x567157 | 周期执行 | |||||
| 209 | CAST SKILL ON RANGED SKILL STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x56716A | 周期执行 | |||||
| 210 | CAST SKILL ON SKILL STRIKE | MIN | MAX | · | · | · | 0x567103 | 周期执行 | |||||
| 211 | CAST SKILL ON SKILL STRIKE FROM TARGET | MIN | MAX | · | · | · | 0x56711A | 周期执行 | |||||
| 212 | TARGET LOCKED | · | · | · | · | · | 纯显示 | ||||||
| 213 | RETARGET | CHANCE | RANGE | TARGET_SKILL_TARGET | · | · | 0x559ADF | 仅 INSTANT | |||||
| 214 | AGRO | CHANCE | · | · | · | · | 0x559ADF | 仅 INSTANT | |||||
| 215 | SHIELD BREAK EFFECT | MIN | MAX | · | · | · | 0x55C495 | 仅 INSTANT | |||||
| 216 | MODIFY SKILL COOLDOWN REMAINING | MIN | MAX | · | · | · | 0x55A486 | 仅 INSTANT | |||||
| 217 | SET SKILL COOLDOWN REMAINING | MIN | MAX | · | · | · | 0x55A4E3 | 仅 INSTANT | |||||
| 218 | INNATE ICE DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 219 | INNATE FIRE DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 220 | INNATE ELECTRIC DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 221 | INNATE POISON DEFENSE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 222 | SET SKILL LEVEL | MIN | MAX | · | · | · | 0x55A314 | 仅 INSTANT | |||||
| 223 | PULL | TIME | EFFECT_DURATION | · | · | · | 0x5597A3 | 仅 INSTANT | |||||
| 224 | PULL RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 225 | PULL EFFECT | TIME | · | · | · | · | ✓ | 0x569824 | 被动/tick | ||||
| 226 | MISS CHANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 227 | SKILL ENABLE | MINTIME | MAXTIME | · | · | · | 0x5626C7 | 0x567472 | 挂载/tick | ||||
| 228 | SKILL DISABLE | MINTIME | MAXTIME | · | · | · | 0x5626C7 | 0x567486 | 挂载/tick | ||||
| 229 | DRAW HEALTH | MIN_PER_HEALTH | MAX_PER_HEALTH | PULSE_RATE | RADIUS | MAX_TARGETS | 0x5676F3 | 周期执行 | |||||
| 230 | DRAW MANA | MIN_PER_MANA | MAX_PER_MANA | PULSE_RATE | RADIUS | MAX_TARGETS | 0x56770B | 周期执行 | |||||
| 231 | PERCENT DAMAGE TAKEN BY MONSTER COUNT | MIN_PER | MAX_PER | RANGE | MAX_COUNT | · | 0x569541 | 周期执行 | |||||
| 232 | REMOVE EFFECT | MIN | MAX | · | · | · | 0x55973D | 仅 INSTANT | |||||
| 233 | BLIND RESISTANCE | MIN | MAX | · | · | · | ✓ | 被动数值 | |||||
| 234 | PERCENT DAMAGE BONUS BY MONSTER COUNT | MIN_PER | MAX_PER | RANGE | MAX_COUNT | · | 0x569554 | 周期执行 | |||||
| 235 | TELEPORT NOPARTICLE | CHANCE | · | MINRANGE | MAXRANGE | COOLDOWN | 0x55C08C | 仅 INSTANT | |||||
| 236 | TELEPORT RANDOM NOPARTICLE | CHANCE | · | MINRANGE | MAXRANGE | COOLDOWN | 0x55C231 | 仅 INSTANT | |||||
| 237 | MINIONDAMAGE | MIN | MAX | MINFORCE | MAXFORCE | · | 纯显示 | ||||||
| 238 | CHEATED | MIN | MAX | · | · | · | ✓ | 周期执行 |
9. 收敛结果:239 个类型的定性
| 分类 | 数量 | 含义 |
|---|---|---|
| 已定位到至少一条路径 | 223 | 有明确的消费方或分派分支 |
| 机制不看 TYPE | 1 | 58 UNIT THEME,靠 UNITTHEME 键驱动 |
| 效果本身无行为 | 2 | 46 IDENTIFY、164 DISENCHANT,实际功能由别的机制实现 |
| 纯显示 | 2 | 212 TARGET LOCKED、237 MINIONDAMAGE,只有描述串,无执行路径。237 在 MOD 库里被误用 801 次,见 9.2 节 |
| 无消费方 | 4 | 六条路径均无,且原版用量为 0 |
| 开发期占位 | 6 | 名字即说明问题 |
| 空操作哨兵 | 1 | 193 NONE,按设计什么都不做 |
| 仍未定位 | 0 | 239 个类型已全部定性 |
9.1 逐个交代
| # | TYPE 名 | 结论 | 依据 |
|---|---|---|---|
| 9 | TO HIT BONUS | 无消费方 | 原版与 MOD 库用量均为 0,很可能是死类型 |
| 11 | DAMAGE TAKEN | 无消费方 | 原版用量 0 |
| 24 | PERCENT TO HIT BONUS | 无消费方 | 原版用量 0 |
| 34 | PERCENT ITEM REQUIREMENTS | 无消费方 | 原版与 MOD 库用量均为 0;且明确不在 sub_57D930 的需求减免链里 |
| 46 | IDENTIFY | 效果本身无行为 | 原版 SKILLS/SHARED/IDENTIFY/IDENTIFY.DAT 写 DURATION:INSTANT,但类型 46 在 INSTANT 跳表里落 default(0x55CA77 = 纯 retn)。鉴定功能由技能层实现,这个效果只是标记 |
| 58 | UNIT THEME | 机制不看 TYPE | CEffect__ctorFromDat 0x7AAD8C 起无条件读 UNITTHEME 键 → toUpperW → sub_661830 查主题 → 存入 CEffect+0x128(0x7AADDC,无 TYPE 守卫)。消费点在 CUnit__tickActiveEffects_dispatch 0x568184:只判 +0x128 != 0,取 theme+0x18 主题名大写化后配合 CUnit__hasStateNamed。所以 TYPE:UNIT THEME 是装饰性的,真正起作用的是 UNITTHEME 键 |
| 164 | DISENCHANT | 效果本身无行为 | 原版 UNITS/ITEMS/SCROLLS/DISENCHANT.DAT 写 DURATION:INSTANT,落 INSTANT default。去附魔实际由标志位 0x400 驱动:唯一写入点 CEquipment__markNewEffectsAsEnchanted 0x73E20E,清除方 CEffectMgr__removeEnchantments_disenchant 0x7AF480 按 flags&0x400 与 CAffix__isEnchantment 过滤,与类型 164 无关 |
| 212 | TARGET 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 全码段无命中。 |
| 237 | MINIONDAMAGE | 纯工具提示文案 ⚠️ | 超出两个分派器的类型范围(INSTANT ≤236、挂载 ≤228),不可能有分支;无 getStat 消费方;CEquipment__bakeDamageEffectsIntoTable(0x590BB0)只认 52 与 10,不认 237。但它有完整描述串(Minions Deal [VALUE] [DMGTYPE] Damage)。原版 9 个效果块分布在 8 个文件,全部是 DUMMY_* 前缀的占位词缀(DUMMY_BERSERKER_WOLFSHADE_DAMAGE 等)——这些词缀存在的意义就是让物品提示里出现一行「随从造成 X 伤害」,实际伤害来自随从自己的数据。 |
9.2 四个值得单说的
58 UNIT THEME 的机制根本不看 TYPE。CEffect__ctorFromDat 从 0x7AAD8C 起无条件读 UNITTHEME 键: 大写化 → sub_661830 按名字查主题 → 结果存进 CEffect+0x128(0x7AADDC), 这一路没有任何 TYPE 守卫。消费点在 CUnit__tickActiveEffects_dispatch 的 0x568184: 只判 +0x128 != 0,取 主题对象+0x18 的名字大写化后配合 CUnit__hasStateNamed。 所以真正驱动主题的是 UNITTHEME: 键,TYPE:UNIT THEME 只是习惯写法—— 换成别的 TYPE、只要留着 UNITTHEME 键,主题照样生效。
46 IDENTIFY 与 164 DISENCHANT 的效果本身什么都不做。 两者原版都写 DURATION:INSTANT,但在 INSTANT 跳表里都落 default, 而 default 是纯 retn(第 5 节)。鉴定由技能层实现; 去附魔实际由标志位 0x400 驱动——唯一写入点是 CEquipment__markNewEffectsAsEnchanted(0x73E20E), 清除方 CEffectMgr__removeEnchantments_disenchant(0x7AF480)按 flags & 0x400 与 CAffix__isEnchantment 过滤,全程与类型 164 无关。
237 MINIONDAMAGE 是纯工具提示文案。 它落在两个分派器的射程之外——INSTANT 的范围是 TYPE 6..236(cmp eax, 0E6h), 挂载是 TYPE 7..228(cmp ecx, 0DDh),所以 229–238 不可能有挂载分支, 237–238 连 INSTANT 分支也不可能有。它也没有任何 getStat 消费方, CEquipment__bakeDamageEffectsIntoTable(0x590BB0)只认 52 与 10。 但它的描述串是完整的:Minions Deal [VALUE] [DMGTYPE] Damage。 原版 9 个效果块散在 8 个文件里,全部是 DUMMY_* 前缀的占位词缀 (DUMMY_BERSERKER_WOLFSHADE_DAMAGE、DUMMY_EMBERMAGE_AVATAR_DAMAGE …)。 它存在的意义就是让物品提示里出现一行「随从造成 X 伤害」,实际伤害来自随从自己的数据。
CAUTION
这是整份数据里代价最大的一处误解。237 在 MOD 库里被写了 801 次(原版只有 9 次,且全是 DUMMY_* 占位词缀)。 照抄的人显然以为它真的会给随从加伤害——它不会,一个数值都不会进任何计算。
想给随从加伤害,要用的是 91 PERCENT PET DAMAGE, 描述串 +[VALUE]% pet and minion Damage。它的读取方式很特别:
.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_filtered(0x7AEA40)里 7 是「不过滤」的哨兵 (合法伤害类型只有 0..6),命中 0x7AEA53 直接返回 239 缓存、连表都不扫。 所以在 91 的效果块上写 DAMAGE_TYPE:FIRE 不会把加成限制成火焰——它照样全额计入。 原版用 19 次、MOD 库 422 次,是有实测支撑的正确写法。
全码段共 7 处读取点,全部核过是指令头,且都紧跟着 CUnit__getRootOwner:
| 函数 | 位置 |
|---|---|
sub_52BD90(近战) | 0x52BF76、0x52C0F4 |
sub_52C170(远程) | 0x52C366、0x52C4E4 |
sub_52DA30(近战,第二对镜像) | 0x52E8C3 |
sub_52E9D0(远程,第二对镜像) | 0x52F860 |
CUnit__applyEffectToSelf_dispatch | 0x563E20 |
两对镜像函数的关系见 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 分支 0x559ADF → CCharacter__setTarget), 212 只负责在玩家的增益条上挂图标和文字。
TIP
这两个类型可以当成**「只写文案、不产生效果」的展示位**来用: 挂上去会正常进增益条/物品提示,[VALUE]/[DURATION] 占位符照常渲染, 但引擎里没有任何代码会因为它们改变游戏状态。 212 尤其干净——零参数、可设时长、可 EXCLUSIVE 刷新。 但 237 不建议这么用:它的文案写死了「随从造成 X 伤害」,已经有 801 个 MOD 块把它当真属性写,再加只会加深误解。
反过来也要知道:引擎自己同样读不到它们。 CUnit__hasActiveEffectOfType 对这两个类型没有调用点, 所以不能指望拿它们去当别的原版机制的触发条件。
WARNING
扫描范围的诚实说明。 消费方是这样定位的:枚举 CUnit__getStat 的 371 个调用点以及三个额外包装器 (CBaseUnit__getEffectStat_selfPlusInventory、sub_5188F0、sub_512C40)的调用点, 逐点回读紧邻的 push 立即数;再加上按类型的布尔查询入口、直接扫表函数, 以及从数据侧反查原版用法定位子系统。 用 cmp 立即数做的补充搜索受工具单次 20 万条指令上限限制,分段跑过 0x401000–0x7D0000, 不能保证 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 个是实现留了洞。
| 死因 | 数量 | 类型 |
|---|---|---|
| 被后来的分类版取代 | 1 | 34 PERCENT ITEM REQUIREMENTS |
| 整个子系统没做 | 1 | 9 TO HIT BONUS |
| 开发期玩笑 | 4 | 130 EMOTIONAL DAMAGE、132 JAUNTINESS、133 SOCKS、134 LOGIC RESISTANCE |
| 实现有洞 | 3 | 147 ADD STAT TO LEVEL、149 SET STAT ON LEVEL、151 CLEAR STAT ON LEVEL |
| 实装完整、原版没用 | 6 | 42 PERCENT VIEW ANGLE BONUS、63 VELOCITY MULT、160 SPAWN UNIT、186 TRANSFORMATION TIME、187 TRAP DISARM CHANCE、208 CAST SKILL ON RANGED SKILL STRIKE |
| 引擎只读不写 | 1 | 238 CHEATED |
逐个交代:
| # | TYPE 名 | 死因 | 依据 |
|---|---|---|---|
| 34 | PERCENT ITEM REQUIREMENTS | 被后来的分类版取代 | 93/94/95/98/100/101 六个按 UNITTYPE 分类的减需求类型接管了它的功能;34 这个通用百分比版没人再读,描述串却留在表里 |
| 9 | TO HIT BONUS | 整个子系统没做 | 命中率系统在成品里不存在(闪避/格挡/失手取而代之)。9 的四条描述串也全是空的,所以它既不显示也不生效——在 6 个「真的没有代码」的零用量类型里只有它是这样 |
| 130 | EMOTIONAL DAMAGE | 开发期玩笑 | [VALUE] Emotional Damage |
| 132 | JAUNTINESS | 开发期玩笑 | +[VALUE] Jauntiness |
| 133 | SOCKS | 开发期玩笑 | [VALUE] Socks |
| 134 | LOGIC RESISTANCE | 开发期玩笑 | +[VALUE] Resistance to Logic |
| 147 | ADD STAT TO LEVEL | 实现有洞 | 写关卡自己的 CStatManager(CLevel+0x350),但缺了 146/148/150 都有的空指针门 |
| 149 | SET STAT ON LEVEL | 实现有洞 | 同上,0x55BC86 |
| 151 | CLEAR STAT ON LEVEL | 实现有洞 | 同上,0x55BCF8 |
| 42 | PERCENT VIEW ANGLE BONUS | 实装完整、原版没用 | CUnit__getViewAngleBonus(0x51B540)真读,结果流进 AI 索敌 |
| 63 | VELOCITY MULT | 实装完整、原版没用 | 同时占三条路径:数值查询、布尔查询、挂载分支 |
| 160 | SPAWN UNIT | 实装完整、原版没用 | 三条路径都有分支:INSTANT 0x55C3B6、挂载 0x5640D0、tick 0x569360 |
| 186 | TRANSFORMATION TIME | 实装完整、原版没用 | CMonster_tick_main 里除变身倒计时的每帧减量 |
| 187 | TRAP DISARM CHANCE | 实装完整、原版没用 | 陷阱触发前掷骰,掷赢就不触发 |
| 208 | CAST SKILL ON RANGED SKILL STRIKE | 实装完整、原版没用 | 安装/触发/执行/卸载四段链齐全 |
| 238 | CHEATED | 引擎只读不写 | 6 处 hasActiveEffectOfType(0xEE),命中就置全局标志位;但二进制里根本没有 CHEATED 这个字符串,没有任何代码路径能造出它 |
「被取代」只有一个,但很典型。34 PERCENT ITEM REQUIREMENTS 想做的事——按百分比减物品需求—— 最后是由六个按装备类别拆开的类型完成的: 93 REDUCED / 94 ARMOR / 95 SPELL / 98 MARTIAL / 100 RANGED / 101 MAGIC, 六个都有消费方(2.3 节的 sub_57D930 按 UNITTYPE 选型)。 34 被留在表里没删,描述串还是完整的——这正是下一节要说的问题。
「实现有洞」是这次新查出来的一类。146/148/150(ADD/SET/CLEAR STAT)与 147/149/151(…ON LEVEL)是同一段后端的两套入口, 唯一区别是后者把 CStatManager 换成关卡自己的那个:
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_desc(0x7A70B0),取描述串那一段是:
.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 * 2828 是这个构建里 std::wstring 的大小,所以整段就是 desc = 数组[TYPE]。 四张表按 2×2 选:
| 普通 | 持续型(OT) | |
|---|---|---|
| GOOD | g_EffectGoodDes_239 0x28A4300 | g_EffectGoodDesOT_239 0x28A5D28 |
| BAD | g_EffectBadDes_239 0x28A7750 | g_EffectBadDesOT_239 0x28A9178 |
四张表都在 EffectsList_dat_load_239(0x401000)里按行填充(GOODDES 0x401157、GOODDESOT 0x4011B0、BADDES 0x401209、BADDESOT 0x401262), 读取端只有 CEffect__build_tooltip_desc 一处。
关键在于:这段代码里没有任何「该类型有没有消费方」的检查。 只要 EFFECTSLIST.DAT 那一行写了描述串,提示就照常渲染、[VALUE] 照常替换。 于是「有文案、没代码」的类型在游戏里表现为——装备栏文字照显示,数值毫无作用:
| # | TYPE 名 | 会显示的文案 |
|---|---|---|
| 34 | PERCENT ITEM REQUIREMENTS | [VALUE]% reduced item level requirements |
| 130 | EMOTIONAL DAMAGE | [VALUE] Emotional Damage |
| 132 | JAUNTINESS | +[VALUE] Jauntiness |
| 133 | SOCKS | [VALUE] Socks |
| 134 | LOGIC RESISTANCE | +[VALUE] Resistance to Logic |
剩下那个 9 TO HIT BONUS 的四条描述串也全是空的,所以它既不显示也不生效—— 上面 6 个「没有代码」的零用量类型里,只有它是两头皆空。
NOTE
描述串全空本身不说明问题。239 行里有 28 个类型四条描述串都是空的, 其中 25 个有正常的消费方——158 TELEPORT、141 OPEN PORTAL、146 ADD STAT、 232 REMOVE EFFECT 这类动作型效果本来就不需要在提示里占一行。 只有当「无描述串」与「无消费方」同时成立时,才说明这一行是彻底的死条目; 符合的只有 9(另外 58 靠 UNITTHEME: 键生效、193 是设计上的空操作,都不算)。
9.5 真正的陷阱:有人在写、引擎不处理
零用量类型至少没人踩。用量不为零、却同样查不到消费方的有 10 个,按两库合计用量排序如下。 这 10 个不是一回事,看最后一列区分:58 靠同块的另一个键真的生效、193 是设计上的空操作, 这两个不算 bug;212/237 是有意为之的展示位;剩下 6 个 (11、24、46、131、135、164)写了就是白写。
| # | TYPE 名 | 原版 | MOD 库 | 为什么不生效 |
|---|---|---|---|---|
| 58 | UNIT THEME | 216 | 3178 | 机制由同块的 UNITTHEME: 键驱动,与 TYPE 无关(9.2 节) |
| 237 | MINIONDAMAGE | 9 | 801 | 纯工具提示文案;原版 9 处全是 DUMMY_* 占位词缀(9.2 节) |
| 193 | NONE | 19 | 275 | 拼错 TYPE 的落点,按设计什么都不做 |
| 11 | DAMAGE TAKEN | 0 | 70 | 百分比版 26 PERCENT DAMAGE TAKEN 有消费方(0x51ADF0,原版用 389 次),这个绝对值版没有 |
| 131 | FUTURISTICNESS | 0 | 21 | 开发期玩笑(+[VALUE] Futuristicness),却被 MOD 写了 21 次 |
| 212 | TARGET LOCKED | 1 | 17 | 纯增益条标记,真正换目标的是同块的 AGRO(9.2 节) |
| 24 | PERCENT TO HIT BONUS | 0 | 15 | 与 9 同族,命中率系统不存在 |
| 164 | DISENCHANT | 1 | 5 | 落 INSTANT default;去附魔由标志位 0x400 驱动(9.2 节) |
| 46 | IDENTIFY | 2 | 2 | 落 INSTANT default(纯 retn);鉴定由技能层做 |
| 135 | NOODLE ARMS | 1 | 0 | 开发期玩笑([VALUE] Noodle Arms)——原版自己也写了 1 次 |
237 MINIONDAMAGE 是这里面代价最大的一个:MOD 库里 801 个块。 原版那 9 处全都叫 DUMMY_*,本来就只是为了让提示里多一行字(9.2 节); 照抄的人多半以为它真的会给随从加伤害。
131 FUTURISTICNESS 被 MOD 写了 21 次,135 NOODLE ARMS 原版自己也写了 1 次—— 开发期玩笑类型混进正式数据,这件事双方都发生过。
拼错 TYPE 的情况也数了。 写了表里不存在的名字, 构造函数的初值 0xC1(0x7AAA7E)不会被覆盖,整个块静默变成 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__getViewAngleBonus(0x51B540):
视野角度 = (getStat(42) + 1.0) × (CUnit+0x338 + getStat(14))唯一调用方是 CCharacter__findAITarget(0x52243C),也就是 AI 索敌的视锥。 注意函数里还有一道短路:AI 管理器 flag 0 置位时直接返回 360.0(0x51B592), 即「全向感知」,这时 42 不起作用。
186 TRANSFORMATION TIMECMonster_tick_main 的 0x56AEAB / 0x56AED2:
[CUnit+0x76C] -= dt / (1.0 + (自身的186 + 随从[0]的186) / 100)CUnit+0x76C 是变身剩余时间,哨兵值 -999.0(flt_213C534)表示「没在变身」。 除数在分母上,所以值越大倒计时走得越慢——与描述串 Transformation time is increased by [VALUE]% 一致。 它会把自己和第一个随从的值加起来,这是 186/187 共用的一个惯用法。
187 TRAP DISARM CHANCECUnit__triggerTrapSkill_rollDisarm(0x5C8930,本次命名):
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__resolveStrikeHit 的 0x54E31F |
| 执行 | 施法模式 0(自己施法,带 CUnit+0x7DC 一秒递归防护) |
| 卸载 | tick OR 链 0x567157 → 0x5671B9 |
63 VELOCITY MULT:数值查询(CUnit__integrateMovement 0x542780)、 布尔查询(0x51A631、0x542EC9)、挂载分支(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) (0x5C114D、0x5C119C、0x5C11D7、0x5C122C、0x5C1258、0x5C12AC)逐件清点装备, 命中就 dword_331B3A0 |= 0x20 置全局标志。 而整个二进制里没有 CHEATED 这个字符串(宽窄两种编码都搜过), 说明没有任何代码路径能构造出这个效果——检测逻辑随成品发布了,触发它的数据没有。
10. 排错
「加了一个属性加成效果,游戏里没反应。」 查第 8 节该类型的前三列。若三列全空且三个分支列也空,先看第 9 节—— 它可能是开发期占位,也可能只是本次未定位。
「写了 DURATION:INSTANT,效果不触发。」 查「INSTANT 分支」列。空白就是空操作,175 个类型都是这样。
「DAMAGE_TYPE 写了但伤害加成没生效。」10/25 按伤害类型分七个桶记账(2.1 节)。写错桶等于加成落在没人取的地方。
「近战有效,远程没效(或反过来)。」 两条结算路径是镜像的独立函数(2.2 节),基础伤害类型分别读类型 0 和类型 1。
「堆了很多弹射反射,还是被打中。」 反射概率按独立事件合成,永远到不了 100%(第 3 节)。
「同样的效果挂装备上有用、放技能里没用。」 挂载路径与数值查询路径是两条独立的路。只有挂载分支的类型在挂上瞬间执行一次,与时长无关。
11. 边界与未完成的部分
11.1 数据快照
文中所有「用量」「块数」都是数出来的,不是估计。两个来源的规模不一样:
| 来源 | .DAT 文件 | 可解析 [EFFECT] 块 |
|---|---|---|
原版 E:/Torchlight 2/MEDIA | 16084 | 9984 |
MOD 库 D:/WizardProject/TL2 | 157950 | 200352 |
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不碰这个字段,函数内也只有两处写入点——但这块内存原本是什么,本次没查。MINLEVEL/MAXLEVEL/AFFIX在效果块里的归属未定论(2.6 节)。 它们可能来自嵌套子块被扫描一并计入,也可能确实是有人误写。CSkillEvent__ctorFromDat(0x6CEE60)也读DAMAGE_TYPE(0x6CF619), 技能事件那一侧的伤害类型语义本文没有展开。- 「本次未找到消费方」不等于「一定没有」。 这个限定同时适用于 第 8 节的三列空白、2.5 节的 90 个类型、2.6 节的丁类。 扫描方法与其边界写在各节末尾的提示框里。
本文覆盖的范围:六条路径的分派映射、79 个分派分支、tick 的两条 OR 链与 23 个动作块、 CAST SKILL ON x 的安装/触发/执行/卸载四段链、五种施法模式、锥形查询的实参与公式、 两个 tick 实现体、DAMAGE_TYPE 的解析与消费、[EFFECT] 块全部键名的对账——都已成表。