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. 核心结论速览

  1. 原版死亡只扣金币,不扣经验、不扣声望MEDIA/RESURRECT/ 三条选项的 XP_PENALTYFAME_PENALTY 全部为 0。加载器确实读这两个键(字段偏移 +96 / +100),但发行数据没给它们任何非零值。按键名去实现“死亡掉经验”,等于实现了一条原版没有的规则。
  2. 复活是发行数据里的三选一INPLACEINDEX 0 / WHERE CURRENT / GOLD_PENALTY 30 / ALLOW_IN_NON_PORTAL_LEVELS:false / DIFFICULTIESBIN:3)、INTOWN(1 / TOWN / 0)、LEVELENTRANCE(2 / LEVEL_START / 10)。一个 DAT 文件一条选项,INDEX 是面板顺序,WEIGHT 三条都是 1。
  3. 复活选项的加载器是 sub_651DF0,它不是“难度 DAT 加载器”。早期语料把它标成了难度加载,判据很简单:它读 WHEREALLOW_IN_NON_PORTAL_LEVELS,而难度 DAT 里没有这两个键。三个 *_PENALTY 键两边都有,大概就是标错的原因。
  4. GOLD_PENALTY 是百分比还是定额,没逆出来。加载器只把它当整数存进 +92;读取 +92 并真正扣钱的消费函数尚未定位。
  5. 任务的两态就在同一个对话块里DIALOG(未完成)与 DIALOG_COMPLETE(已完成)是两条独立文本;[DUNGEONS_WHEN_COMPLETE] / [DUNGEONS_WHEN_NOT_COMPLETE] 是同一个两态在关卡侧的投影。编辑器导出只有 EditorSetEditorActiveQuests / EditorSetEditorCompletedQuests 两个动作——两态坐实,没有第三态。
  6. 发行任务普查常见六种 [DIALOG] 子块INTRO / DETAILS / COMPLETE / RETURN / HUDDETAILS / PASSIVE。后续代码逆向还确认引擎支持第七种 MOREDETAILS;各自触发时机见任务完成、目标计数与对话状态机
  7. ICONABOVEHEAD 在全 MEDIA 16,084 份 DAT 里出现 445 处,取值全部是 false,另有 424 个对话块根本没写它。而它的 UTF-16LE 宽字符串在 exe 里命中 1 处——引擎确实读它,不是死键。于是图标“亮”那一态只能来自缺键时的默认值,而那个默认值没逆。想靠这个键点亮头顶感叹号,行不通。
  8. 任务完成条件已有后续补完。通用谓词、DEFEAT 计数与存档、[DEFEATCOUNT] 替换、对话状态机和 GLOBAL_TRILLBOT 特判均已钉住;[REWARD]GOLDMINPCT 等百分比口径仍未查。见任务完成、目标计数与对话状态机
  9. 玩家的三个面板是三个真容器MAIN BAG 24 格(单槽 BAG)、BODY 13 槽、SPELLSLOTS 4 格(单槽 SPELL)。格数、槽位名、顺序全是数据,不是界面布局的选择。
  10. 技能栏是 4 格,不是 10。原版界面上那排 1–9/0 是技能快捷键,与 SPELLSLOTS 容器不是一回事。写成 10 格是把界面印象当成了数据。
  11. BODY 的第 13 槽是 POINTER(拿在手上那件)。它在容器定义里就属于 BODY,不是复刻时加的。

