NOTE

逆向方法与数据支撑:本文以 32 位客户端 Torchlight2.exe(ImageBase 0x400000,文内一切 sub_XXXXXX 与 0xXXXXXX 均为绝对虚地址)为对象,证据分三档:O0 字节锚点(可直接在镜像里按字节核对)、O2 反编译读出的控制流、O3 引擎自己的键位数据(MEDIA/KEYMAPPING/ 三份 DAT)。全部结论已收敛进真值账本 C-0172(must-exact 级、guarded 状态)。

本文只到「什么时候下单、下给谁」为止。 真正启动移动/攻击的 sub_544200 与 CCharacter::castSelectedSkill 的内部没有逆,伤害管线一行都不在本文范围。凡是材料里标为「没逆 / 按名字推的 / 未实现 / 已知偏差」的,文内一律原样标注,不补全。

核心工程结论:左键在引擎眼里就叫 Move/Attack——数据里写着,汇编里也读得出来,两个独立来源逐条对上。按住不放会每 tick 重新下单,节奏交给被调方;攻击距离先过一道 ±2.5 的高度门再比水平距离,而远近不是角色的常量,是装备的函数。


0. 核心结论速览 ​

  1. 左键是「移动/攻击」二合一,名字是引擎自己起的:MEDIA/KEYMAPPING/KEYBIND.DAT 的第一条绑定就是 NAME:Move/Attack KEY:LMB EXCLUSIVE:true。同一张表里 Move(只走不打)是独立按键 `。⇒ 「只走」在引擎眼里是需要额外按键的例外,默认路径就是 Move/Attack。
  2. 两条调用链汇到同一个分叉函数:鼠标消息 sub_43D610 → sub_439750(点击派发)→ sub_42A4E0;每 tick 的 sub_439180(命令处理器)→ sub_438080(order 处理器)→ 同一个 sub_42A4E0。
  3. sub_42A4E0(controller, a5) 每次被调都重新分叉:光标下的单位进 controller+488(主目标)/ +512(次目标);CCharacter__isHostileTo 为真走攻击路(够不着就先靠近再 sub_544200,够得着直接 sub_544200);没有敌对目标且 a5 为真 ⇒ sub_426DC0() 走路。
  4. 主目标不粘:controller+488 每 tick 由悬停拾取刷新,不是点一次就锁住。
  5. 按住不放会持续下单,证据是 0x43A903 起的 20 个字节:cmp byte [ebp+52h],0 → jnz +0Dh 正好跨过把待处理点击目标 dword_287DBF8/DBFC 清成 −1 的三条指令(3+5+5=13 字节)→ mov byte [ebp+52h],1。第一 tick 清一次并置位,之后每 tick 目标还在就重复下单,直到松手(+0x52 在松手路径 0x43A69E 被清 0)。
  6. 下单是每 tick 的,节奏由被调方把关:纯移动 sub_426DC0 内部有 *(float*)(this+56) = 0.1 的 0.1 秒节流;攻击的节奏在 sub_544200 / sub_545070 里(攻击延迟 / 动画)。调用方一行冷却都不写。
  7. 攻击距离 CCharacter::isTargetInAttackRange = sub_5350C0,形状是「先高度门、再水平距离」:|dy| > 2.5 且不是远程 ⇒ 直接 false;然后 reach + rangeBonus >= (d − r目标) − r自己。
  8. 2.5 是从数据段读出来的:0x5351FE 处 fcomp dword ptr [0x216FDF8],0x216FDF8 的四个字节 00 00 20 40 就是小端 float 2.5。
  9. 远近由装备决定:CInventory::getItemAtEquipLocation(inv, 1) 与 (inv, 2) 两个武器槽,任一件 hasUnitType(0x23) 且不是 0xBC、也不是 0x74 ⇒ 远程分支;当前技能的 +36 字节为真也算远程。rangeBonus 只在远程时非 0,值是 getStat(78, 7, 0)。
  10. 三份 KEYMAPPING DAT 一共 64 条绑定:KEYBIND 40 + QUICKKEYS 14 + HELPERBINDINGS 10。隐藏 11、独占 48、带修饰键 14、鼠标 5,全部能映射。
  11. 原版数据自带一条冲突:CTRL+S 绑了两次(SoundDebug / SteamStatsData,都在 HELPERBINDINGS.DAT,都 EXCLUSIVE:false、都 HIDDEN:true)。谁赢没查。
  12. w 是 WeaponSwap、s 是 SkillMenu、a 是 AutoTarget ⇒ TL2 原生没有 WASD 移动,任何要加 WASD 的 MOD 都得先解决这三条冲突。
  13. TL2 的字符串转 bool 只看首字符:非空且首字符 ∉ {F, f, 0} ⇒ 真。MOD 里写 no / off 全都是 true。

1. 左键是 Move/Attack 二合一 ​

1.1 引擎自身命名与数据定义 ​

该结论源自两个互不依赖的证据链:反汇编控制流首先揭示了「移动与普通攻击共用同一入口」,随后原版配置文件 KEYBIND.DAT 第一条显式声明 Move/Attack。汇编实现与数据定义完全吻合,互为独立佐证。

KEYBIND.DAT 里与这条口径直接相关的绑定:

NAME(原始拼写)DISPLAYNAMEKEYEXCLUSIVEHIDDEN口径
Move/AttackMove/AttackLMBtruefalse第一条。移动与攻击二合一,默认路径
MoveMove`truefalse只走不打的独立键 ⇒ 「只走」是需要额外按键的例外
CastSKillCast Active SkillRMBtruefalse释放当前技能。SKill 的大写 K 是数据里的原始拼写
HoldPositionHold PositionSHIFTfalsefalse站桩打、不追(按 DISPLAYNAME 推断字面语义)
AttackOnlyTargetsAttack Only Valid TargetsCTRLtruefalse只打目标、不误触地面(按 DISPLAYNAME 推断字面语义)
AutoTargetAuto-Targetafalsetrue隐藏绑定

