NOTE

逆向方法与数据支撑:本文基于对 32 位客户端 Torchlight2.exe(ImageBase 0x400000,文内所有 sub_XXXXXX 与 0xXXXXXX 均为绝对虚拟地址)寻路模块 0x462F60–0x464400 的反编译体与汇编分析,并对 10 处关键常量与指令完成 PE 字节级核对(每步移动代价、结点大小、提前收敛阈值、迭代上限字段、吸附半径、哨兵、后继槽数)。该模块共 24 个函数,当前已逆向其中 8 个核心函数。配套数据涵盖官方 7 张地图 .mpp 走格的连通性普查,以及静态道具阻挡与走格对账分析。所有尚未完全闭环的边界均如实标注。

核心工程结论:原版寻路采用 8 邻域 A* 算法,但代价模型与启发函数具有特殊设计——正交与斜向每步代价恒定为 1.0,而启发函数为欧氏距离的平方。由于 $g$(单位:格)与 $h$(单位:格²)量纲不一致,启发函数不可采纳,算法行为偏向贪心最佳优先搜索,无法保证生成最短路径;怪物绕远与贴墙转向等表象系搜索算法特性导致。此外,“可行走站立”与“路径规划通行”为两套判定系统(前者单点无碰撞半径,后者具备净空约束,单格缝隙无法规划通过);TL2 客户端不基于 3D 碰撞体裁决通行,场景装饰物无实体碰撞,通行性完全由 .mpp 走格数据决定。


0. 核心结论速览 ​

  1. 模块与入口:寻路模块位于 0x462F60–0x464400,共 24 个函数(已分析 8 个核心函数)。移动逻辑 sub_5425A0 调用 sub_464010(unit+112 /*世界X*/, unit+120 /*世界Z*/, goalX, goalZ) 触发寻路。
  2. 8 邻域调用确认:sub_463940 中对 sub_4637B0 的 call 指令共有 8 处,整段 .text 对 sub_4637B0 的引用亦仅此 8 处。结点结构中分配了 8 个后继槽位(cmp ecx, 8 @ 0x463881),对应 8 邻域展开。
  3. ★★★ 每步代价恒 1.0,启发函数为距离平方:每步代价固定增加 1.0(fadd dword ptr [0x213C5DC],内存值为 float 1.0),斜向移动亦为 1.0;启发函数 $h = (dx)^2 + (dz)^2$(单位为格²)。由于启发不可采纳,原版无法保证最短路径。辅助证明:sub_463650 为向已闭合结点回传并更新更优 g 值的专用例程。
  4. 起终点 ±16 格吸附:目标吸附 sub_463270 在 [cx−16, cx+16] × [cz−16, cz+16](33×33 方格范围)内选取世界坐标欧氏距离最近的可走格,哨兵距离 9999.0;起点吸附 sub_4633F0 同样扫描 ±16 范围,但候选格额外要求周围具备净空。16 格对应 6.4 世界单位。
  5. A 内核结构*:结点通过 calloc(1, 0x40) 分配(64 字节);Open 表 ctx+12 与 Closed 表 ctx+16 均为单链表,sub_463220 按 f 值升序插入,主循环取表头;按索引查找结点采用单链表线性遍历($O(n)$)。
  6. 迭代上限与提前收敛退出:主循环存在迭代次数限制(if (i > *(DWORD*)(ctx+68)) break;),超出上限时以当前 h 最小结点作为局部路径返回,并清零完整路径标志(ctx+66 = 0)。若目标曾被吸附修正,只要当前最佳结点 $h < 2.0$(格²,约 1.41 格距离,常量地址 [0x2143F64])即提前终止搜索。
  7. ★★ 可行走判定与路径规划判定分离:CLevel::isWalkable(sub_5DB690)为单点无半径查表,判定单位能否在此处站立;寻路例程 sub_463940 具备净空要求(正交移动要求侧向有空格,斜向要求两个正交邻格均可走),判定能否规划路径通过。因此单格宽(0.4u)缝隙无法规划穿行,地图通道设计应至少预留 2 格宽。
  8. 双层格网与阻挡极性:ctx+48 为主格网(必查),ctx+52 受 ctx+65 门控(仅为 false 时检查)。阻挡判定条件为有符号字节 $> 0$;255 解释为 $-1$ 视为可行走,与 isWalkable(有符号 $\le 0$ 可走)保持一致。
  9. 地图连通分量分布:7 张官方测试地图走格并非单块整体,包含 3 至 67 个独立连通分量,主分量占比在 39.7% 至 92.1% 之间。寻路系统无“最大连通分量”概念,吸附格不可达即寻路失败。
  10. ★★★ 静态模型碰撞脱节(幽灵墙现象):7 张地图中 4,943 个标记为 OBSTACLE 的装饰道具中,有 639 个(约 13%)其坐标所在格子在 .mpp 中为可行走。TL2 原版不使用 3D 碰撞体裁剪移动,阻挡完全由 .mpp 走格决定,因此穿透部分装饰模型属于原版设计行为。
  11. 未闭环事项:净空规则总开关 ctx+72 的写入条件、双层格网的具体职责分工、迭代上限配置值以及是否存在视线平滑(string pulling)例程尚未定位。

