NOTE

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

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

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


0. 核心结论速览

  1. 左键是「移动/攻击」二合一,名字是引擎自己起的MEDIA/KEYMAPPING/KEYBIND.DAT第一条绑定就是 NAME:Move/Attack KEY:LMB EXCLUSIVE:true。同一张表里 Move(只走不打)是独立按键 `。⇒ 「只走」在引擎眼里是需要额外按键的例外,默认路径就是 Move/Attack。
  2. 两条调用链汇到同一个分叉函数:鼠标消息 sub_43D610sub_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],0jnz +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 是从数据段读出来的0x5351FEfcomp 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。两边逐条对上——这种情况不多见,引擎自己的数据当了第二个 oracle。

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

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

HoldPositionAttackOnlyTargets 两行的「口径」一列是按 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::isTargetInAttackRangesub_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 意味着什么

  • 给武器加/减 0x230xBC0x74 这几个 unittype 会直接改攻击距离的分支:走不走远程、吃不吃 getStat(78) 的加成、受不受高度门管。
  • 近战有一条 ±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

KEYMODIFIERKEY 都是字符串,大小写混用(KEY:FKEY: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+zx / SHIFT+xBESTPOTION / BESTPETPOTIONBESTMANA / 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 把字符串转 bool 的规则是:非空且首字符 ∉ {F, f, 0} ⇒ 真。⇒ MOD 里写 EXCLUSIVE:noHIDDEN:off,引擎读到的都是 true。想写假,用 false / F / 0 开头。

TIP

这对做 MOD 意味着什么

  • 做 WASD 类 MOD,第一步不是加绑定,是把 w / s / a 三条原版绑定挪开(a 是隐藏的)。
  • 复制原版 HELPERBINDINGS.DAT 改的时候,CTRL+S 那两条已经是重的,别把它当成自己改出来的 bug。
  • 布尔字段别写 no / off——它们是真。

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

下表把材料里标为「没逆 / 按名字推的 / 未实现 / 已知偏差」的项全部列出。「复刻实现侧」指照本条口径接进 Godot 竞技场的那份实现,它与引擎的差别也一并记在这里。

现状
sub_544200(启动移动/攻击)与 CCharacter::castSelectedSkill 的内部没逆。本文只到「什么时候下单、下给谁」为止。复刻侧的伤害走 core-sim 的 Attack(),与原版的技能执行不是一条路
HoldPosition(SHIFT)引擎行为没逆。复刻侧按名字实现成「按住不挪窝」
AttackOnlyTargets(CTRL)、Move`引擎行为没逆。复刻侧只解析了绑定,没有实现
光标拾取引擎按渲染包围盒拾取(能点到怪的头);复刻侧用「地面点落在脚印圆里」近似。后果是偶尔点了没反应,已知偏差
高度门在复刻侧的开关常量 MELEE_HEIGHT_GATE = 2.5 留着,开关 MELEE_HEIGHT_GATE_ENABLED 默认关。接上之后近战判成永远够不着(hits=0):echopass_jt 水平只差 0.28u 而 dy = 9.18(怪 y=8.74、玩家 y=−0.44),echopass_pb dy = 6.18,两张图都是——复刻侧怪的节点 Y 与玩家物理体的 Y 不在同一个基准上。别把开关读成「引擎没有高度门」,引擎有;Y 差 6–9u 的成因没查
reach 在复刻侧用常量 1.5(0.5+0.25+0.75,来自 DAT 硬编默认值表 B′);没有远程分支;r自己 折进了 reach。引擎是 技能的+108 × 单位的+1588 并被武器抬高。没接
技能 +108、单位 +1588、技能 +36、stat 78、unittype 0x23/0xBC/0x74 对应的 DAT 键名 / UNITTYPES 名字材料里只有偏移与下标,没有名字
CTRL+S 两条绑定谁赢没查。键位工具只报冲突,不裁决
KEY / MODIFIERKEY 字符串在引擎侧怎么解析(大小写、具名键)没逆
寻路原版走的是走格 A*sub_5425A0sub_464010(世界→格)→ sub_463EA0(A* core)。复刻侧是直线 + 轴向贴墙滑(先只走 X、再只走 Z、都不行就原地),凹角里死锁——实测杀掉第一只后玩家停在 (49.7, 88.4)、下一只在 12.1u 外、moving=true,位置 3,000 帧不动。没做
测量覆盖复刻侧只在 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_5425A0sub_464010sub_463EA0

附录 B:材料来源

  • 真值账本 truth/ledger.jsonlC-0172must-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。