HoldPosition 与 AttackOnlyTargets 两行的「口径」一列是按 DISPLAYNAME 推断的字面语义,底层具体执行分支未做深入展开。

1.2 两条调用链,一个分叉点 ​

鼠标消息  sub_43D610 ──► sub_439750(点击派发)        ──► sub_42A4E0(移动/攻击分叉)
每 tick   sub_439180(命令处理器)──► sub_438080(order 处理器)──► sub_42A4E0(同一个)

点击那一刻走上面那条,之后每个 tick 走下面那条,终点都是 sub_42A4E0。这就是为什么「按住」与「点一下」在引擎里是同一个函数在反复跑,而不是两套逻辑。

1.3 sub_42A4E0(controller, a5) 的三步分叉 ​

每次被调都从头分叉,不带上一次的状态:

步做什么细节
① 取目标光标下的单位写进 controller+488(主目标)/ controller+512(次目标)这两个槽每 tick 由悬停拾取刷新,见 §1.4
② 有敌对目标CCharacter__isHostileTo(目标) 为真 ⇒ 攻击路isTargetInAttackRange() 为假 ⇒ setTarget + vt+512(player, 4, 0)(移动模式 4 = 靠近)+ vt+584(转向),随后仍然调 sub_544200(...);为真 ⇒ 直接 sub_544200(...)
③ 没有敌对目标且 a5 为真 ⇒ sub_426DC0() 走路sub_426DC0 内部有 0.1 秒节流,见 §2.3

a5 是「允不允许走」的开关。点击路径上传的是 1——0x43A8FA 处的 9 个字节 6A 01 8B CD E8 DD FB FE FF 就是 push 1 / mov ecx,ebp / call sub_42A4E0。

sub_544200 是真正「启动移动/攻击」的函数,本文只到调它为止,它内部怎么起动画、怎么算伤害没有逆。

1.4 主目标每 tick 刷新,不粘 ​

controller+488 不是点一次就锁住的。它由悬停拾取管线每 tick 重写,光标挪开了目标就变。⇒ 「按住左键扫过一群怪」在引擎里等价于每 tick 对光标当前指着的那只重新下单。

TIP

做 MOD 的工程启示

  • 左键的「移动/攻击二合一」是原版操作手感的基础。在自定义键位方案中若将其强行拆分,改变的不只是单键映射,更直接影响分叉函数 sub_42A4E0 的触发前提。
  • 若需要「只走不打」,原版数据中已存在独立绑定 Move(默认键 `),它并非 Move/Attack 的修饰状态。
  • HoldPosition(SHIFT)、AttackOnlyTargets(CTRL)、Move(`)三键在本文中仅依据数据定义确立名称,具体底层分支逻辑需结合独立反编译分析。

2. 按住不放会持续下单 ​

2.1 0x43A903 起的 20 个字节 ​

这是「按住持续下单」的全部证据。原始字节:

80 7D 52 00 75 0D 83 C8 FF A3 F8 DB 87 02 A3 FC DB 87 02 C6

