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. 核心结论速览
- 左键是「移动/攻击」二合一,名字是引擎自己起的:
MEDIA/KEYMAPPING/KEYBIND.DAT的第一条绑定就是NAME:Move/Attack KEY:LMB EXCLUSIVE:true。同一张表里Move(只走不打)是独立按键`。⇒ 「只走」在引擎眼里是需要额外按键的例外,默认路径就是 Move/Attack。 - 两条调用链汇到同一个分叉函数:鼠标消息
sub_43D610→sub_439750(点击派发)→sub_42A4E0;每 tick 的sub_439180(命令处理器)→sub_438080(order 处理器)→ 同一个sub_42A4E0。 sub_42A4E0(controller, a5)每次被调都重新分叉:光标下的单位进controller+488(主目标)/+512(次目标);CCharacter__isHostileTo为真走攻击路(够不着就先靠近再sub_544200,够得着直接sub_544200);没有敌对目标且a5为真 ⇒sub_426DC0()走路。- 主目标不粘:
controller+488每 tick 由悬停拾取刷新,不是点一次就锁住。 - 按住不放会持续下单,证据是
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)。 - 下单是每 tick 的,节奏由被调方把关:纯移动
sub_426DC0内部有*(float*)(this+56) = 0.1的 0.1 秒节流;攻击的节奏在sub_544200/sub_545070里(攻击延迟 / 动画)。调用方一行冷却都不写。 - 攻击距离
CCharacter::isTargetInAttackRange=sub_5350C0,形状是「先高度门、再水平距离」:|dy| > 2.5 且不是远程⇒ 直接 false;然后reach + rangeBonus >= (d − r目标) − r自己。 - 2.5 是从数据段读出来的:
0x5351FE处fcomp dword ptr [0x216FDF8],0x216FDF8的四个字节00 00 20 40就是小端 float 2.5。 - 远近由装备决定:
CInventory::getItemAtEquipLocation(inv, 1)与(inv, 2)两个武器槽,任一件hasUnitType(0x23)且不是0xBC、也不是0x74⇒ 远程分支;当前技能的+36字节为真也算远程。rangeBonus只在远程时非 0,值是getStat(78, 7, 0)。 - 三份 KEYMAPPING DAT 一共 64 条绑定:KEYBIND 40 + QUICKKEYS 14 + HELPERBINDINGS 10。隐藏 11、独占 48、带修饰键 14、鼠标 5,全部能映射。
- 原版数据自带一条冲突:
CTRL+S绑了两次(SoundDebug/SteamStatsData,都在 HELPERBINDINGS.DAT,都EXCLUSIVE:false、都HIDDEN:true)。谁赢没查。 w是 WeaponSwap、s是 SkillMenu、a是 AutoTarget ⇒ TL2 原生没有 WASD 移动,任何要加 WASD 的 MOD 都得先解决这三条冲突。- TL2 的字符串转 bool 只看首字符:非空且首字符 ∉ {F, f, 0} ⇒ 真。MOD 里写
no/off全都是 true。
1. 左键是 Move/Attack 二合一
1.1 引擎自身命名与数据定义
该结论源自两个互不依赖的证据链:反汇编控制流首先揭示了「移动与普通攻击共用同一入口」,随后原版配置文件 KEYBIND.DAT 第一条显式声明 Move/Attack。汇编实现与数据定义完全吻合,互为独立佐证。
KEYBIND.DAT 里与这条口径直接相关的绑定:
| NAME(原始拼写) | DISPLAYNAME | KEY | EXCLUSIVE | HIDDEN | 口径 |
|---|---|---|---|---|---|
Move/Attack | Move/Attack | LMB | true | false | 第一条。移动与攻击二合一,默认路径 |
Move | Move | ` | true | false | 只走不打的独立键 ⇒ 「只走」是需要额外按键的例外 |
CastSKill | Cast Active Skill | RMB | true | false | 释放当前技能。SKill 的大写 K 是数据里的原始拼写 |
HoldPosition | Hold Position | SHIFT | false | false | 站桩打、不追(按 DISPLAYNAME 推断字面语义) |
AttackOnlyTargets | Attack Only Valid Targets | CTRL | true | false | 只打目标、不误触地面(按 DISPLAYNAME 推断字面语义) |
AutoTarget | Auto-Target | a | false | true | 隐藏绑定 |
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拆开看:
| 相对偏移 | 字节 | 指令 | 长度 | 作用 |
|---|---|---|---|---|
| +0 | 80 7D 52 00 | cmp byte ptr [ebp+52h], 0 | 4 | 查「已在按住态」标志 |
| +4 | 75 0D | jnz +0Dh | 2 | 标志非零 ⇒ 跳过下面 13 字节 |
| +6 | 83 C8 FF | or eax, -1 | 3 | 准备 −1 |
| +9 | A3 F8 DB 87 02 | mov dword_287DBF8, eax | 5 | 待处理点击目标清成 −1 |
| +14 | A3 FC DB 87 02 | mov dword_287DBFC, eax | 5 | 同上,第二个槽 |
| +19 | C6 … | 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) : 0sub_52A9D0取的是当前选中技能的+108字段,并会被武器抬高;双持时取两把里大的那个。+1588是单位上的一个 float 乘子。rangeBonus只在远程分支非 0,值是 stat 下标 78。
技能的 +108、单位的 +1588、stat 78 各自对应哪个 DAT 键名,本文材料里没有,不猜。
3.3 远近由装备决定
「远程标志」(§3.4 汇编里的 bl)这样算:
| 条件 | 读什么 | 结果 |
|---|---|---|
| 武器槽 1 | CInventory::getItemAtEquipLocation(inv, 1) 的物品 hasUnitType(0x23) 且不是 0xBC、不是 0x74 | 远程 |
| 武器槽 2 | CInventory::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 02 | fcomp dword ptr [0x216FDF8] | 与数据段里的常量比较 |
DF E0 | fnstsw ax | 取 x87 状态字 |
F6 C4 41 | test ah, 41h | |
75 08 | jnz +8 | 不超过 2.5 ⇒ 跳过这道门 |
84 DB | test bl, bl | bl = 远程标志(§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.DAT | 40 |
QUICKKEYS.DAT | 14 |
HELPERBINDINGS.DAT | 10 |
| 合计 | 64 |
普查:隐藏 11 · 独占 48 · 带修饰键 14 · 鼠标 5 · 映射不出来的 0。HELPERBINDINGS.DAT 的 10 条全部 HIDDEN:true、全部 EXCLUSIVE:false。
KEY 与 MODIFIERKEY 都是字符串,大小写混用(KEY:F 与 KEY:c 并存)。引擎侧怎么解析这两个串没逆——下面的表照数据原样列,不折大小写。
4.2 64 条全表
| # | NAME | DISPLAYNAME | KEY | MODIFIERKEY | EXCLUSIVE | HIDDEN | 来源 |
|---|---|---|---|---|---|---|---|
| 1 | Move/Attack | Move/Attack | LMB | true | false | KEYBIND | |
| 2 | Move | Move | ` | true | false | KEYBIND | |
| 3 | CastSKill | Cast Active Skill | RMB | true | false | KEYBIND | |
| 4 | InventoryMenu | Inventory | i | true | false | KEYBIND | |
| 5 | PetMenu | Pet Inventory | p | true | false | KEYBIND | |
| 6 | HoldPosition | Hold Position | SHIFT | false | false | KEYBIND | |
| 7 | AutoTarget | Auto-Target | a | false | true | KEYBIND | |
| 8 | AttackOnlyTargets | Attack Only Valid Targets | CTRL | true | false | KEYBIND | |
| 9 | Automap | Automap | m | true | false | KEYBIND | |
| 10 | AutomapReset | Automap Reset | m | SHIFT | true | false | KEYBIND |
| 11 | AutomapVisible | Map Visibility | m | CTRL | true | false | KEYBIND |
| 12 | SkillMenu | Skill Menu | s | true | false | KEYBIND | |
| 13 | StatMenu | Character Stats Menu | c | true | false | KEYBIND | |
| 14 | OptionsMenu | Options Menu | ESC | true | false | KEYBIND | |
| 15 | CloseAll | Close All Menus | SPACE | true | false | KEYBIND | |
| 16 | Journal | Arcane Statistics | j | true | false | KEYBIND | |
| 17 | WeaponSwap | Weapon Swap | w | true | false | KEYBIND | |
| 18 | SkillSwap | Active Skill Swap | TAB | true | false | KEYBIND | |
| 19 | QuestMenu | Quest Menu | q | true | false | KEYBIND | |
| 20 | ShowItems | Show Items | ALT | true | false | KEYBIND | |
| 21 | CycleSkillDown | Cycle Skills Down | , | true | false | KEYBIND | |
| 22 | CycleSkillUp | Cycle Skills Up | . | true | false | KEYBIND | |
| 23 | AutomapZoomOut | Automap Zoom Out | [ | true | false | KEYBIND | |
| 24 | AutomapZoomIn | Automap Zoom In | ] | true | false | KEYBIND | |
| 25 | CameraZoomOut | Camera Zoom Out | MWDOWN | true | false | KEYBIND | |
| 26 | CameraZoomIn | Camera Zoom In | MWUP | true | false | KEYBIND | |
| 27 | ResetCamera | Reset Camera | MMB | true | false | KEYBIND | |
| 28 | F1 | Function 1 | F1 | true | false | KEYBIND | |
| 29 | F2 | Function 2 | F2 | true | false | KEYBIND | |
| 30 | F3 | Function 3 | F3 | true | false | KEYBIND | |
| 31 | F4 | Function 4 | F4 | true | false | KEYBIND | |
| 32 | F5 | Function 5 | F5 | true | false | KEYBIND | |
| 33 | F6 | Function 6 | F6 | true | false | KEYBIND | |
| 34 | F7 | Function 7 | F7 | true | false | KEYBIND | |
| 35 | F8 | Function 8 | F8 | true | false | KEYBIND | |
| 36 | F9 | Function 9 | F9 | true | false | KEYBIND | |
| 37 | F10 | Function 10 | F10 | true | false | KEYBIND | |
| 38 | F11 | Function 11 | F11 | true | false | KEYBIND | |
| 39 | F12 | Function 12 | F12 | true | false | KEYBIND | |
| 40 | QUICKSAVE | Quick Save | F11 | CTRL | true | false | KEYBIND |
| 41 | QK1 | Quickslot 1 | 1 | true | false | QUICKKEYS | |
| 42 | QK2 | Quickslot 2 | 2 | true | false | QUICKKEYS | |
| 43 | QK3 | Quickslot 3 | 3 | true | false | QUICKKEYS | |
| 44 | QK4 | Quickslot 4 | 4 | true | false | QUICKKEYS | |
| 45 | QK5 | Quickslot 5 | 5 | true | false | QUICKKEYS | |
| 46 | QK6 | Quickslot 6 | 6 | true | false | QUICKKEYS | |
| 47 | QK7 | Quickslot 7 | 7 | true | false | QUICKKEYS | |
| 48 | QK8 | Quickslot 8 | 8 | true | false | QUICKKEYS | |
| 49 | QK9 | Quickslot 9 | 9 | true | false | QUICKKEYS | |
| 50 | QK0 | Quickslot 0 | 0 | true | false | QUICKKEYS | |
| 51 | BESTPOTION | Best Health Potion | z | false | false | QUICKKEYS | |
| 52 | BESTMANA | Best Mana Potion | x | false | false | QUICKKEYS | |
| 53 | BESTPETPOTION | Best Pet Health Potion | z | SHIFT | false | false | QUICKKEYS |
| 54 | BESTPETMANA | Best Pet Mana Potion | x | SHIFT | false | false | QUICKKEYS |
| 55 | Console | Console | INSERT | false | true | HELPERBINDINGS | |
| 56 | FPS | FPS Details | F | CTRL | false | true | HELPERBINDINGS |
| 57 | SoundDebug | Sound Debug | S | CTRL | false | true | HELPERBINDINGS |
| 58 | PlayerEffectData | Player Effect Data | E | CTRL | false | true | HELPERBINDINGS |
| 59 | PlayerQuestData | Player Quest Data | Q | CTRL | false | true | HELPERBINDINGS |
| 60 | CombatLog | CombatLog | c | CTRL | false | true | HELPERBINDINGS |
| 61 | TimeOfDay | Time Of Day | T | CTRL | false | true | HELPERBINDINGS |
| 62 | LevelData | Level Data | L | CTRL | false | true | HELPERBINDINGS |
| 63 | PeerData | Peer Data | P | CTRL | false | true | HELPERBINDINGS |
| 64 | SteamStatsData | Steam Stats Data | S | CTRL | false | true | HELPERBINDINGS |
4.3 数据里本来就有的冲突
同一个 (KEY, MODIFIERKEY) 出现多次是原版数据的性质,不是解析错误:
| 键 | 绑定 | 性质 |
|---|---|---|
m / SHIFT+m / CTRL+m | Automap / AutomapReset / AutomapVisible | 靠 MODIFIERKEY 区分,正常 |
z / SHIFT+z,x / SHIFT+x | BESTPOTION / BESTPETPOTION,BESTMANA / BESTPETMANA | 同上,正常 |
c / CTRL+c | StatMenu / CombatLog | 同上,正常 |
q / CTRL+Q | QuestMenu / PlayerQuestData | 同上,正常 |
CTRL+S(两次) | SoundDebug / SteamStatsData | 真冲突。两条都在 HELPERBINDINGS.DAT,都 EXCLUSIVE:false、都 HIDDEN:true。谁赢没查 |
CTRL+S 是把单字符大小写折叠后查出来的唯一一处同键同修饰键重复。引擎侧怎么裁决、EXCLUSIVE 在这种场合起不起作用,没逆。
4.4 w / s / a:WASD 的三条路障
表里第 17、12、7 行:
| 键 | 原版绑定 | HIDDEN |
|---|---|---|
w | WeaponSwap(换武器) | false |
s | SkillMenu(技能面板) | false |
a | AutoTarget(自动索敌) | 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.5 | 0x216FDF8,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。