1. 模块位置、入口与函数清单 ​

寻路模块是 .text 里一段连续的区间:0x462F60–0x464400,ALLFUNCS/_realfuncs.json 落在该区间的条目共 24 个。本文已分析 8 个核心例程,职责如下:

函数体积(字节)职责
sub_464010656世界坐标 → 格坐标、起终点吸附、派发搜索
sub_463270371目标吸附:±16 格方块内取世界欧氏最近的空格
sub_4633F0604起点吸附:同样 ±16,候选格另要求周围有净空
sub_463EA0360A* 主循环
sub_4639401367邻域展开,正好 8 次 call sub_4637B0
sub_4637B0387松弛一个邻居:g = parent.g + 1.0,h = 距离²,f = g + h
sub_46322065开表按 f 升序插入
sub_463650266把改小了的 g 回传给已闭合结点

其余未分析函数:sub_462F60 / sub_463030 / sub_4630A0 / sub_463130 / … / sub_4642A0 / sub_464360 等。部分高级机制(如平滑拉线)仍需结合剩余函数进一步排查。

入口:移动侧 sub_5425A0 调 sub_464010(unit+112, unit+120, goalX, goalZ),其中 +112 / +120 是单位结构体里的世界 X / Z。

搜索方向:sub_464010 调的是 sub_463EA0(goalX, goalZ, startX, startZ, 1)——根结点是起点、终止条件是走到终点;最后那个 1 让它把 parent 链就地反转,结果读出来是「起点 → 终点」。起终点落在同一格时不跑搜索,直接置 ctx+67 = 1(=「已经到了」)。

MOD 开发结论:寻路系统为独立硬编码算法,不解析单位 DAT 配置,无法通过单位属性修改搜索行为;地图通行性仅由对应关卡的 .mpp 走格数据决定。


2. 走格取值:格子换算、两层格网、阻挡极性 ​

2.1 世界坐标到格坐标 ​

cell = floor((world − origin) × (1.0 / cellSize))

寻路上下文(下文写作 ctx)里与格网相关的字段:

ctx 偏移含义
+32Z 方向格数
+36X 方向格数
+40cellSize(一格 = 0.4 世界单位)
+48第一层格网(永远查)
+52第二层格网(仅 ctx+65 为假时查)
+56origin X
+60origin Z

2.2 两层格网 ​

ctx+48 永远查;ctx+52 只在 ctx+65 为假时查,两层的判定完全同形。

WARNING

哪一层对应静态地形、哪一层对应动态阻挡占位,尚需结合动态写入点确认;ctx+65 置位源头未分析。

2.3 阻挡极性:有符号 byte > 0 ​

阻挡判定条件为有符号 byte > 0。这与 CLevel::isWalkable(sub_5DB690)的 setle(有符号 ≤ 0 视为可走)保持一致:数值 255 在两处均被解析为 −1(可行走)。

该判定与走格导出工具的解析规则 walk = (cells == 0) 相互印证——发行 .mpp 中的原始数据仅包含 {0, 1, 255},255 在装载后进入内存不会被视为阻挡。

MOD 开发结论:走格数据中可行走判定采用区间模型(有符号 ≤ 0)。数值 255(−1)通常代表地图边界外或未定义瓦片,引擎将其视为可行走。编写走格编辑工具时切忌将 255 误判为实心阻挡。