拆开看:

相对偏移字节指令长度作用
+080 7D 52 00cmp byte ptr [ebp+52h], 04查「已在按住态」标志
+475 0Djnz +0Dh2标志非零 ⇒ 跳过下面 13 字节
+683 C8 FFor eax, -13准备 −1
+9A3 F8 DB 87 02mov dword_287DBF8, eax5待处理点击目标清成 −1
+14A3 FC DB 87 02mov dword_287DBFC, eax5同上,第二个槽
+19C6 …mov byte ptr [ebp+52h], 1 的首字节—处理完之后置位

jnz +0Dh 的 0x0D = 13 = 3+5+5,正好跨过那三条清 −1 的指令,落在 C6 上。于是:

  • 第一 tick:标志为 0,不跳 ⇒ 清一次待处理目标,然后置位。
  • 之后每 tick:标志为 1,跳过清除 ⇒ 目标还在,就再下一次单。
  • 松手:+0x52 在松手路径 0x43A69E 被清 0,循环结束。

2.2 它紧接着 call sub_42A4E0 ​

0x43A8FA 的 9 字节 push 1 / mov ecx,ebp / call sub_42A4E0 加 9 正好是 0x43A903。也就是说这 20 字节是 call sub_42A4E0 返回之后的第一件事——每 tick 先分叉一次(§1.3),回来再决定要不要把目标清掉。

2.3 节奏由被调方把关 ​

下单是每 tick 的,但攻击不会每 tick 挥一次。节奏在被调方:

分支节奏在哪证据
纯移动 sub_426DC0函数内部 *(float*)(this+56) = 0.1,0.1 秒的下单节流0x426DC0 序言 16 字节 83 EC 1C 56 8B F1 8D 4C 24 08 FF 15 68 71 11 02
攻击sub_544200 / sub_545070(攻击延迟 / 动画)内部没逆,只定位到节奏归它们管

调用方 sub_42A4E0 及其上游一行冷却都不写。

TIP

这对做 MOD 意味着什么

  • 「按住左键连续攻击」不是 UI 层的自动重复,是每 tick 真的重新走一遍分叉。想改攻击节奏,去看攻击延迟与动画那一侧(sub_544200 / sub_545070),不在输入层。
  • 移动那 0.1 秒是写死在 sub_426DC0 里的浮点常量,不来自 DAT。

3. 攻击距离:CCharacter::isTargetInAttackRange(sub_5350C0) ​

3.1 判定形状:先高度门,再水平距离 ​

① 高度门   if (fabs(dy) > 2.5 && !远程)  return false;      // 先于一切
② 水平距离 d = length(目标位置 − 自己位置, 其中 Y 分量置 0);
③ 判定     return reach + rangeBonus >= (d − r目标) − r自己;

顺序是硬的:高度门在水平距离比较之前。一只怪站在比你高 2.5 以上的台子上,近战连水平距离都不比就判「够不着」。

3.2 reach 的构成 ​

reach      = sub_52A9D0(player) * *(float*)(player + 1588)
rangeBonus = 远程 ? getStat(78, 7, 0) : 0
  • sub_52A9D0 取的是当前选中技能的 +108 字段,并会被武器抬高;双持时取两把里大的那个。
  • +1588 是单位上的一个 float 乘子。
  • rangeBonus 只在远程分支非 0,值是 stat 下标 78。

技能的 +108、单位的 +1588、stat 78 各自对应哪个 DAT 键名,本文材料里没有,不猜。

3.3 远近由装备决定 ​

「远程标志」(§3.4 汇编里的 bl)这样算:

条件读什么结果
武器槽 1CInventory::getItemAtEquipLocation(inv, 1) 的物品 hasUnitType(0x23) 且不是 0xBC、不是 0x74远程
武器槽 2CInventory::getItemAtEquipLocation(inv, 2),同上判定远程
当前技能技能的 +36 字节为真远程

三条任一成立即远程。⇒ 「够不够得着」是装备的函数,不是角色的常量。三个 unittype 下标在材料里只有十六进制值,没有对应到 UNITTYPES 名字。

3.4 高度门的字节,与那个 2.5 ​

0x5351FE 起 18 字节:

D8 1D F8 FD 16 02 DF E0 F6 C4 41 75 08 84 DB 0F 84 D9
字节指令作用
D8 1D F8 FD 16 02fcomp dword ptr [0x216FDF8]与数据段里的常量比较
DF E0fnstsw ax取 x87 状态字
F6 C4 41test ah, 41h
75 08jnz +8不超过 2.5 ⇒ 跳过这道门
84 DBtest bl, blbl = 远程标志(§3.3)
0F 84 D9 …jz …非远程 ⇒ 返回 0(够不着)

