分析对象:
Torchlight2.exe(v1.25.9.5,基址0x400000,加载函数sub_604D20)
核心定位:解析规则模板如何将[CHUNK_RANDOM].TYPE字符串映射至内存CHUNKTYPE、物理目录及具体变体.LAYOUT。
0. 结论速览
[CHUNK_RANDOM] 中的 TYPE 并非物理磁盘目录名,底层亦不存在前缀模糊匹配或去除 _LANDMARK 等特殊剥离规则。它精确指向同一份模板内部声明的 [CHUNKTYPE].NAME。
引擎底层的完整装配链路如下:
CHUNK_RANDOM.TYPE
│ 精确字符串比较
▼
CHUNKTYPE.NAME (CChunkType + 0x44)
│
├─ FOLDER 缺省为 NAME
│ 最终目录 = 模板所在目录 + FOLDER + "/"
│
├─ 存在 INCLUSIVE_FILES 清单
│ 仅加载声明的 FILE + ".LAYOUT"
│
└─ 未声明 INCLUSIVE_FILES
枚举最终目录,排除 /MERGE/,按名排序
│
▼
CChunk 变体列表此前部分自动化检查工具采用「目录名精确比对 → 去除 _LANDMARK 后缀 → TYPE_ 前缀推导」的启发式算法,导致 170 处合法引用被误报为解析失败;逆向确认该逻辑并不符合引擎原生语法,引擎始终采用同模板内声明的精确全名匹配。
1. 静态汇编证据
目标为 Torchlight2.exe v1.25.9.5,IDA imagebase 0x00400000,加载函数为 sub_604D20。
| 地址 | 行为 |
|---|---|
0x606DD5..0x606E8C | 收集全部 CHUNKTYPE,读取 NAME |
0x606FEA..0x6070C2 | getString(FOLDER, NAME);模板目录与结果拼接,保证尾部斜杠 |
0x6073D9..0x6074D2 | 有 INCLUSIVE_FILES 时,逐项读取 FILE、补 .LAYOUT、存在才加入 |
0x60776B..0x6078B6 | 无清单时枚举目录;0x607819 排除 /MERGE/;随后排序 |
0x6079F1..0x607A7B | 创建 CChunk,保存所属类型和 .LAYOUT 路径 |
0x60836C..0x6083E3 | 读取 CHUNK_RANDOM.TYPE,从类型向量头线性精确比较 CChunkType+0x44 |
类型比较核心字节(0x6083B5,48 字节):
8B 46 30 3B 6E 38 73 03 8D 04 A8 8B 00 8D 8C 24
60 01 00 00 83 C0 44 51 50 FF 15 68 68 11 02 83 C4
08 84 C0 75 08 45 3B 6E 34 72 D4 EB 02 8B DD83 C0 44 把当前 CChunkType* 调整到 NAME 字段,随后调用宽字符串 operator==;失败就递增索引并继续线性扫描。因此,模板出现同名声明时,首个声明 胜出。
字段与对象布局
本轮确认的必要字段如下:
| 对象 | 偏移 | 语义 |
|---|---|---|
CChunkType | +0x18 | 拼好的最终文件夹路径 |
CChunkType | +0x34 | INCLUSIVE_FILES 成功加载的布局指针向量 |
CChunkType | +0x44 | NAME 宽字符串 |
CChunk | +0x08 | 所属类型/类型索引 |
CChunk | +0x14 | .LAYOUT 路径宽字符串 |
FOLDER 不存在时,CDataGroup::getString 的默认参数正是刚读出的 NAME。这个 细节解释了绝大多数普通模板为什么看起来像“TYPE 就是目录”:它们只是通过默认值 恰好同名;一旦模板显式写 FOLDER,目录启发式便会错。
全库重跑结果
命令:
python -B tools/m4_seam_check.py --json build/m4/seam_check.json固定 seed 1234 的结果:
| 指标 | 数值 |
|---|---|
| 模板 | 186 |
| 有预设、实际进入检查的模板 | 119 |
| 预设 | 286 |
CHUNK_RANDOM 引用 | 2,479 |
精确匹配 CHUNKTYPE.NAME | 2,477(99.9%) |
| 通过目录枚举得到可加载变体 | 2,009 |
通过 INCLUSIVE_FILES 得到可加载变体 | 316 |
| 成功摆下 | 2,325 |
| 匹配到类型但无可加载布局 | 152 |
| 模板未声明该类型 | 2 |
| 实际覆盖的不同瓦片 | 610 |
| 接触相邻对 | 3,015 |
| 真接缝洞 | 0 |
旧 170 个启发式遗漏中,有 16 个通过正确的 FOLDER/INCLUSIVE_FILES 依赖得到准确解析。其余未命中项也得以重新定性:其成因属于发行版原始资产缺失或模板可达性遗留,而非规则语法未明。
3. 剩余 154 处未决引用的成因分析
其中 152 个引用能够在同模板中定位到匹配的 CHUNKTYPE.NAME,但按原版加载规则无法映射到任何物理磁盘布局文件:
| 规则模板 | 未决引用数 |
|---|---|
ACT1/SNOW/RULES.TEMPLATE | 144 |
TEST/RULES.TEMPLATE | 4 |
ACT2_CAVES/MANAVENT_RULES.TEMPLATE | 1 |
DRAGON/CASTLERUINS_RULES.TEMPLATE | 1 |
MAINMENUS/MAINMENU_SPLASHRULES.TEMPLATE | 1 |
TEST/TESTROOM_RULES.TEMPLATE | 1 |
另有两项属于模板内缺失对应类型声明:
ACT3_Z2/NG_DWARFARMORY_RULES.TEMPLATE中的1X1SINGLE_ROOM_BOSSTESTROOM/TESTROOMRULES.TEMPLATE中的1X1SINGLE_ROOM_BOSS
以 ACT1/SNOW/RULES.TEMPLATE 为例:其声明的 25 个类型按原版规则均寻址至 ACT1/SNOW/<NAME>/ 路径,但在最终发行版资源中该路径仅存规则模板及其 BINDAT,同名瓦片实际散落于上级目录 ACT1/<NAME>/。原版 C++ 解析逻辑中并不存在自动回溯父级目录的回落分支;逆向工具不应为了强行消除缺失告警而引入未经证实的回退规则,应严格如实反映引擎解析行为。
4. 对重制与解析工具的工程约束
在实现规则解析器时,应严格遵循原版装配阶段:先建立全量 CHUNKTYPE 映射表,再解析并派发 [LAYOUT] 预设。针对每个 CHUNK_RANDOM.TYPE 执行与宽字符串等价的精确匹配;变体布局检索必须严格限制在该类型自身的 FOLDER 或 INCLUSIVE_FILES 范围内。严禁跨兄弟目录扫描、前缀模糊匹配或擅自剥离 _LANDMARK 等命名后缀。