3. 起点与目标的吸附:±16 格 ​

3.1 目标吸附 sub_463270 ​

目标格越界或被挡时,扫 [cx−16, cx+16] × [cz−16, cz+16] 这个 33×33 方块,把每个空格换算回世界坐标求欧氏距离,取最小。哨兵是 9999.0;一个空格都没有就原样返回。命中时把 ctx+64 置 1(=「目标被挪过」)。

字节层:lea ebx, [esi − 0x10] @ 0x4632DD、lea eax, [esi + 0x10] @ 0x4632E4;哨兵 float 9999.0 在 0x214882C。

3.2 起点吸附 sub_4633F0 ​

同样的 ±16 方块、同样的欧氏最近,但候选格额外要求周围有净空(见 §5)。

3.3 吸附机制的影响 ​

玩家点击阻挡区域角色移动至边缘的现象,并非前端表现层插值,而是寻路规划层吸附逻辑的直接结果。

MOD 开发结论:

  • 16 格对应 6.4 世界单位。目标点距最近可行走格超过 16 格时寻路直接判定失败,单位不会尝试部分趋近。
  • 吸附计算取世界坐标欧氏最近距离,非简单的曼哈顿或切比雪夫逐圈扩展,斜向边界处判定更为严格。
  • 目标若触发吸附(ctx+64 置 1),将激活 §4.5 中的提前终止条件(最佳结点 $h < 2.0$ 即退出),可能导致单位在距离吸附目标约 1.41 格处停止移动。

4. A* 内核:结点、开闭表、代价、启发 ​

4.1 结点:64 字节 ​

主循环用 calloc(1, 0x40) 分配结点(push 0x40 @ 0x463ED7),布局按 DWORD 下标:

下标字节偏移含义
[0]+0f
[1]+4h
[2]+8g
[3]+12x
[4]+16z
[5]+20index = x + z × width
[6]+24parent
[7..14]+28..+56最多 8 个后继回指针(cmp ecx, 8 @ 0x463881)
[15]+60链表 next

4.2 开表与闭表:单链表 + 线性扫 ​

开表 ctx+12、闭表 ctx+16,都是单链表(next 在结点 +60)。sub_463220(65 字节)按 f 升序插入,主循环每轮摘表头 ⇒ 等价于优先队列。

但按 index 找结点是线性扫链(while (node[5] != idx)),O(n)。这正是它需要迭代上限的原因。

4.3 ★★★ 代价恒 1.0,启发是距离的平方 ​

sub_4637B0 松弛一个邻居:

g = parent.g + 1.0        // fadd dword ptr [0x213C5DC],该四字节现读 = float 1.0
h = (dx)² + (dz)²         // 单位是格²
f = g + h

两项关键特征:

  1. 每步代价恒定 1.0:正交与斜向移动代价相同,度量标准为切比雪夫度量,而非欧几里得度量。
  2. 启发函数为欧氏距离平方:$g$ 的度量单位为格,而 $h$ 的单位为格²。量纲不匹配导致 $h$ 远距离时呈平方级放大,启发函数不可采纳(Admissible 条件失效)。算法整体偏向贪心最佳优先搜索,无法保证生成最短路径。

代码佐证:sub_463650(266 字节)为专门负责向已闭合结点回传更优 g 值的逻辑。在经典 A* 中若启发函数严格可采纳,闭合结点的 g 值即为最优,无需回传;当启发不可采纳导致闭表结点可能被更优路径重新松弛时,方需此逻辑进行修正。

MOD 开发结论:怪物绕行、贴墙转弯或舍近求远等寻路表现,由底层启发函数的数学特性决定,并非 AI 决策层逻辑缺陷或碰撞半径配置错误。

4.4 迭代上限与部分路径 ​

if (i > *(DWORD*)(ctx+68)) break;      // cmp ebp, [esi+0x44] @ 0x463F8A

迭代计数超过上限后,搜索直接将 ctx+8(历史 h 最小结点)作为局部终点返回,并将 ctx+66 设为 0(标记非完整路径)。

WARNING

迭代上限字段 ctx+68 的配置初值尚未通过动态调试确认。