常量本体在 0x216FDF8,4 字节 00 00 20 40,小端 float = 2.5。该数值直接提取自可执行文件的数据段常量。

TIP

做 MOD 的工程启示

  • 为武器配置增减 0x23、0xBC、0x74 等 unittype 会直接改变攻击距离的逻辑分支:包括是否走远程分支、是否享受 getStat(78) 射程加成、以及是否受 ±2.5 高度门约束。
  • 近战存在硬性的 ±2.5 高度门,远程无此限制。对于分布在高低差地形上的目标,近战因高度截断无法触及而远程可正常命中,可据此设计地形机制。
  • reach 受到武器基础属性的抬升,双持时取数值较大者。调整武器即等价于调整 reach,无需修改技能定义。

4. 键位全表 ​

4.1 三份 DAT 与普查 ​

MEDIA/KEYMAPPING/ 下三份 DAT(UTF-16LE),每条绑定是一个 [KEYBIND] … [/KEYBIND] 块,字段 NAME / DISPLAYNAME / KEY / MODIFIERKEY / EXCLUSIVE / HIDDEN。

文件条数
KEYBIND.DAT40
QUICKKEYS.DAT14
HELPERBINDINGS.DAT10
合计64

普查:隐藏 11 · 独占 48 · 带修饰键 14 · 鼠标 5 · 映射不出来的 0。HELPERBINDINGS.DAT 的 10 条全部 HIDDEN:true、全部 EXCLUSIVE:false。

KEY 与 MODIFIERKEY 都是字符串,大小写混用(KEY:F 与 KEY:c 并存)。引擎侧怎么解析这两个串没逆——下面的表照数据原样列,不折大小写。

4.2 64 条全表 ​