1. 复活系统(MEDIA/RESURRECT/

1.1 三条 authored 选项全表

MEDIA/RESURRECT/ 下一个 DAT 文件对应一条复活选项。三份文件的全部有效键值如下:

源文件NAMETITLEWHEREGOLD_PENALTYXP_PENALTYFAME_PENALTYINDEXWEIGHTDIFFICULTIESBINALLOW_IN_NON_PORTAL_LEVELS
RES_IN_PLACE.DATINPLACEResurrectCURRENT3000013false
RES_IN_TOWN.DATINTOWNTownTOWN00011(未写)(未写)
RES_IN_DUNGEON.DATLEVELENTRANCEEntranceLEVEL_START100021(未写)(未写)

三条的 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 都是 NONEREQUIRES_ITEM / TAKE_ITEM / EFFECT / BLOCKING_EFFECT 四个键在三份文件里一条都没用(全为空)。DESCRIPTION 里的 [GOLD] 是占位符,其替换逻辑未查。

三点观察:

  • XP_PENALTYFAME_PENALTY 三条全为 0。这两个键加载器是读的(见 §1.2),所以它们不是死键——只是 Runic 从没给过非零值。原版“死亡惩罚”的全部内容就是 30 或 10 金币。
  • INDEX 0 / 1 / 2 对应面板上的顺序;WEIGHT 三条都是 1,它参与什么计算没查。
  • DIFFICULTIESBIN:3ALLOW_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 加载器。这个标签是错的,判据很简单:

  • 它读 WHEREALLOW_IN_NON_PORTAL_LEVELS——难度 DAT 里没有这两个键;
  • GOLD_PENALTY / XP_PENALTY / FAME_PENALTY 三个键在难度 DAT 与复活 DAT 里都有,大概就是因此被标错的。

这个错标签的代价不在标签本身,而在它把人往哪引:按“难度加载”去找 GOLD_PENALTY 的消费端,会一路走到难度系统去,而那里没有复活扣款的代码。把它记成墓碑,是为了让后来者不再走这条弯路。

1.4 没逆出来的部分

现状
GOLD_PENALTY百分比还是定额未定。加载器把它当整数存进 +92;读取 +92 并施加扣款的消费函数尚未定位。
复活后回多少 HP三份 DAT 里没有这个键,加载器也不读 ⇒ 这是引擎行为,没逆。
WHERECURRENT / 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_LEVELSDIFFICULTIESBIN 不生效。实测同图同种子、不开自动复活时角色死在场上;开了之后死 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 / TREASURECLASS123 个任务带奖励块

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

模板里没有 PASSIVEPASSIVE 与任务状态的关系没查;能确定的只有两件事:它一个任务里能有多个(全库 449 个 PASSIVE 块 vs 184 个任务),以及 ICONABOVEHEAD 主要就写在它里面(445 处里的 441 处,见 §2.4)。

子块里的键:UNITNAME(说这段话的 NPC)、DIALOGDIALOG_COMPLETEICONABOVEHEAD,另有几个菜单 / 暂停 / 看向玩家性质的键,本文不展开。RETURN 这类子块同一任务里也可以有多个——Taking Notes 的 RETURN 就有 4 块:1 块 UNITNAMEA1-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任务名
任务对象 +45664 位 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 份 DATICONABOVEHEAD 只出现在 QUESTS/ 目录下,共 445 处,取值全部是 false;其中 441 处QUEST/DIALOG/PASSIVE 子块里;另有 424 个对话块根本没写这个键。按任务数,32 个任务写过它。它是子块里的 bool,与 UNITNAME 在一起——按 NPC、按对话块给,不是全局开关。

“全是 false”有两种解释,必须分开

  1. 它是死键——引擎根本不读,写什么都无所谓。
  2. 引擎读它,缺键时有个默认值,而 Runic 只在想“关掉”的地方显式写了 false

判别用的是 exe 本身:在 Torchlight2.exe 里搜 ICONABOVEHEADUTF-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  WEAPON

3.2 玩家的三个面板 = 三个真容器

容器名源文件格数槽位构成
MAIN BAGINVENTORY/CONTAINERS/MAINBAG.DAT24BAG × 24
BODYINVENTORY/CONTAINERS/BODY.DAT1313 个不同槽位各 1(见 §3.3)
SPELLSLOTSINVENTORY/CONTAINERS/SPELLSLOTS.DAT4SPELL × 4

BAG 槽位(INVENTORY/BAG.DAT)允许 TAKEABLEQUESTITEM,不可装备;SPELL 槽位(INVENTORY/SPELL.DAT)允许 SPELL,可装备。

复刻侧按容器表自绘三个面板并实跑 dump 校验,读出的格数就是 24 / 13 / 4,槽位名与顺序与 BODY.DAT 逐一相同。格数是现读容器表比对的,不写死在测试里——写死的话“界面摆了 24 个”与“容器表说 24 格”两件事就分不开了。

3.3 BODY 的 13 个槽位

#槽位名槽位源文件允许的 unittype不允许备注
1HEADINVENTORY/HEAD.DATHELMET
2LEFTHANDINVENTORY/LEFTHAND.DATWEAPON, SHIELDRIGHT HANDED
3RIGHTHANDINVENTORY/RIGHTHAND.DATWEAPONBOW, LEFT HANDED
4BOOTSINVENTORY/BOOTS.DATBOOTS
5BELTINVENTORY/BELT.DATBELT
6PANTSINVENTORY/PANTS.DATPANTS
7TORSOINVENTORY/TORSO.DATCHEST ARMOR
8SHOULDERSINVENTORY/SHOULDERS.DATSHOULDER ARMOR
9GLOVESINVENTORY/GLOVES.DATGLOVES
10NECKLACEINVENTORY/NECKLACE.DATNECKLACECOLLAR
11RING1INVENTORY/RING1.DATRINGSTUD
12RING2INVENTORY/RING2.DATRINGSTUD
13POINTERINVENTORY/POINTER.DATTAKEABLE隐藏槽,不可装备;“拿在手上那件”

第 13 槽 POINTERBODY.DAT 的容器定义里就有,是数据的一部分。MERCHANTBODYBODY 的 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/格数槽位构成
BODYBODY.DAT13见 §3.3
MAIN BAGMAINBAG.DAT24BAG × 24
SPELLSLOTSSPELLSLOTS.DAT4SPELL × 4
POINTERPOINTER.DAT1POINTER × 1
WEAPONSWAPWEAPONSWAP.DAT2LEFTHAND_HIDDEN, RIGHTHAND_HIDDEN
PLAYER_BAG_ARMSPLAYER_BAG_ARMS.DAT32BAG_ARMS_SLOT × 32
PLAYER_BAG_CONSUMABLESPLAYER_BAG_CONSUMABLES.DAT32BAG_CONSUMABLES_SLOT × 32
PLAYER_BAG_SPELLSPLAYER_BAG_SPELLS.DAT24BAG_SPELL_SLOT × 24
STASHSTASHBAG.DAT20BAG × 20
PLAYER_STASH_BAG_ARMSPLAYER_STASH_BAG_ARMS.DAT40BAG_ARMS_SLOT × 40
PLAYER_STASH_BAG_CONSUMABLESPLAYER_STASH_BAG_CONSUMABLES.DAT40BAG_CONSUMABLES_SLOT × 40
PLAYER_STASH_BAG_SPELLSPLAYER_STASH_BAG_SPELLS.DAT40BAG_SPELL_SLOT × 40
SHARED_STASH_BAG_ARMSSHARED_STASH_BAG_ARMS.DAT40BAG_ARMS_SLOT × 40
SHARED_STASH_BAG_CONSUMABLESSHARED_STASH_BAG_CONSUMABLES.DAT40BAG_CONSUMABLES_SLOT × 40
SHARED_STASH_BAG_SPELLSSHARED_STASH_BAG_SPELLS.DAT40BAG_SPELL_SLOT × 40
PLAYER_TRADEPLAYER_TRADE.DAT6TRADE_SLOT × 6
PET BAGPETBAG.DAT24BAG × 24
PETBODYPETBODY.DAT3COLLAR, STUD, STUD2
MERCHANTBODYMERCHANTBODY.DAT13BODY 相同
MERCHANT BAGMERCHANTBAG.DAT35BAG × 35
MERCHANT BUY BACK BAGMERCHANTBUYBACKBAG.DAT40BAG × 40
MERCHANT ARMORSMERCHANTARMOR.DAT40MERCHANT ARMOR × 40
MERCHANT WEAPONSMERCHANTWEAPON.DAT40MERCHANT WEAPON × 40
MERCHANTITEMMERCHANTITEM.DAT40MERCHANT ITEM × 40
MONSTERBODYMONSTERBAG.DAT12HEAD, RIGHTHAND, LEFTHAND, BOOTS, BELT, PANTS, TORSO, SHOULDERS, GLOVES, LEVEL ITEM × 2, POINTER
NPCBODYNPCBODY.DAT2RIGHTHAND, LEFTHAND
LEVELBAGLEVELBAG.DAT10LEVEL ITEM × 10
QUESTREWARDSQUESTREWARDS.DAT3BAG × 3
ITEMCOMBINERBAGITEMCOMBINERBAG.DAT4ITEMCOMBINER_SLOT ×1, _SLOT2, _SLOT3, _SLOT4
ENCHANTER BAGENCHANGERBAG.DAT1ENCHANTER_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.DATSPELL count——它现在是 4,与那排 10 个数字键无关。改了容器格数之后界面能不能显示出来是另一层的事,本文没查。
  • 一个槽能放什么由槽位 DAT 决定INVENTORY/<槽名>.DAT 的允许 / 不允许 unittype 列表)。RIGHTHAND 不收 BOWLEFT HANDEDNECKLACE 不收 COLLARRING1 / RING2 不收 STUD——这是数据,改它们比改物品的 unittype 更直接。
  • BODY 有 13 槽而不是 12,第 13 个是隐藏的 POINTER。处理装备存档、写自己的装备栏脚本时把它算上,否则会差一个。
  • MERCHANTBODYBODY 完全同构,MONSTERBODY 少三个首饰槽、多两个 LEVEL ITEM——给商人或怪物做装备相关 MOD 时,别拿玩家的 13 槽套用。
  • 玩家仓库分三类(ARMS / CONSUMABLES / SPELLS)各 40 格、共享仓库同样三类各 40 格;商人侧 MERCHANT BAG 35 格,回购 / 护甲 / 武器 / 杂物四个容器各 40 格。这些数字全在表里,改它们不需要碰代码。