MOD 开发结论:超大开阔场景中超远距离点击可能导致单位中途停步,系触发迭代步数上限所返回的截断路径。

4.5 提前终止退出:h < 2.0 ​

当目标曾触发吸附(ctx+64 置位)时,若当前搜索最优结点的启发值 $h < 2.0$(格²,即实际距离小于约 1.41 格),寻路立即提前终止并退出,同时标记 ctx+66 = 0。浮点常量 2.0 读取自 [0x2143F64](fld dword ptr [0x2143F64] @ 0x463F76)。

注意触发前提:该逻辑仅在目标曾被吸附修正时生效。若目标初始即落在可走格上,搜索将严格推进至终点。


5. 8 邻域展开与净空规则分析 ​

5.1 8 邻域调用点静态核对 ​

sub_463940(1367 字节)负责邻域展开,汇编中调用 sub_4637B0 的 call 指令共有 8 处:

  • sub_463940 内部包含 8 处 call sub_4637B0;
  • 整个 .text 代码段中对 sub_4637B0 的交叉引用亦严格仅有 8 处;
  • _realfuncs.json 统计显示 sub_4637B0 仅被单个函数调用。

结点数据结构中偏移 [7..14] 预留了 8 个后继结点槽位(cmp ecx, 8 @ 0x463881),在结构层面印证了 8 邻域模型。

NOTE

寻路邻域展开方式经汇编调用计数与结点槽位验证,确认为 8 邻域展开模型,而非 4 邻域。

5.2 净空与拐角约束 ​

情形条件约束
斜向移动两个相邻的正交格均必须可行走(拐角规则,禁止贴墙切角移动)
正交移动目标格所在行必须包含至少一个可通行的横向相邻格
起点吸附(sub_4633F0)候选落脚格周围必须具备净空

正交移动约束表明:单格宽度(0.4u)的狭缝无法被规划算法选为通路。即便狭缝内各格子 isWalkable 均返回可通行,由于缺乏侧向净空格,寻路算法依然会拒绝规划穿行。

CAUTION

上述净空判定均以 ctx+72 为真作为前置开关。ctx+72 的赋值逻辑与开启条件当前尚未逆向闭环。

5.3 ★★ 可行走判定与路径规划判定的分离机制 ​

两者在引擎中具有明确分工:

判定范畴对应函数判定准则
站立可行走性CLevel::isWalkable(sub_5DB690)单点查表,有符号 ≤ 0 即可,不考虑碰撞体积半径
路径规划通行性寻路例程 sub_463940 / sub_4633F0包含净空与拐角约束(§5.2),具备隐式宽度要求

二者互不冲突:“单位可以被放置于单格宽缝隙中站立”与“寻路规划器拒绝将单位规划穿过单格宽缝隙”同时成立。

MOD 开发结论:

  • 场景通路设计须保持至少 2 格宽度(0.8u)。单格宽度的通道即使在 .mpp 中刷为可走,寻路算法亦无法生成穿越路径。
  • 摆放装饰物与绘制走格时,通道余量应依据规划净空规则评估,而非仅以单点可站立为准。

6. 字节锚点表 ​

下表列出本文所有关键结论的 PE 字节级锚点。每一条都可以在 Torchlight2.exe 里按虚地址直接核对。

锚点虚地址字节长度说明
step_cost_fadd0x4637BFD8 05 DC C5 13 026sub_4637B0:fadd dword ptr [0x213C5DC]——每步代价的加数
step_cost_value0x213C5DC00 00 80 3F4上一条指向的四字节 = float 1.0
node_is_64_bytes0x463ED76A 402sub_463EA0:push 0x40——calloc 的结点大小
early_accept_fld0x463F76D9 05 64 3F 14 026sub_463EA0:fld dword ptr [0x2143F64]
early_accept_value0x2143F6400 00 00 404上一条指向的四字节 = float 2.0(单位是格²)
iteration_cap_field0x463F8A3B 6E 443sub_463EA0:cmp ebp, [esi+0x44]——0x44 = 68 = 迭代上限字段
snap_radius_minus_160x4632DD8D 5E F03sub_463270:lea ebx, [esi − 0x10]
snap_radius_plus_160x4632E48D 46 103sub_463270:lea eax, [esi + 0x10]
snap_sentinel_value0x214882C00 3C 1C 464sub_463270 的哨兵 = float 9999.0
eight_child_slots0x46388183 F9 083sub_4637B0:cmp ecx, 8——结点最多 8 个后继

