NOTE
逆向方法与数据支撑:本文结论来自三类证据的交叉:① 32 位客户端 Torchlight2.exe(ImageBase 0x400000,文内 sub_XXXXXX 均为绝对虚地址)中加载器与任务状态函数的反编译体;② 原版全量数据资产的普查——MEDIA/RESURRECT/ 3 份 DAT、MEDIA/QUESTS/ 184 份 DAT、MEDIA/INVENTORY/ 37 份槽位 DAT 与 MEDIA/INVENTORY/CONTAINERS/ 30 份容器 DAT,以及对全 MEDIA 16,084 份 DAT 的键名扫描;③ 对 exe 数据段的 UTF-16LE 宽字符串检索。全部结论已收敛至真值账本(Ledger C-0175 / C-0176 / C-0177,状态均为 guarded、级别 must-agree)。源材料中标为“没逆 / 未定 / 移植选择”的部分,本文一律如实标注,不作补全。
核心工程结论:三个子系统都是数据驱动的——在哪复活、复活扣多少、任务对白在完成前后各说什么、背包与装备栏各有几格,全部写在 DAT 里而不在代码里。但有三处极容易“按键名想当然”:XP_PENALTY 这个键存在,发行数据里却全是 0;ICONABOVEHEAD 这个键存在,发行数据里却全是 false;技能栏容器 SPELLSLOTS 只有 4 格,而不是界面上那排 10 个数字键。
0. 核心结论速览
- 原版死亡只扣金币,不扣经验、不扣声望。
MEDIA/RESURRECT/三条选项的XP_PENALTY与FAME_PENALTY全部为 0。加载器确实读这两个键(字段偏移 +96 / +100),但发行数据没给它们任何非零值。按键名去实现“死亡掉经验”,等于实现了一条原版没有的规则。 - 复活是发行数据里的三选一:
INPLACE(INDEX0 /WHERECURRENT/GOLD_PENALTY30 /ALLOW_IN_NON_PORTAL_LEVELS:false/DIFFICULTIESBIN:3)、INTOWN(1 /TOWN/ 0)、LEVELENTRANCE(2 /LEVEL_START/ 10)。一个 DAT 文件一条选项,INDEX是面板顺序,WEIGHT三条都是 1。 - 复活选项的加载器是
sub_651DF0,它不是“难度 DAT 加载器”。早期语料把它标成了难度加载,判据很简单:它读WHERE与ALLOW_IN_NON_PORTAL_LEVELS,而难度 DAT 里没有这两个键。三个*_PENALTY键两边都有,大概就是标错的原因。 GOLD_PENALTY是百分比还是定额,没逆出来。加载器只把它当整数存进 +92;读取 +92 并真正扣钱的消费函数尚未定位。- 任务的两态就在同一个对话块里:
DIALOG(未完成)与DIALOG_COMPLETE(已完成)是两条独立文本;[DUNGEONS_WHEN_COMPLETE]/[DUNGEONS_WHEN_NOT_COMPLETE]是同一个两态在关卡侧的投影。编辑器导出只有EditorSetEditorActiveQuests/EditorSetEditorCompletedQuests两个动作——两态坐实,没有第三态。 - 发行任务普查常见六种
[DIALOG]子块:INTRO/DETAILS/COMPLETE/RETURN/HUDDETAILS/PASSIVE。后续代码逆向还确认引擎支持第七种MOREDETAILS;各自触发时机见任务完成、目标计数与对话状态机。 ICONABOVEHEAD在全 MEDIA 16,084 份 DAT 里出现 445 处,取值全部是false,另有 424 个对话块根本没写它。而它的 UTF-16LE 宽字符串在 exe 里命中 1 处——引擎确实读它,不是死键。于是图标“亮”那一态只能来自缺键时的默认值,而那个默认值没逆。想靠这个键点亮头顶感叹号,行不通。- 任务完成条件已有后续补完。通用谓词、DEFEAT 计数与存档、
[DEFEATCOUNT]替换、对话状态机和GLOBAL_TRILLBOT特判均已钉住;[REWARD]中GOLDMINPCT等百分比口径仍未查。见任务完成、目标计数与对话状态机。 - 玩家的三个面板是三个真容器:
MAIN BAG24 格(单槽BAG)、BODY13 槽、SPELLSLOTS4 格(单槽SPELL)。格数、槽位名、顺序全是数据,不是界面布局的选择。 - 技能栏是 4 格,不是 10。原版界面上那排 1–9/0 是技能快捷键,与
SPELLSLOTS容器不是一回事。写成 10 格是把界面印象当成了数据。 BODY的第 13 槽是POINTER(拿在手上那件)。它在容器定义里就属于 BODY,不是复刻时加的。
1. 复活系统(MEDIA/RESURRECT/)
1.1 三条 authored 选项全表
MEDIA/RESURRECT/ 下一个 DAT 文件对应一条复活选项。三份文件的全部有效键值如下:
| 源文件 | NAME | TITLE | WHERE | GOLD_PENALTY | XP_PENALTY | FAME_PENALTY | INDEX | WEIGHT | DIFFICULTIESBIN | ALLOW_IN_NON_PORTAL_LEVELS |
|---|---|---|---|---|---|---|---|---|---|---|
RES_IN_PLACE.DAT | INPLACE | Resurrect | CURRENT | 30 | 0 | 0 | 0 | 1 | 3 | false |
RES_IN_TOWN.DAT | INTOWN | Town | TOWN | 0 | 0 | 0 | 1 | 1 | (未写) | (未写) |
RES_IN_DUNGEON.DAT | LEVELENTRANCE | Entrance | LEVEL_START | 10 | 0 | 0 | 2 | 1 | (未写) | (未写) |
三条的 DESCRIPTION 原文:
INPLACE Resurrect here, but lose [GOLD] Gold.
INTOWN Resurrect in town and lose nothing.
LEVELENTRANCE Resurrect at the entrance to this area, but lose [GOLD] Gold.三条的 IMAGE 都是 NONE;REQUIRES_ITEM / TAKE_ITEM / EFFECT / BLOCKING_EFFECT 四个键在三份文件里一条都没用(全为空)。DESCRIPTION 里的 [GOLD] 是占位符,其替换逻辑未查。
三点观察:
XP_PENALTY与FAME_PENALTY三条全为 0。这两个键加载器是读的(见 §1.2),所以它们不是死键——只是 Runic 从没给过非零值。原版“死亡惩罚”的全部内容就是 30 或 10 金币。INDEX0 / 1 / 2 对应面板上的顺序;WEIGHT三条都是 1,它参与什么计算没查。DIFFICULTIESBIN:3与ALLOW_IN_NON_PORTAL_LEVELS:false只出现在INPLACE这一条上。两个键的运行时语义都没逆,本文只报数据。
1.2 加载器 sub_651DF0:键表与字段偏移
sub_651DF0 读取一份复活 DAT 并填充一个对象。它读的键共 16 个:
ALLOW_IN_NON_PORTAL_LEVELS NAME TITLE DESCRIPTION
GOLD_PENALTY XP_PENALTY FAME_PENALTY INDEX
REQUIRES_ITEM TAKE_ITEM EFFECT BLOCKING_EFFECT
IMAGE WEIGHT DIFFICULTIESBIN WHERE从反编译体读出的字段偏移(对象基址起,字节):
| DAT 键名 | 字段偏移 |
|---|---|
NAME | +8 |
GOLD_PENALTY | +92 |
XP_PENALTY | +96 |
FAME_PENALTY | +100 |
WHERE | +104 |
WEIGHT | +136 |
REQUIRES_ITEM | +144 |
TAKE_ITEM | +152 |
ALLOW_IN_NON_PORTAL_LEVELS | +160 |
DIFFICULTIESBIN | +164 |
INDEX | +212 |
TITLE / DESCRIPTION / EFFECT / BLOCKING_EFFECT / IMAGE | — |
加载器用到的三个取值例程是引擎通用的 DAT 读法:sub_677F00 取字符串、sub_677F30 取 bool、sub_677F80 取 int。
NOTE
表中“—”表示源逆向材料未记录该键的专属偏移,并非该键不存在。留下偏移表的目的只有一个:下次去找 +92 的消费端时不必再逆一遍加载器。
1.3 墓碑:语料曾把 sub_651DF0 标成“难度 DAT 加载”
早期的全函数语料(ALLFUNCS/_FINDINGS.md)把 sub_651DF0 标成了难度 DAT 加载器。这个标签是错的,判据很简单:
- 它读
WHERE与ALLOW_IN_NON_PORTAL_LEVELS——难度 DAT 里没有这两个键; GOLD_PENALTY/XP_PENALTY/FAME_PENALTY三个键在难度 DAT 与复活 DAT 里都有,大概就是因此被标错的。
这个错标签的代价不在标签本身,而在它把人往哪引:按“难度加载”去找 GOLD_PENALTY 的消费端,会一路走到难度系统去,而那里没有复活扣款的代码。把它记成墓碑,是为了让后来者不再走这条弯路。
1.4 没逆出来的部分
| 项 | 现状 |
|---|---|
GOLD_PENALTY 是百分比还是定额 | 未定。加载器把它当整数存进 +92;读取 +92 并施加扣款的消费函数尚未定位。 |
| 复活后回多少 HP | 三份 DAT 里没有这个键,加载器也不读 ⇒ 这是引擎行为,没逆。 |
WHERE 除 CURRENT / TOWN / LEVEL_START 之外的取值 | 引擎认不认,没查。 |
ALLOW_IN_NON_PORTAL_LEVELS 的判定 | 数据里 INPLACE 写了 false,但“本关是不是 portal level”的判定逻辑没逆。 |
DIFFICULTIESBIN | 只在 INPLACE 上出现,值为 3;语义没逆。 |
EFFECT / BLOCKING_EFFECT / REQUIRES_ITEM / TAKE_ITEM | 发行数据里一条都没用,语义没查。 |
WEIGHT | 三条都是 1,参与什么计算没查。 |
复刻侧的处理(移植选择,不是原版事实):复活动作放在模拟核心里,不掷随机数(复活是“把状态摆回去”,多掷一次会让联机两端随机流错位),HP 回满并在事件行标注 port choice;三份代价一份都不施加(模拟核心里没有金币 / 经验 / 声望);ALLOW_IN_NON_PORTAL_LEVELS 与 DIFFICULTIESBIN 不生效。实测同图同种子、不开自动复活时角色死在场上;开了之后死 5 次、每次原地复活、最后打完三只——INPLACE 标 30 金币代价而 TOWN / LEVEL_START 更安全,与发行数据的设计意图对得上。
1.5 这对做 MOD 意味着什么
- 复活是数据驱动的三选一。改代价、改地点、改面板顺序,直接改
MEDIA/RESURRECT/下的三份 DAT。按数据结构看,加第四条选项只要多一个 DAT 与一个新的INDEX;但WHERE是否认得三个值之外的写法没查,稳妥的做法是新选项只用CURRENT/TOWN/LEVEL_START这三个有先例的值。 - 别按键名实现“死了掉经验”。
XP_PENALTY/FAME_PENALTY存在、加载器也读,但原版三条选项一点经验声望都不扣。写非零值会怎样,发行数据里没有先例。 GOLD_PENALTY写 30 到底扣 30 金币还是 30%,本文给不出答案。改这个值之前先在游戏里试一次,不要按字面推。REQUIRES_ITEM/TAKE_ITEM/EFFECT/BLOCKING_EFFECT四个键引擎会读,但 Runic 自己一条都没用,语义没查——想做“消耗一件道具原地复活”之类的玩法,这四个键是入口,行为要自己验。
2. 任务系统(MEDIA/QUESTS/,184 个)
2.1 一个任务 DAT 的骨架
以 A1-ICEDEEP-TAKINGNOTES.DAT(任务 a1-IceDeep-TakingNotes,显示名 Taking Notes)为例,一份任务 DAT 由顶层键与若干块组成。顶层键有 NAME / DISPLAYNAME / QUEST_GUID / QUESTRANKMIN / QUESTRANKMAX / DIALOGPRIORITY / SHOWTIPS / CANABANDON / SHOWQUESTCOMPLETE(这些键的运行时语义本文没逆,只列出)。块有:
| 块 | 内容 | 语料覆盖 |
|---|---|---|
[DIALOG] | 六种子块(见 §2.2),每个子块里是 UNITNAME / DIALOG / DIALOG_COMPLETE / ICONABOVEHEAD 等键 | 131 个任务至少有一个对话子块 |
[DUNGEONS_WHEN_COMPLETE] / [DUNGEONS_WHEN_NOT_COMPLETE] | 各列一组 dungeon 名 | 77 个任务带这一对 |
[REQUIREMENTS] / [QUESTSCOMPLETE] | 前置任务名列表 | 85 个任务带前置 |
[REWARD] | GOLDMINPCT / GOLDMAXPCT / XPMINPCT / XPMAXPCT / FAMEMINPCT / FAMEMAXPCT / TREASURECLASS | 123 个任务带奖励块 |
Taking Notes 的关卡两分与前置:[DUNGEONS_WHEN_COMPLETE] = FROSTEDHILLS,[DUNGEONS_WHEN_NOT_COMPLETE] = ICEDEEPCAVERNS,[QUESTSCOMPLETE] 为空。Runic 自己的模板任务 A1-ACQUIRETEMPLATE.DAT 则前置于 A1-GOTOTOWN_TALKTOREGENT,关卡两分为 ESTHERIANCITY / THETEMPLESTEPPES。
2.2 [DIALOG] 下的六种子块
六种子块的名字:INTRO / DETAILS / COMPLETE / RETURN / HUDDETAILS / PASSIVE。
[!UPDATE] 本节原始普查只确认发行数据中的六种常见子块。后续逆向已补全触发时机,并确认引擎还支持
MOREDETAILS;见任务完成、目标计数与对话状态机。下面的模板文字仍仅作为当时的数据侧线索保留。
INTRO Quest introduction text
DETAILS Obtain the [item] from the [place], in the [zone].
(DIALOG_COMPLETE) Return to the [questNPC] in the [zone] for your reward.
COMPLETE Quest completion text
RETURN Return to Quest NPC text
HUDDETAILS - Obtain [item] in [zone]
(DIALOG_COMPLETE) - Return Armor Schematics to Vanquisher Scout模板里没有 PASSIVE。PASSIVE 与任务状态的关系没查;能确定的只有两件事:它一个任务里能有多个(全库 449 个 PASSIVE 块 vs 184 个任务),以及 ICONABOVEHEAD 主要就写在它里面(445 处里的 441 处,见 §2.4)。
子块里的键:UNITNAME(说这段话的 NPC)、DIALOG、DIALOG_COMPLETE、ICONABOVEHEAD,另有几个菜单 / 暂停 / 看向玩家性质的键,本文不展开。RETURN 这类子块同一任务里也可以有多个——Taking Notes 的 RETURN 就有 4 块:1 块 UNITNAME 为 A1-JADOK,3 块分别是 A1-JADOK_NOTE1/2/3_ICEDEEP-TAKINGNOTES(三本日志各一段独白,ICONABOVEHEAD:false)。所以每种子块都要当列表存,不能当单条。
2.3 两态:DIALOG / DIALOG_COMPLETE,以及它在关卡侧的投影
同一个对话子块里,DIALOG 是任务未完成时的文本,DIALOG_COMPLETE 是已完成时的文本,两条独立。Taking Notes 的 HUDDETAILS 块:
DIALOG - Acquire |cFFD1FF7A[DEFEATCOMPLETECOUNT]/[DEFEATCOUNT] Journals|u in the |cFFD1FF7AIcedeep Caverns|u
DIALOG_COMPLETE - Return to |cFFD1FF7AJadok|u in the |cFFD1FF7AFrosted Hills|u(|cFFD1FF7A…|u 是文本里原样存在的标记,本文不展开其语义。)
全库 184 个任务里 68 个带两态文本;77 个带 [DUNGEONS_WHEN_COMPLETE] / [DUNGEONS_WHEN_NOT_COMPLETE]——那是同一个两态在关卡侧的投影;85 个带 [QUESTSCOMPLETE] 前置。
引擎侧把两态坐实了(来自全函数语料 ALLFUNCS/J_quests.funcs.json):
| 项 | 事实 |
|---|---|
| 任务对象 +152 | 任务名 |
| 任务对象 +456 | 64 位 GUID |
sub_6FB460 | 按名查任务 |
sub_6F97C0 | 状态推进;+8 是“已处理”标志;内含 GLOBAL_TRILLBOT 的具名硬编特判 |
| 编辑器导出 | EditorSetEditorActiveQuests / EditorSetEditorCompletedQuests——只有 active / completed 两个动作 |
所以没有“进行中”这个第三态:发行数据每个块只给两条文本,编辑器也只有两个动作。
后续进展:任务完成谓词、objective 计数、[DEFEATCOUNT] / [DEFEATCOMPLETECOUNT] 替换与 GLOBAL_TRILLBOT 特判已经补完,见任务完成、目标计数与对话状态机。[item] / [zone] / [place] / [questNPC] 的替换,以及两个 [DUNGEONS_WHEN_*] 块在关卡侧具体驱动什么,仍未查。
2.4 ICONABOVEHEAD:445 处全部是 false
这是本节最重要、也最容易判错的一条。
普查结果:扫全 MEDIA 16,084 份 DAT,ICONABOVEHEAD 只出现在 QUESTS/ 目录下,共 445 处,取值全部是 false;其中 441 处在 QUEST/DIALOG/PASSIVE 子块里;另有 424 个对话块根本没写这个键。按任务数,32 个任务写过它。它是子块里的 bool,与 UNITNAME 在一起——按 NPC、按对话块给,不是全局开关。
“全是 false”有两种解释,必须分开:
- 它是死键——引擎根本不读,写什么都无所谓。
- 引擎读它,缺键时有个默认值,而 Runic 只在想“关掉”的地方显式写了
false。
判别用的是 exe 本身:在 Torchlight2.exe 里搜 ICONABOVEHEAD 的 UTF-16LE 宽字符串,命中 1 处 ⇒ 引擎读它,不是死键,第一种解释排除。这个检索配了负对照:搜一个编出来的键名必须 0 命中,否则“搜得到”这件事本身没有判别力。
于是只剩第二种解释:图标“亮”那一态只能来自缺键时的默认值。而那个默认值没逆——要拿到它,得找到 bool 读法 sub_677F30 读这个键的那个调用点。
结论:ICONABOVEHEAD 不能用来驱动头顶图标的可见性。照数据做,图标永远不显示;写 true 会怎样没人验证过——发行数据里一个 true 都没有。
WARNING
这条结论第一眼很容易判反。看到键名 ICONABOVEHEAD,直觉是“头顶图标的数据就在这儿”。名字对上了不等于机制对上了——先问该从哪一层读:这个键管的是“关掉”,管“亮”的是一个还没逆出来的默认值。
复刻侧的处理(移植选择):按任务状态驱动可见性(未完成显示、已完成不显示),面板上如实写明“数据说 false / 按状态显示 / 缺键默认值没逆”。
2.5 解析陷阱:漏掉一整种子块,而自检全绿
第一版任务解析器的 DIALOG_KINDS 里没有 PASSIVE。后果是 445 处 ICONABOVEHEAD 只解析到 1 处——另外 441 处全在 PASSIVE 里。而当时的自检全绿:它只断言“至少有一个缺键的块”,没有任何数量约束。
修法不是猜一个阈值。第一次补的断言是“≥100 个任务带该键”,而真值是 32——阈值猜大了会误报,猜小了照样放过。最后改成两种独立方法对账:A 用结构化解析数“带该键的任务数”,B 用裸文本数“哪些文件出现过这个键”,两者必须相等。这才把 PASSIVE 漏块照出来。
顺带一条同型的教训:PASSIVE 一个任务里有多个(449 vs 184),解析器若按“每种子块一条”存,会静默丢掉后面的块。
2.6 [REWARD] 与占位符:数据在,口径没查
Taking Notes 的 [REWARD]:
GOLDMINPCT 2000 GOLDMAXPCT 2000
XPMINPCT 1000 XPMAXPCT 1000
FAMEMINPCT 500 FAMEMAXPCT 500
TREASURECLASS QUESTREWARD_SIDEQUEST模板任务 A1-ACQUIRETEMPLATE.DAT 的对应值是金币 0 / 经验 0 / 声望 400,TREASURECLASS 同为 QUESTREWARD_SIDEQUEST。
键名带 PCT,但**“百分之多少、以什么为基数”没查**。2000 是 20 倍还是 2000%,还是别的口径,本文不猜。TREASURECLASS 的值 QUESTREWARD_SIDEQUEST 怎么被消费,同样没查。
2.7 这对做 MOD 意味着什么
- 加一句“交完任务之后 NPC 说什么”不用碰代码。同一个对话块里写第二条
DIALOG_COMPLETE就行。184 个任务里只有 68 个写了两态文本,其余 116 个没有DIALOG_COMPLETE——缺这条时引擎显示什么,没查。 - 想让 NPC 头顶亮感叹号,别指望
ICONABOVEHEAD。全库都是false,写true的效果没有先例、没人验证。逆向给出的结论是“亮”那一态只能来自缺键默认值,而默认值没逆——所以发行数据里唯一有先例的写法就是不写这个键,但它为什么亮,目前没有逆向证据。 PASSIVE块可以很多个,一个任务写十几段路人闲话是发行数据里的常态。用自己的脚本处理任务 DAT 时,六种子块都要当列表。[QUESTSCOMPLETE]是前置链的入口,85 个任务在用。[DUNGEONS_WHEN_COMPLETE]/[DUNGEONS_WHEN_NOT_COMPLETE]跟着任务状态切关卡侧的内容,77 个任务在用——照抄发行数据的写法最安全,因为它们在关卡侧具体驱动什么没逆。[REWARD]的*PCT键改之前先在游戏里对一次实际拿到的数,口径不明。GLOBAL_TRILLBOT在状态推进函数里有具名特判——涉及这个任务的改动,行为可能与其他任务不同。
3. 容器系统(MEDIA/INVENTORY/CONTAINERS/,30 个)
3.1 两层定义:槽位 DAT 与容器 DAT
容器系统在 MEDIA/INVENTORY/ 下分两层:
- 槽位(slot):
INVENTORY/*.DAT,共 37 个。一个槽位定义一种“格子”能放什么——允许的 unittype、不允许的 unittype、是否可装备、是否隐藏等。 - 容器(container):
INVENTORY/CONTAINERS/*.DAT,共 30 个。一个容器是一组(槽位名, 数量)的列表;容器的总格数就是各槽位数量之和。
37 个槽位名:
ANY ARMOR BAG BAG_ARMS_SLOT BAG_CONSUMABLES_SLOT BAG_SPELL_SLOT BELT BOOTS
COLLAR ENCHANTER_SLOT FISH GLOVES HEAD ITEMCOMBINER_SLOT ITEMCOMBINER_SLOT2
ITEMCOMBINER_SLOT3 ITEMCOMBINER_SLOT4 LEFTHAND LEFTHAND_HIDDEN LEVEL ITEM
MERCHANT ARMOR MERCHANT ITEM MERCHANT WEAPON NECKLACE PANTS POINTER RIGHTHAND
RIGHTHAND_HIDDEN RING1 RING2 SHOULDERS SPELL STUD STUD2 TORSO TRADE_SLOT WEAPON3.2 玩家的三个面板 = 三个真容器
| 容器名 | 源文件 | 格数 | 槽位构成 |
|---|---|---|---|
MAIN BAG | INVENTORY/CONTAINERS/MAINBAG.DAT | 24 | BAG × 24 |
BODY | INVENTORY/CONTAINERS/BODY.DAT | 13 | 13 个不同槽位各 1(见 §3.3) |
SPELLSLOTS | INVENTORY/CONTAINERS/SPELLSLOTS.DAT | 4 | SPELL × 4 |
BAG 槽位(INVENTORY/BAG.DAT)允许 TAKEABLE 与 QUESTITEM,不可装备;SPELL 槽位(INVENTORY/SPELL.DAT)允许 SPELL,可装备。
复刻侧按容器表自绘三个面板并实跑 dump 校验,读出的格数就是 24 / 13 / 4,槽位名与顺序与 BODY.DAT 逐一相同。格数是现读容器表比对的,不写死在测试里——写死的话“界面摆了 24 个”与“容器表说 24 格”两件事就分不开了。
3.3 BODY 的 13 个槽位
| # | 槽位名 | 槽位源文件 | 允许的 unittype | 不允许 | 备注 |
|---|---|---|---|---|---|
| 1 | HEAD | INVENTORY/HEAD.DAT | HELMET | — | |
| 2 | LEFTHAND | INVENTORY/LEFTHAND.DAT | WEAPON, SHIELD | RIGHT HANDED | |
| 3 | RIGHTHAND | INVENTORY/RIGHTHAND.DAT | WEAPON | BOW, LEFT HANDED | |
| 4 | BOOTS | INVENTORY/BOOTS.DAT | BOOTS | — | |
| 5 | BELT | INVENTORY/BELT.DAT | BELT | — | |
| 6 | PANTS | INVENTORY/PANTS.DAT | PANTS | — | |
| 7 | TORSO | INVENTORY/TORSO.DAT | CHEST ARMOR | — | |
| 8 | SHOULDERS | INVENTORY/SHOULDERS.DAT | SHOULDER ARMOR | — | |
| 9 | GLOVES | INVENTORY/GLOVES.DAT | GLOVES | — | |
| 10 | NECKLACE | INVENTORY/NECKLACE.DAT | NECKLACE | COLLAR | |
| 11 | RING1 | INVENTORY/RING1.DAT | RING | STUD | |
| 12 | RING2 | INVENTORY/RING2.DAT | RING | STUD | |
| 13 | POINTER | INVENTORY/POINTER.DAT | TAKEABLE | — | 隐藏槽,不可装备;“拿在手上那件” |
第 13 槽 POINTER 在 BODY.DAT 的容器定义里就有,是数据的一部分。MERCHANTBODY 与 BODY 的 13 个槽位完全相同;MONSTERBODY 是 12 槽——没有 NECKLACE / RING1 / RING2,多出 LEVEL ITEM × 2。
3.4 技能栏是 4 格,不是 10
SPELLSLOTS.DAT 只有 SPELL × 4。原版界面底部那排 1–9/0 是技能快捷键——那是键位映射,不是容器。两件事名字都带“技能”,但一个在 MEDIA/INVENTORY/CONTAINERS/,一个在 MEDIA/KEYMAPPING/,互不相干。
把技能栏做成 10 格,是把界面印象当成了数据。发行数据说 4。
3.5 30 个容器全表
| 容器名 | 源文件(INVENTORY/CONTAINERS/) | 格数 | 槽位构成 |
|---|---|---|---|
BODY | BODY.DAT | 13 | 见 §3.3 |
MAIN BAG | MAINBAG.DAT | 24 | BAG × 24 |
SPELLSLOTS | SPELLSLOTS.DAT | 4 | SPELL × 4 |
POINTER | POINTER.DAT | 1 | POINTER × 1 |
WEAPONSWAP | WEAPONSWAP.DAT | 2 | LEFTHAND_HIDDEN, RIGHTHAND_HIDDEN |
PLAYER_BAG_ARMS | PLAYER_BAG_ARMS.DAT | 32 | BAG_ARMS_SLOT × 32 |
PLAYER_BAG_CONSUMABLES | PLAYER_BAG_CONSUMABLES.DAT | 32 | BAG_CONSUMABLES_SLOT × 32 |
PLAYER_BAG_SPELLS | PLAYER_BAG_SPELLS.DAT | 24 | BAG_SPELL_SLOT × 24 |
STASH | STASHBAG.DAT | 20 | BAG × 20 |
PLAYER_STASH_BAG_ARMS | PLAYER_STASH_BAG_ARMS.DAT | 40 | BAG_ARMS_SLOT × 40 |
PLAYER_STASH_BAG_CONSUMABLES | PLAYER_STASH_BAG_CONSUMABLES.DAT | 40 | BAG_CONSUMABLES_SLOT × 40 |
PLAYER_STASH_BAG_SPELLS | PLAYER_STASH_BAG_SPELLS.DAT | 40 | BAG_SPELL_SLOT × 40 |
SHARED_STASH_BAG_ARMS | SHARED_STASH_BAG_ARMS.DAT | 40 | BAG_ARMS_SLOT × 40 |
SHARED_STASH_BAG_CONSUMABLES | SHARED_STASH_BAG_CONSUMABLES.DAT | 40 | BAG_CONSUMABLES_SLOT × 40 |
SHARED_STASH_BAG_SPELLS | SHARED_STASH_BAG_SPELLS.DAT | 40 | BAG_SPELL_SLOT × 40 |
PLAYER_TRADE | PLAYER_TRADE.DAT | 6 | TRADE_SLOT × 6 |
PET BAG | PETBAG.DAT | 24 | BAG × 24 |
PETBODY | PETBODY.DAT | 3 | COLLAR, STUD, STUD2 |
MERCHANTBODY | MERCHANTBODY.DAT | 13 | 与 BODY 相同 |
MERCHANT BAG | MERCHANTBAG.DAT | 35 | BAG × 35 |
MERCHANT BUY BACK BAG | MERCHANTBUYBACKBAG.DAT | 40 | BAG × 40 |
MERCHANT ARMORS | MERCHANTARMOR.DAT | 40 | MERCHANT ARMOR × 40 |
MERCHANT WEAPONS | MERCHANTWEAPON.DAT | 40 | MERCHANT WEAPON × 40 |
MERCHANTITEM | MERCHANTITEM.DAT | 40 | MERCHANT ITEM × 40 |
MONSTERBODY | MONSTERBAG.DAT | 12 | HEAD, RIGHTHAND, LEFTHAND, BOOTS, BELT, PANTS, TORSO, SHOULDERS, GLOVES, LEVEL ITEM × 2, POINTER |
NPCBODY | NPCBODY.DAT | 2 | RIGHTHAND, LEFTHAND |
LEVELBAG | LEVELBAG.DAT | 10 | LEVEL ITEM × 10 |
QUESTREWARDS | QUESTREWARDS.DAT | 3 | BAG × 3 |
ITEMCOMBINERBAG | ITEMCOMBINERBAG.DAT | 4 | ITEMCOMBINER_SLOT ×1, _SLOT2, _SLOT3, _SLOT4 |
ENCHANTER BAG | ENCHANGERBAG.DAT | 1 | ENCHANTER_SLOT × 1 |
两处文件名与容器名对不上的地方是发行数据本身如此:ENCHANTER BAG 的文件叫 ENCHANGERBAG.DAT(少了 T 多了 G),MONSTERBODY 的文件叫 MONSTERBAG.DAT。按文件名找容器时要留意。
3.6 顺带:面板键位来自 MEDIA/KEYMAPPING/
复刻侧的面板开关绑的是从原版 MEDIA/KEYMAPPING/ 生成的 action:i = InventoryMenu(背包)、c = StatMenu(角色面板)。同一份键位表里还有 s = SkillMenu、w = WeaponSwap、a = AutoTarget——任何想在 TL2 系上加 WASD 移动的方案,先得处理这三条冲突。数字键 1–9/0 在原版键位表里就是放技能的。
3.7 这对做 MOD 意味着什么
- 改背包格数、改装备槽,改的是
INVENTORY/CONTAINERS/*.DAT里的槽位数量,不是界面文件。MAIN BAG的 24 就是BAG槽的 count。 - 想给技能栏加格子,改
SPELLSLOTS.DAT的SPELLcount——它现在是 4,与那排 10 个数字键无关。改了容器格数之后界面能不能显示出来是另一层的事,本文没查。 - 一个槽能放什么由槽位 DAT 决定(
INVENTORY/<槽名>.DAT的允许 / 不允许 unittype 列表)。RIGHTHAND不收BOW与LEFT HANDED,NECKLACE不收COLLAR,RING1/RING2不收STUD——这是数据,改它们比改物品的 unittype 更直接。 BODY有 13 槽而不是 12,第 13 个是隐藏的POINTER。处理装备存档、写自己的装备栏脚本时把它算上,否则会差一个。MERCHANTBODY与BODY完全同构,MONSTERBODY少三个首饰槽、多两个LEVEL ITEM——给商人或怪物做装备相关 MOD 时,别拿玩家的 13 槽套用。- 玩家仓库分三类(
ARMS/CONSUMABLES/SPELLS)各 40 格、共享仓库同样三类各 40 格;商人侧MERCHANT BAG35 格,回购 / 护甲 / 武器 / 杂物四个容器各 40 格。这些数字全在表里,改它们不需要碰代码。
4. 给 MOD 开发者的速查
4.1 键名会骗人:直觉与事实的对照
| 键 / 数字 | 直觉 | 事实 |
|---|---|---|
XP_PENALTY / FAME_PENALTY | 死亡掉经验、掉声望 | 加载器读,但三条选项全为 0——原版只扣金币 |
GOLD_PENALTY: 30 | 扣 30 金币(或 30%) | 百分比还是定额未定;消费端没找到 |
sub_651DF0 | 难度 DAT 加载器(旧标签) | 复活选项加载器;读 WHERE 与 ALLOW_IN_NON_PORTAL_LEVELS |
ICONABOVEHEAD | 写 true 让 NPC 头顶亮图标 | 全库 445 处全 false,引擎读它但缺键默认值没逆;写 true 无先例 |
| 任务“进行中” | 任务有三态 | 数据与编辑器都只有两态:未完成 / 已完成 |
| 五种对话子块 | INTRO / DETAILS / COMPLETE / RETURN / HUDDETAILS | 六种,还有 PASSIVE;且 441/445 个 ICONABOVEHEAD 在它里面 |
| 技能栏 10 格 | 界面上那排 1–9/0 | SPELLSLOTS 容器是 4 格;数字键是键位映射 |
BODY 12 槽 | 头、双手、四件甲、腰带、鞋、项链、两戒 | 13 槽,第 13 个是隐藏的 POINTER |
ENCHANGERBAG.DAT | 文件名打错了 | 发行数据就叫这个名字,ENCHANTER BAG 容器的源文件 |
4.2 直接可用的数据入口
| 想做的事 | 改哪里 | 注意 |
|---|---|---|
| 改复活代价 / 地点 / 面板顺序 | MEDIA/RESURRECT/RES_IN_*.DAT 的 GOLD_PENALTY / WHERE / INDEX | GOLD_PENALTY 口径未定,改完实测 |
| 让 NPC 交任务后换台词 | 同一对话块里加 DIALOG_COMPLETE | 116 个原版任务还没用两态文本 |
| 给任务加前置 | [REQUIREMENTS] / [QUESTSCOMPLETE] | 85 个原版任务的写法可照抄 |
| 改背包 / 仓库 / 商人格数 | INVENTORY/CONTAINERS/*.DAT 的槽位 count | 界面能否跟着长没查 |
| 改一个装备槽能放什么 | INVENTORY/<槽名>.DAT 的允许 / 不允许 unittype | 比改物品 unittype 更直接 |
5. 未解与尚未闭环事项清单
| 待解项 | 现状 |
|---|---|
GOLD_PENALTY 的应用口径(百分比 vs 定额) | 读取 +92 并施加扣款的消费函数尚未定位 |
| 复活后 HP 回多少 | DAT 里没有对应键,属引擎行为,没逆 |
WHERE 是否认得 CURRENT / TOWN / LEVEL_START 以外的值 | 没查 |
ALLOW_IN_NON_PORTAL_LEVELS 的判定逻辑、DIFFICULTIESBIN 的语义、WEIGHT 参与的计算 | 没逆 |
EFFECT / BLOCKING_EFFECT / REQUIRES_ITEM / TAKE_ITEM | 发行数据里一条都没用,语义没查 |
任务完成条件([REQUIREMENTS]、objective 计数) | 已在后续报告补完;见 ../wiki/quest-completion-objectives-dialog-state-machine.md |
[DEFEATCOUNT] / [item] / [zone] 等占位符的替换 | [DEFEATCOUNT] / [DEFEATCOMPLETECOUNT] 已补完;其余占位符未查 |
| 对话子块各自的触发时机 | 已在后续报告补完,并确认引擎支持第七种 MOREDETAILS |
ICONABOVEHEAD 缺键时的默认值 | 没逆——需要 sub_677F30 在任务加载器里的调用点;这才是图标“亮”的来源 |
写 ICONABOVEHEAD:true 的实际效果 | 发行数据无先例,没人验证 |
[REWARD] 的 *PCT 口径与 TREASURECLASS 的抽取方式 | 没查 |
[DUNGEONS_WHEN_COMPLETE] / [DUNGEONS_WHEN_NOT_COMPLETE] 在关卡侧具体驱动什么 | 没逆 |
sub_6F97C0 里 GLOBAL_TRILLBOT 特判的具体分支 | 已补完:解锁内部成就 TRILLBOTASSEMBLED(成就表第 113 项) |
任务 DAT 顶层键(QUESTRANKMIN/MAX、DIALOGPRIORITY、SHOWTIPS、CANABANDON、SHOWQUESTCOMPLETE)的语义 | 本文只列出,没逆 |
| 改容器格数后界面是否跟着变 | 没查 |
| 容器 DAT 里格数之外的键(优先级、自动放置、回购等) | 本文未展开 |
附录 A:符号、偏移与数字速查
| 项 | 值 |
|---|---|
| 复活选项加载器 | sub_651DF0(Torchlight2.exe,ImageBase 0x400000) |
| DAT 通用读法 | sub_677F00(字符串)/ sub_677F30(bool)/ sub_677F80(int) |
| 复活对象字段偏移 | NAME +8 / GOLD_PENALTY +92 / XP_PENALTY +96 / FAME_PENALTY +100 / WHERE +104 / WEIGHT +136 / REQUIRES_ITEM +144 / TAKE_ITEM +152 / ALLOW_IN_NON_PORTAL_LEVELS +160 / DIFFICULTIESBIN +164 / INDEX +212 |
| 任务对象字段偏移 | +152 任务名 / +456 64 位 GUID / +8 已处理标志 |
| 任务按名查找 / 状态推进 | sub_6FB460 / sub_6F97C0(含 GLOBAL_TRILLBOT 特判) |
| 编辑器任务状态导出 | EditorSetEditorActiveQuests / EditorSetEditorCompletedQuests |
| 任务语料 | 184 个任务;68 带两态文本 / 32 写过 ICONABOVEHEAD / 77 带关卡两分 / 85 带前置;PASSIVE 块 449 个 |
ICONABOVEHEAD 普查 | 全 MEDIA 16,084 份 DAT;445 处全 false(441 在 PASSIVE);424 个块缺键;exe 内 UTF-16LE 命中 1 处 |
| 容器语料 | 37 个槽位 DAT / 30 个容器 DAT;MAIN BAG 24 / BODY 13 / SPELLSLOTS 4 |
附录 B:支撑源材料与真值账本映射
- 真值账本
truth/ledger.jsonl:- C-0175(复活三选一、
sub_651DF0字段偏移、sub_651DF0旧标签墓碑); - C-0176(UI 三件套格数与槽位名来自容器表;技能栏 4 格);
- C-0177(任务两态
DIALOG/DIALOG_COMPLETE;ICONABOVEHEAD全库 445 处false)。
- C-0175(复活三选一、
- 数据结构与静态配置(
原版游戏分析/reversed_cpp/REGISTRY/):resurrect_and_saveload.json、quest_two_state.json、ui_three_panels.json。 - 语料解析产物(
build/f2/):quests.json(184 个任务的结构化解析)、containers.json(37 槽位 + 30 容器)。 - 解析工具:
tools/f2_resurrect.py、tools/f2_quests.py。