4. 给 MOD 开发者的速查

4.1 键名会骗人:直觉与事实的对照

键 / 数字直觉事实
XP_PENALTY / FAME_PENALTY死亡掉经验、掉声望加载器读,但三条选项全为 0——原版只扣金币
GOLD_PENALTY: 30扣 30 金币(或 30%)百分比还是定额未定;消费端没找到
sub_651DF0难度 DAT 加载器(旧标签)复活选项加载器;读 WHEREALLOW_IN_NON_PORTAL_LEVELS
ICONABOVEHEADtrue 让 NPC 头顶亮图标全库 445 处全 false,引擎读它但缺键默认值没逆;写 true 无先例
任务“进行中”任务有三态数据与编辑器都只有两态:未完成 / 已完成
五种对话子块INTRO / DETAILS / COMPLETE / RETURN / HUDDETAILS六种,还有 PASSIVE;且 441/445 个 ICONABOVEHEAD 在它里面
技能栏 10 格界面上那排 1–9/0SPELLSLOTS 容器是 4 格;数字键是键位映射
BODY 12 槽头、双手、四件甲、腰带、鞋、项链、两戒13 槽,第 13 个是隐藏的 POINTER
ENCHANGERBAG.DAT文件名打错了发行数据就叫这个名字,ENCHANTER BAG 容器的源文件

4.2 直接可用的数据入口

想做的事改哪里注意
改复活代价 / 地点 / 面板顺序MEDIA/RESURRECT/RES_IN_*.DATGOLD_PENALTY / WHERE / INDEXGOLD_PENALTY 口径未定,改完实测
让 NPC 交任务后换台词同一对话块里加 DIALOG_COMPLETE116 个原版任务还没用两态文本
给任务加前置[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_6F97C0GLOBAL_TRILLBOT 特判的具体分支已补完:解锁内部成就 TRILLBOTASSEMBLED(成就表第 113 项)
任务 DAT 顶层键(QUESTRANKMIN/MAXDIALOGPRIORITYSHOWTIPSCANABANDONSHOWQUESTCOMPLETE)的语义本文只列出,没逆
改容器格数后界面是否跟着变没查
容器 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_COMPLETEICONABOVEHEAD 全库 445 处 false)。
  • 数据结构与静态配置(原版游戏分析/reversed_cpp/REGISTRY/resurrect_and_saveload.jsonquest_two_state.jsonui_three_panels.json
  • 语料解析产物(build/f2/quests.json(184 个任务的结构化解析)、containers.json(37 槽位 + 30 容器)。
  • 解析工具tools/f2_resurrect.pytools/f2_quests.py