7. 发行地图走格的连通分量普查 ​

用 tools/f2_walkgrid_reach.py 离线读七张验收图导出的走格(四邻域洪泛),分量阈值 200 格(≥ 200 算「主要分量」,否则算碎屑)。「最近可走格」一列以复刻实现侧的合成出生点 (132.0, 44.0) 为基准。

场景格网可走格分量数主要分量(格)主分量占比碎屑(块 / 格)最近可走格落在主分量上?
act1_town301 × 35020,003916,572 / 3,16682.8%7 / 265是
clockwork_boss401 × 22618,310315,715 / 2,59485.8%1 / 1否(落在 2,594)
echopass_jd601 × 105086,6112934,413 / 28,058 / 13,465 / 8,322 / 880 / 555 / 43039.7%22 / 488否(落在 28,058)
echopass_jt650 × 90080,4962956,734 / 13,467 / 8,356 / 898 / 42970.5%24 / 612是
echopass_lm925 × 90087,9412364,156 / 13,465 / 8,322 / 430 / 411 / 358 / 28173.0%16 / 518是
echopass_pb625 × 112599,8342853,709 / 23,451 / 13,465 / 8,322 / 43053.8%23 / 457否(落在 129 格的碎屑上)
sundered_zone1328 × 1352506,88667466,800 / 37,023 / 2,148 / 33092.1%63 / 585是

七张里 3 张的「最近可走格」不在主分量上。echopass_pb 最极端:主分量 53,709 格,最近可走格却落在一块 129 格(0.1%)的碎屑上。

NOTE

4 张 Echo Pass 地图变体中重复出现相同的 13,465 / 8,322 格连通区域,系共用地形瓦片导致的真实阻隔(如水体/悬崖),而非数据导出异常。普查工具仅统计拓扑分布,不裁决设计合理性。

MOD 开发结论:

  • 引擎内部不存在“最大连通分量”的概念。目标吸附仅在 ±16 格范围内寻找欧氏距离最近的可行空格,不预先验证该空格与起点是否处于同一连通块;若不可达则寻路直接失败。
  • 地图全局“可行走”不代表“任意两点可互相到达”。拼装地图中被水体、悬崖物理阻隔划分的孤岛属于正常地形;但若将出生点、传送门或关键 NPC 放置在孤立碎屑连通分量中,将导致逻辑死锁。

8. 场景物理碰撞与走格阻挡解耦分析(幽灵墙现象) ​

8.1 现象复现与微观抖动 ​

在 echopass_jd(seed 1234)场景测试中,移动状态机出现持续卡顿:无法移动至目标点,解卡逻辑持续触发(849 次),坐标在 $x = 45.3$ 与 $45.7$ 之间往复振荡,振幅严格等于单次解卡步长(0.4u)。

8.2 阻挡源排查定位链路 ​

  1. 启用碰撞体检测,获取产生阻挡接触的具体碰撞体名称:col_Room_Piece0_clone723428_85,法线方向为 (0.96, 0.00, 0.29),呈现朝向 +X 的竖直阻挡面。
  2. 前方每 0.4u 发射垂直射线检测地面高度剖面:0.00 / 0.00 / 2.00 / 1.72 / 0.79 / 0.00 …,证实前方存在约 2.0u 的高度突变,台阶攀爬判定正确拒绝(最大攀爬高度限制为 0.6u)。
  3. 将走格通行性与物理几何模型映射至同一网格比对:走格整行全部显示为可行走,而几何模型上呈现 1.1–4.0u 的隆起带。
  4. 追溯场景模型资源节点:对应 Mesh 路径为 props/z1tundra_props/rock_06.res(独立石头模型)。

8.3 通行阻挡裁决机制定性 ​

早期移植管线根据模型资源命名包含 rock 将其归入 OBSTACLE_KEYS 并挂载了实心 3D 碰撞体。然而发行 .mpp 走格数据中该位置标记为可行走。

