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,而非难度加载器。早期语料曾将其误标为难度加载,其实际读取WHERE与ALLOW_IN_NON_PORTAL_LEVELS,推测系因字段重合导致的误判。 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 格属于由前端 UI 表现对底层数据产生的误读。 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,参与公式未定位。 |
复刻实现的工程处理:复活动作交由状态重置处理,不额外消耗随机数以保证联机同步一致性;状态重置为满生命值并触发对应事件;未接入经济与难度限制系统时,惩罚字段暂不结算。实测验证表明,原地复活 (INPLACE) 配置的高代价与城镇/关卡起点 (TOWN / LEVEL_START) 的零代价机制,与官方资源的设计意图一致。
1.5 MOD 制作建议
- 复活属于数据驱动机制。修改消耗代价、复活位置或界面排序时,直接调整
MEDIA/RESURRECT/下的三份 DAT。如需新增第四项复活选项,需增加新 DAT 文件并分配新的INDEX;建议复活位置继续使用验证过的CURRENT/TOWN/LEVEL_START。 - 死亡掉落机制注意点:
XP_PENALTY/FAME_PENALTY虽被加载器读取,但官方默认选项未配置扣除经验与声望,配置非零值的实际结算机制尚需游戏内实测确认。 GOLD_PENALTY计算口径:当前尚未明确数值为百分比还是固定数值,修改前建议通过游戏内实测对照。REQUIRES_ITEM/TAKE_ITEM/EFFECT/BLOCKING_EFFECT在引擎中有字段解析,但官方未启用;制作“消耗特定物品复活”等机制时可优先尝试这组接口。
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:全库取值均为 false
该字段的分析过程与结论如下:
普查统计:全 MEDIA 目录下 16,084 份 DAT 中,ICONABOVEHEAD 仅出现在 QUESTS/ 目录下,共 445 处,取值全部为 false;其中 441 处位于 QUEST/DIALOG/PASSIVE 子块中;另有 424 个对话块未声明该键。按任务分布,共 32 个任务包含该字段。该键属于对话子块内部的布尔配置,与 UNITNAME 关联,属于局部配置而非全局开关。
原语判定:
- 若为冗余失效键,则引擎内部不解析该字符串。
- 若引擎读取该键,则缺省时具备默认值,官方仅在需要关闭头顶图标的子块中显式声明
false。
通过在 Torchlight2.exe 二进制中检索 ICONABOVEHEAD 的 UTF-16LE 宽字符常量,命中 1 处调用点,证实引擎明确解析该键。
结论:ICONABOVEHEAD 无法直接用于正向激活头顶感叹号。若仅根据该键的字面值绑定逻辑,图标将全部处于隐藏状态;官方数据中不存在配置为 true 的前例。
WARNING
键名字面含义容易引发误判。ICONABOVEHEAD 在数据中实际扮演“抑制”开关(显式置 false 屏蔽图标),常规任务状态下的图标激活由状态机解析时的缺省逻辑驱动。
复刻实现的工程处理:由任务状态(未完成显示、已完成隐藏)直接驱动图标可见性,避免受全 false 数据干扰。
2.5 解析注意事项:多重子块与对账校验
在脚本解析任务数据时,若未将 PASSIVE 纳入对话子块枚举,会导致 445 处 ICONABOVEHEAD 中仅能捕获到 1 处(其余 441 处均位于 PASSIVE 块内)。
为避免此类数据遗漏,解析工具应当采用两套独立机制进行校验对账:一套通过结构化语法树提取字段统计,另一套基于裸文本全局匹配文件名列表,两者结果严格一致方可验证解析完备性。此外,单个任务内支持声明多个 PASSIVE 块(全库 449 个子块对应 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 后缀,但其具体百分比计算基数与缩放口径尚未反编译完全。TREASURECLASS 的掉落池抽取逻辑同属未完全逆向项。
2.7 MOD 制作建议
- 新增完成态 NPC 对话:直接在对话块内添加
DIALOG_COMPLETE字段即可生效。官方 184 个任务中有 68 个定义了双态文本,其余 116 个未配置DIALOG_COMPLETE。 - 任务头顶感叹号配置:官方全库均为
false,不建议依赖ICONABOVEHEAD:true实现图标显示;官方资源中让图标正常显示的常规做法是不显式声明该键。 - 对话多重块结构:
PASSIVE块在单个任务中通常存在多个实例,编写工具或解析脚本时须将其视为数组或列表进行处理。 - 前置依赖链:
[QUESTSCOMPLETE]为前置任务链声明入口(85 个任务使用);[DUNGEONS_WHEN_COMPLETE]/[DUNGEONS_WHEN_NOT_COMPLETE]负责在任务完成前后动态切换关联地下城(77 个任务使用)。 [REWARD]奖励百分比与TREASURECLASS在修改前建议结合实际数值进行实测校准。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:支撑源材料与逆向结论映射
- 逆向验证项:
- 复活机制:复活三选一机制、
sub_651DF0字段偏移及调用分析; - 容器机制:UI 面板格数与槽位映射规范、技能栏容器结构;
- 任务机制:任务双态
DIALOG/DIALOG_COMPLETE文本规范、ICONABOVEHEAD字段全库分布与缺省特性。
- 复活机制:复活三选一机制、
- 数据结构与配置清单:
resurrect_and_saveload.json、quest_two_state.json、ui_three_panels.json。 - 语料解析产物:
quests.json(184 个任务的结构化解析)、containers.json(37 槽位 + 30 容器)。 - 分析工具:
tools/f2_resurrect.py、tools/f2_quests.py。