#NAMEDISPLAYNAMEKEYMODIFIERKEYEXCLUSIVEHIDDEN来源
1Move/AttackMove/AttackLMBtruefalseKEYBIND
2MoveMove`truefalseKEYBIND
3CastSKillCast Active SkillRMBtruefalseKEYBIND
4InventoryMenuInventoryitruefalseKEYBIND
5PetMenuPet InventoryptruefalseKEYBIND
6HoldPositionHold PositionSHIFTfalsefalseKEYBIND
7AutoTargetAuto-TargetafalsetrueKEYBIND
8AttackOnlyTargetsAttack Only Valid TargetsCTRLtruefalseKEYBIND
9AutomapAutomapmtruefalseKEYBIND
10AutomapResetAutomap ResetmSHIFTtruefalseKEYBIND
11AutomapVisibleMap VisibilitymCTRLtruefalseKEYBIND
12SkillMenuSkill MenustruefalseKEYBIND
13StatMenuCharacter Stats MenuctruefalseKEYBIND
14OptionsMenuOptions MenuESCtruefalseKEYBIND
15CloseAllClose All MenusSPACEtruefalseKEYBIND
16JournalArcane StatisticsjtruefalseKEYBIND
17WeaponSwapWeapon SwapwtruefalseKEYBIND
18SkillSwapActive Skill SwapTABtruefalseKEYBIND
19QuestMenuQuest MenuqtruefalseKEYBIND
20ShowItemsShow ItemsALTtruefalseKEYBIND
21CycleSkillDownCycle Skills Down,truefalseKEYBIND
22CycleSkillUpCycle Skills Up.truefalseKEYBIND
23AutomapZoomOutAutomap Zoom Out[truefalseKEYBIND
24AutomapZoomInAutomap Zoom In]truefalseKEYBIND
25CameraZoomOutCamera Zoom OutMWDOWNtruefalseKEYBIND
26CameraZoomInCamera Zoom InMWUPtruefalseKEYBIND
27ResetCameraReset CameraMMBtruefalseKEYBIND
28F1Function 1F1truefalseKEYBIND
29F2Function 2F2truefalseKEYBIND
30F3Function 3F3truefalseKEYBIND
31F4Function 4F4truefalseKEYBIND
32F5Function 5F5truefalseKEYBIND
33F6Function 6F6truefalseKEYBIND
34F7Function 7F7truefalseKEYBIND
35F8Function 8F8truefalseKEYBIND
36F9Function 9F9truefalseKEYBIND
37F10Function 10F10truefalseKEYBIND
38F11Function 11F11truefalseKEYBIND
39F12Function 12F12truefalseKEYBIND
40QUICKSAVEQuick SaveF11CTRLtruefalseKEYBIND
41QK1Quickslot 11truefalseQUICKKEYS
42QK2Quickslot 22truefalseQUICKKEYS
43QK3Quickslot 33truefalseQUICKKEYS
44QK4Quickslot 44truefalseQUICKKEYS
45QK5Quickslot 55truefalseQUICKKEYS
46QK6Quickslot 66truefalseQUICKKEYS
47QK7Quickslot 77truefalseQUICKKEYS
48QK8Quickslot 88truefalseQUICKKEYS
49QK9Quickslot 99truefalseQUICKKEYS
50QK0Quickslot 00truefalseQUICKKEYS
51BESTPOTIONBest Health PotionzfalsefalseQUICKKEYS
52BESTMANABest Mana PotionxfalsefalseQUICKKEYS
53BESTPETPOTIONBest Pet Health PotionzSHIFTfalsefalseQUICKKEYS
54BESTPETMANABest Pet Mana PotionxSHIFTfalsefalseQUICKKEYS
55ConsoleConsoleINSERTfalsetrueHELPERBINDINGS
56FPSFPS DetailsFCTRLfalsetrueHELPERBINDINGS
57SoundDebugSound DebugSCTRLfalsetrueHELPERBINDINGS
58PlayerEffectDataPlayer Effect DataECTRLfalsetrueHELPERBINDINGS
59PlayerQuestDataPlayer Quest DataQCTRLfalsetrueHELPERBINDINGS
60CombatLogCombatLogcCTRLfalsetrueHELPERBINDINGS
61TimeOfDayTime Of DayTCTRLfalsetrueHELPERBINDINGS
62LevelDataLevel DataLCTRLfalsetrueHELPERBINDINGS
63PeerDataPeer DataPCTRLfalsetrueHELPERBINDINGS
64SteamStatsDataSteam Stats DataSCTRLfalsetrueHELPERBINDINGS

4.3 数据里本来就有的冲突 ​

同一个 (KEY, MODIFIERKEY) 出现多次是原版数据的性质,不是解析错误:

键绑定性质
m / SHIFT+m / CTRL+mAutomap / AutomapReset / AutomapVisible靠 MODIFIERKEY 区分,正常
z / SHIFT+z,x / SHIFT+xBESTPOTION / BESTPETPOTION,BESTMANA / BESTPETMANA同上,正常
c / CTRL+cStatMenu / CombatLog同上,正常
q / CTRL+QQuestMenu / PlayerQuestData同上,正常
CTRL+S(两次)SoundDebug / SteamStatsData真冲突。两条都在 HELPERBINDINGS.DAT,都 EXCLUSIVE:false、都 HIDDEN:true。谁赢没查

CTRL+S 是把单字符大小写折叠后查出来的唯一一处同键同修饰键重复。引擎侧怎么裁决、EXCLUSIVE 在这种场合起不起作用,没逆。

4.4 w / s / a:WASD 的三条路障 ​

表里第 17、12、7 行:

键原版绑定HIDDEN
wWeaponSwap(换武器)false
sSkillMenu(技能面板)false
aAutoTarget(自动索敌)true

64 条里没有一条 KEY 是 d。TL2 原生没有 WASD 移动——移动是左键 Move/Attack(§1)。任何「加 WASD」的 MOD 都得先把这三条挪开,其中 a 那条是隐藏绑定,界面上看不见,容易漏。

4.5 EXCLUSIVE / HIDDEN 的取值规则:仅判定首字符 ​

TL2 将配置字符串转换为布尔值的规则为:字符串非空且首字符 ∉ {F, f, 0} 时均判定为真。因此在 MOD 数据中即使书写 EXCLUSIVE:no 或 HIDDEN:off,引擎在底层均解释为 true。若需显式声明为假,必须使用 false、F 或 0 开头。

TIP

做 MOD 的工程启示

  • 开发 WASD 类移动 MOD 时,首要任务并非直接引入新绑定,而是将原版占用的 w、s、a 键位重新分配(其中 a 属于隐藏绑定)。
  • 复制并修改原版 HELPERBINDINGS.DAT 时,CTRL+S 重复属于原版固有数据,无需视作配置异常。
  • 布尔字段避免书写 no / off 等非标词,严格使用 false / 0。

5. 未闭环与已知偏差清单 ​

下表列出逆向分析中明确保留的未决项,以及独立运行时(复刻侧)实测发现的边界差异:

项现状
sub_544200(启动移动/攻击)与 CCharacter::castSelectedSkill 的内部未深入分析。本文只覆盖指令分发与下单判定时机。复刻侧伤害走 core-sim 的 Attack(),与原版技能执行体系不同。
HoldPosition(SHIFT)引擎底层汇编分支未分析;复刻侧依据字面语义实现为驻足射击。
AttackOnlyTargets(CTRL)、Move(`)引擎底层汇编分支未分析;复刻侧仅解析了键位定义,未做行为绑定。
光标拾取引擎按渲染包围盒执行像素/几何拾取;复刻侧采用「脚印圆点落点判定」近似,存在极小范围判定差异。
高度门在复刻侧的开关原版常量 MELEE_HEIGHT_GATE = 2.5 确认有效;复刻侧因实体坐标基准不一致暂时默认关停高度门。原版引擎中高度门确定存在且常驻生效。
reach 在复刻侧采用常量 1.5(0.5+0.25+0.75,来自默认值表);原版引擎实际为 技能字段(+108) × 单位缩放(+1588) 并结合装备抬升。
技能 +108、单位 +1588、技能 +36、stat 78、unittype 0x23/0xBC/0x74 对应的数据键名当前逆向仅定位到底层内存偏移与类型码,尚未一一绑定至数据表字符串。
CTRL+S 两条冲突绑定的裁决仲裁引擎内部裁决先后顺序尚未单独逆向。
KEY / MODIFIERKEY 字符串解析逻辑引擎对键名字符串的具体字典匹配规则未展开。
寻路算法原版采用 8 邻域走格 A*(sub_5425A0 → sub_464010 → sub_463EA0);复刻侧简易直线滑动在复杂死角下存在停滞问题。
测量覆盖复刻侧实测仅覆盖 echopass_jt / echopass_pb 地图变体。