原版走格具有唯一裁决权:Torchlight II 客户端在角色常规通行中不依赖 3D Mesh 物理碰撞体,能否通行完全由 CLevel::isWalkable(sub_5DB690)单点查表决定。场景装饰物(如石头、路障模型)在原版中不具备实体碰撞阻挡效果,角色穿透模型通过属于符合原版设计的行为。

8.4 官方场景资源普查统计 ​

通过工具扫描官方场景瓦片,比对模型分类与底层走格的可通行性:

场景节点数OBSTACLEGROUND不建碰撞幽灵墙(模型标记阻挡但走格可通行)占 OBSTACLE 比
act1_town1,8001231161,5613629.3%
clockwork_boss7061696347442.4%
echopass_jd4,1843274453,4127522.9%
echopass_jt5,5984704424,68611524.5%
echopass_lm4,5242404573,8275322.1%
echopass_pb6,4735145305,42911221.8%
sundered_zone21,0123,1005,02412,8882447.9%
合计—4,943——63913%

echopass_jd 典型样例如下:

节点Mesh 路径XZ 坐标
Room_Piece0_clone12282319_12props/Z1Tundra_props/rock_05.res(38.2, 68.9)
Room_Piece0_clone12093446_34props/z1tundra_props/rock_06.res(37.9, 70.6)
Room_Piece0_clone12282230_61props/Z1Tundra_props/rock_05.res(54.9, 67.6)
Room_Piece0_clone12227765_65props/z1tundra_props/rock_06.res(75.9, 25.7)
Room_Piece3_clone12281951_75props/Z1Tundra_props/rock_01.res(69.7, 48.8)

WARNING

统计数值 639 属于保守下界(仅根据节点原点坐标落点判定,未叠加模型包围盒占地面积)。由于走格为 2D 投影,局部桥梁分层区域可能存在投影重叠。

8.5 调试过程中的常见误区与工具校验陷阱 ​

  1. 碰撞射线误判落脚面:排查初期将模型高度抬升误认为桥面落脚点,反复调整高度解卡逻辑无果。后经核查发现测试射线未配置 collision_mask,射线击中 OBSTACLE 层而非 GROUND 真实可行走地形。
  2. 正则边界导致普查漏检:早期普查脚本对瓦片节点末尾 groups=["tl2_room_piece"] 中的闭合括号解析错误,导致含组属性的房间瓦片被全部过滤。后引入端到端基准对照机制(以实测卡顿节点作为强制测试样本),消除工具解析偏差。

8.6 MOD 制作与关卡设计准则 ​

  • ★★★ 装饰道具不产生通行阻挡:通行性完全由 .mpp 走格数据决定。单纯摆放石头、木箱等装饰模型无法阻挡角色;若需形成阻挡,必须在走格数据中将对应格子刷为阻挡。
  • ★ 避免空气墙事故:若在 .mpp 中刷为阻挡但视觉上无任何模型覆盖,将导致玩家遭遇“空气墙”。
  • ★ 重制与移植建议:若基于资源名称规则自动生成 3D 碰撞体,必须与 .mpp 走格数据进行双向对账校验,防止生成阻挡原本可行走的幽灵墙。

9. 移植与复刻实现口径对照 ​

本节对照重制与移植开发中的算法参数与行为差异:

机制项原版客户端引擎移植参考实现(Godot AStarGrid2D 等)行为差异与影响对齐建议
邻域拓扑8 邻域展开8 邻域展开一致—
拐角规则斜向移动要求两个正交邻格均可行走启用斜向阻挡检测一致—
目标吸附半径16 格(方块区域取欧氏最近)16 格(切比雪夫由内向外逐圈扩散)斜角方向存在 1 格差异对齐欧氏距离扫描公式
起终点重合跳过搜索直接成功跳过搜索直接成功一致—
代价与启发模型每步代价 1.0 + 欧氏距离平方启发(偏贪心)OCTILE 启发 + $\sqrt{2}$ 斜向代价(最优路径)移植版路径通常比原版更短更直如需完全复刻原版走位手感,需重载自定义启发函数
正交净空约束目标行需具备横向可行走邻格无额外净空约束移植版允许穿行 1 格宽狭缝引入净空后处理或自定义展开准则
迭代上限存在上限(超出返回局部最优路径)通常无内置上限极端远距离可能引起搜索帧耗时波动结合连通分量预检提前拦截不可达目标
双层格网ctx+52 动态层(受门控控制)单层静态格网动态单位阻挡处理差异待明确动态格网写入机制后补齐
视线平滑(string pulling)未完全确定(当前分析函数未见拉线逻辑)包含拐点平滑过滤移植版折线拐点较少当前逆向范围尚未覆盖,不代表原版完全缺失
最大连通分量无全局概念(局部吸附失败即退出)生成期约束出生点落在最大分量保证动态出生点全局可达原版关卡出生点为静态指定,无需动态计算

NOTE

吸附半径参数从 12 调整至 16 系规范对齐;由于实测样本中目标离最近可行走格通常不超过 12 格,该调整对小范围测试集的命中率无显著影响。

幽灵墙临时工程处理方案:在物理层发生卡顿时,临时从碰撞检测掩码中剔除装饰碰撞层,待移动指定位移或超时后恢复;关卡生成期根据走格数据过滤重合碰撞体方为根本解决方案。

CAUTION

寻路模块与原版的等价性验证最终须依赖原版运行时的逐帧轨迹(Trace Oracle)进行严格判定,单元测试通过仅表明算法自洽。


10. 未闭环事项清单 ​

待验证项当前状态
ctx+72(净空/拐角规则总开关)的写入调用点与激活条件尚未完全定位,当前净空结论均基于该开关于运行时为真
双层格网 ctx+48 / ctx+52 的具体职责分工及 ctx+65 触发条件尚未闭环,需排查动态单位占位逻辑
迭代步数上限 ctx+68 的默认初始化数值需结合动态调试读取
引擎是否存在视线平滑(string pulling)机制模块 24 个函数中尚有 16 个未逐行逆向,平滑例程尚未排除
寻路模块未读函数功能梳理sub_462F60、sub_463030、sub_4630A0、sub_463130 等例程待分析
装饰物碰撞与走格重叠的生成期清洗规则需在瓦片导出阶段增加走格比对过滤
走格不可走但物理层缺失碰撞体的反向普查尚未统计

附录 A:核心符号与虚拟地址表(Torchlight2.exe,ImageBase 0x400000) ​

功能描述虚拟地址 / 符号特征
寻路模块连续区间0x462F60–0x464400(共 24 个例程)
移动触发调用入口sub_5425A0 → sub_464010(unit+112, unit+120, goalX, goalZ)
坐标转换与搜索派发sub_464010(656 字节)
目标点吸附(±16 格,欧氏最近)sub_463270(371 字节;lea @ 0x4632DD / 0x4632E4;哨兵 0x214882C)
起点吸附(±16 格 + 净空约束)sub_4633F0(604 字节)
A* 主搜索循环sub_463EA0(360 字节;push 0x40 @ 0x463ED7;fld [0x2143F64] @ 0x463F76;cmp ebp, [esi+0x44] @ 0x463F8A)
邻域展开(8 处调用)sub_463940(1367 字节)
邻居结点松弛(g + 1.0,h = 距离²)sub_4637B0(387 字节;fadd [0x213C5DC] @ 0x4637BF;cmp ecx, 8 @ 0x463881)
Open 表按 f 升序插入sub_463220(65 字节)
已闭合结点 g 值回传更新sub_463650(266 字节)
每步代价浮点常量 1.00x213C5DC
提前终止阈值浮点常量 2.00x2143F64
吸附距离初值哨兵 9999.00x214882C
基础可行走判定 CLevel::isWalkablesub_5DB690(单点查表,setle)

附录 B:支撑源材料与逆向结论映射 ​

  • 逆向验证项:
    • 走格寻路:8 邻域展开模型、切比雪夫移动代价与平方启发函数;可行走与路径规划判定分离;
    • 碰撞解耦:静态装饰道具碰撞与 .mpp 走格阻挡解耦机制、场景幽灵墙普查分析。
  • 数据结构与静态配置清单:walkgrid_pathfinding.json、phantom_walls.json。
  • 分析工具:tools/f2_walkgrid.py(走格导出)、tools/f2_walkgrid_reach.py(连通分量普查)、tools/f2_phantom_walls.py(装饰物与走格重合普查)。