附录 A:虚地址与字节锚点表(Torchlight2.exe,ImageBase 0x400000) ​

功能虚地址 / 符号
鼠标消息入口sub_43D610
点击派发sub_439750
每 tick 命令处理器 / order 处理器sub_439180 / sub_438080
移动/攻击分叉sub_42A4E0(controller, a5);主目标 controller+488、次目标 controller+512
靠近 / 转向(虚表)vt+512(player, 4, 0)(移动模式 4)/ vt+584
启动移动/攻击(内部没逆)sub_544200;攻击节奏亦涉及 sub_545070
纯移动(0.1 秒节流)sub_426DC0,*(float*)(this+56) = 0.1
点击下单 call(a5=1)0x43A8FA,9 B:6A 01 8B CD E8 DD FB FE FF
按住持续下单门0x43A903,20 B:80 7D 52 00 75 0D 83 C8 FF A3 F8 DB 87 02 A3 FC DB 87 02 C6
待处理点击目标dword_287DBF8 / dword_287DBFC
松手清标志0x43A69E([ebp+52h] 清 0)
攻击距离判定CCharacter::isTargetInAttackRange = sub_5350C0
高度门0x5351FE,18 B:D8 1D F8 FD 16 02 DF E0 F6 C4 41 75 08 84 DB 0F 84 D9
高度门常量 2.50x216FDF8,4 B:00 00 20 40
reach 的技能侧因子sub_52A9D0(当前技能 +108,被武器抬高,双持取大)
装备槽读取CInventory::getItemAtEquipLocation(inv, 1) / (inv, 2)
远程 unittype 判定hasUnitType(0x23) 且非 0xBC / 0x74
远程加成getStat(78, 7, 0)
原版寻路sub_5425A0 → sub_464010 → sub_463EA0

附录 B:材料来源 ​

  • 真值账本 truth/ledger.jsonl:C-0172(must-exact / O0 / guarded),本文的主干。
  • 证据 JSON 原版游戏分析/reversed_cpp/REGISTRY/attack_trigger_semantics.json:调用链、分叉、字节锚点、攻击距离公式、复刻侧实测与未闭环清单。
  • 键位工具 tools/f2_keybinds.py:三份 KEYMAPPING DAT 的解析口径、冲突报告、bool 首字符规则、project.godot [input] 段生成。
  • 键位数据 build/f2/keybinds.json:64 条绑定原始串与普查数字(§4 全表的来源)。
  • 引擎数据 MEDIA/KEYMAPPING/{KEYBIND,QUICKKEYS,HELPERBINDINGS}.DAT:第二 oracle。