《火炬之光 2》掉落物表现层逆向报告
对应
HANDOFF_RE_BACKLOG.md的 C-2 / M5-1。
发行程序:E:/Torchlight 2/Torchlight2.exe,imagebase0x400000,SHA-256186472C3057B38F4CDFF4696959A943C396AE6166995F7418997B5EA853E8A5E。
本文只补 C-0178/0179 没有进入的表现层:飞出动画、所谓“光柱”、品质特效、拾取音。
结论
原版装备掉落不是刚体抛射。引擎先按逐件延迟隐藏模型,延迟到期后由 sub_58EB70 建立一条名为 DROP 的三控制点 CPath;sub_57D2A0 每帧把路径进度 增加 dt × 20,通过 Catmull–Rom 曲线取样位置,到达路径总长时播放落地音并结束。
品质表现也不是“一根光柱按稀有度换色”。sub_5808F0 在四套完整粒子布局之间选择:
| 选择 | 资源 | 主要可见成分 |
|---|---|---|
| 魔法/带附魔效果 | MAGIC-ITEM.LAYOUT | 蓝色直立 Light、蓝色碎片、火花、gleam |
| 普通 | GENERIC-ITEM.LAYOUT | gleam 与上升火花;没有光柱 |
| 任务物品 | QUEST-ITEM.LAYOUT | 黄色光、尖刺、爆闪、火花和明确命名的 Beam |
| 特殊/唯一类别 | UNIQUE-ITEM.LAYOUT | 橙色碎片、橙色 LightFloor、火花、gleam |
数据还可以用 NODROPPARTICLES 完全禁用掉落粒子,或用 DROPPARTICLE 指定任意 LAYOUT 覆盖默认选择。因此复刻时应实例化原始粒子布局组合,不能只做一个按 rarity 改颜色的统一光柱材质。
声音分成三个明确阶段:FALL_SOUND、LAND_SOUND、TAKE_SOUND 分别装入声音库槽 25、26、27;飞出开始、路径结束、拾取时各触发一次。
1. 先订正 C-0178 的两个旧标签
CEquipment 主虚表起点是 0x2174864。直接读取指针得到:
| 虚表偏移 | 指针位置 | 目标 | 实际职责 |
|---|---|---|---|
+460 | 0x2174A30 | sub_5A4C00 | 模型/部件显隐切换 |
+468 | 0x2174A38 | sub_5A51D0 | 按地面法线调整模型朝向 |
+532 | 0x2174A78 | sub_58EB70 | 完整装备掉落表现初始化 |
对应表字节从 0x2174A30 开始为:
00 4C 5A 00 30 D4 57 00 D0 51 5A 00 ... 70 EB 58 00所以旧记录中的“+460 是落地弹跳、+532 只负责延迟显形”都不准确。+460 只是切换可见部件;+532 本身就是后文完整的路径、姿态、粒子和飞出音初始化函数。
2. 延迟与飞出状态
与表现直接相关的 CEquipment 字段如下:
| 偏移 | 类型 | 用途 | 指令证据 |
|---|---|---|---|
+0x4AC | float | 逐件掉落延迟 | 0x583017: fcom [esi+4ACh];0x583032: fst [esi+4ACh] |
+0x288 | CPath* | DROP 路径 | 0x57D2B6: mov ecx,[ebx+288h] |
+0x28C | float | 路径进度 | 0x57D2E4: fadd [ebx+28Ch] |
+0x290 | byte | 飞行/延迟状态 | 0x58300C: cmp byte ptr [esi+290h],0 |
+0x360 | Matrix4 | 旋转所用起始姿态 | 0x57D3F3: lea ecx,[ebx+360h] |
+0x43C | 粒子实例指针 | 掉落品质粒子 | 0x580A9E: mov ecx,[esi+43Ch] |
+0x1F0 | 声音库 | 25/26/27 三声音槽 | 0x592B29: mov ecx,[esi+1F0h] |
sub_582FF0 是逐帧外层更新。其关键控制流是:
if (equipment->drop_active) { // +0x290
if (0.0f <= equipment->drop_delay) { // +0x4AC
equipment->drop_delay -= dt;
if (equipment->drop_delay < 0.0f)
equipment->vftable_plus_532(); // sub_58EB70
else
equipment->set_parts_visible(0, 1); // vtable+460
}
equipment_update_drop_flight(equipment, dt); // sub_57D2A0
}调用 +532 的原始指令为:
0x583041 8B 82 14 02 00 00 mov eax,[edx+214h]
0x583047 FF D0 call eax0x214 即十进制 532。延迟尚未结束时则从 [edx+0x1CC] 取 +460,传入 (0,1)。
3. sub_58EB70:装备掉落初始化
3.1 延迟分支
若 +0x4AC > 0,函数只调用 vtable+460(0,1) 隐藏模型,将 +0x290 置为 1, 然后返回。真正的飞出初始化由上一节的逐帧函数在延迟变为负数时再次调用。
延迟由 C-0178 已确认的发放循环按“第几件装备”递增;装备和金币各自使用独立计数器。 因此同一点生成的多件装备虽然逻辑落点重合,视觉上仍会依次飞出。
3.2 初始化步骤
延迟到期后的顺序是:
- 调用
sub_58DEB0整理渲染状态。 - 在
[0,360]内随机初始 yaw,并乘0.017453292转为弧度。 - 通过
vtable+460(1,1)显示模型,调用sub_5808F0附着品质粒子。 - 创建或重置名为
DROP的CPath,清+0x28C,置+0x290=1。 - 向路径加入起点、中点和落点三个控制点;中间弧度带
3.5..4.5的随机抬升/偏转, 每点以-60/+60为角度边界传给sub_675690。 - 调
sub_5F5240向地面扫描并求法线,再经vtable+468/sub_5A51D0贴合斜坡。 - 调
sub_577610对武器类别作额外展示姿态校正,保存起始矩阵到+0x360。 - 播放声音槽 25,即
FALL_SOUND。
默认分支的落点 Y 为地表再抬 1.51;另一分支抬 1.01。若是武器再加 0.05, unit type 188 还会再加 0.25。这些是展示高度,不应回写成 C-0178 的逻辑散落坐标。
3.3 曲线类型
sub_674C40(path, progress) 的展开公式含标准的 0.5 系数和相邻四点组合,是 Catmull–Rom spline 取样。这里没有重力积分、速度碰撞或刚体反弹;看起来像抛物线, 实质是预先构造的样条路径。
4. sub_57D2A0:每帧推进与落地
函数先要求:
+0x290 != 0
+0x288 != nullptr
+0x4AC <= 0之后执行:
progress += dt * 20.0f;
if (progress >= path->total_length) {
progress = path->total_length;
virtual_plus_48(1);
drop_active = false;
play_sound_slot(26); // LAND_SOUND
}
position = path->sample(progress); // sub_674C4020.0f 取自 0x2142624。关键指令:
0x57D2D7 fld [esp+arg_0]
0x57D2DE fmul ds:flt_2142624 ; 20.0
0x57D2E4 fadd [ebx+28Ch]
0x57D2EA fst [ebx+28Ch]
0x57D2F0 fcomp [ecx+24h] ; 路径总长
0x57D32D push 1Ah ; 槽 26
0x57D32F call sub_664F10
0x57D344 mov ecx,[ebx+288h]
0x57D34D call sub_674C40unit type 8 / 21 / 33 / 42 还会按 min(progress / total_length, 1) × 2π 在整段飞行中旋转一周。本文不强行给这四个数补 未证实的正式枚举名;控制流本身已钉死。
5. sub_5808F0:品质粒子选择
5.1 数据覆盖优先级
函数首先读取装备持有的原始 DAT group(CEquipment+0x1B0):
NODROPPARTICLES=true:跳过全部掉落粒子。DROPPARTICLE非空:装入指定 LAYOUT,覆盖默认品质选择。- 否则进入默认四分支。
发行 MEDIA/UNITS 普查结果:NODROPPARTICLES 只有 1 个 DAT;DROPPARTICLE 出现于 31 个 DAT,共 5 个不同路径,其中 27 个使用任务物品的 greenlights 布局。
5.2 默认四分支
if (sub_512D80(item))
index = 23; // QUEST-ITEM
else if (hasUnitType(0x36) || hasUnitType(0xF1))
index = 24; // UNIQUE-ITEM
else
index = 22 - virtual_plus_448(); // true -> 21 MAGIC, false -> 22 GENERIC直接证据:
0x580A54 call sub_512D80
0x580A5D mov edi,17h ; 23
0x580A64 push 36h
0x580A71 push 0F1h
0x580A83 mov eax,[edx+1C0h] ; vtable+448
0x580A94 add edi,16h ; 得 21 或 22
0x580A99 mov edi,18h ; 24主虚表的 +448 指向 sub_57EB60。它不仅检查某个“稀有度整数”,还会看 unit type、 affix/effect 向量并回退到 sub_577DB0;因此最稳妥的移植语义是“需要魔法掉落表现的 装备谓词”,不要简化成 rarity >= rare。
四个索引对应的路径由静态初始化函数 sub_107BE50 直接写死:
0x107C01C MAGIC-ITEM.LAYOUT -> 静态表项 21
0x107C031 GENERIC-ITEM.LAYOUT -> 静态表项 22
0x107C046 QUEST-ITEM.LAYOUT -> 静态表项 23
0x107C05B UNIQUE-ITEM.LAYOUT -> 静态表项 245.3 “光柱”的真实构成
GENERIC-ITEM:XING6.DDS的 Gleam 和 Rising sparkles。没有 Beam,也没有直立 Light。MAGIC-ITEM:GLOWBLUE.DDS的Billboard Up、IS LIGHT=true的 Light,另有蓝色 碎片、BLUEPARTICLE 上升火花与 gleam。QUEST-ITEM:GLOWYELLOW、BLUESTREAK、STAR3、MULTI-SPARKLE CLEAR、FLASH09,再加一个明确名为Beam的 Particle。Beam 使用RAY33.DDS,RENDER TYPE=Billboard Forward Camera、ORIGIN=Bottom Center、HEIGHT=10。UNIQUE-ITEM:STREAK、FRAGMENT_4X4ORANGE、FAST火花、XING6,以及GLOWORANGE.DDS的Billboard UpLightFloor。这是橙色地面亮光,不是同一个 Beam。
6. 三阶段声音
装备 DAT 加载函数 CEquipment__loadFromDat/sub_5923E0 读取三个键:
| DAT 键 | 声音槽 | 装载指令 | 播放时机 |
|---|---|---|---|
FALL_SOUND | 25 | 0x592B31: push 19h | sub_58EB70 飞出开始 |
LAND_SOUND | 26 | 0x592BCD: push 1Ah | sub_57D2A0 路径结束 |
TAKE_SOUND | 27 | 0x592C69: push 1Bh | sub_577830 拾取 |
三处随后均调用 sub_665FB0 将解析出的声音装进 CEquipment+0x1F0 的 sound bank。 实际播放通过 sub_664F10 按槽位、位置与声音配置分派。
有一个容易复刻错的时序:sub_53B110 在把装备加入背包之前就调用 sub_577830(item,0)。若背包已满,物品随后会被放回拾取者脚下,但拾取音已经播放。 所以声音对应“拾取动作被接受并从地面摘除”,不对应“背包最终成功容纳”。
发行 MEDIA/UNITS 有 624 个 DAT 显式写 TAKE_SOUND,共 16 种值;最高频是 ClothGet=279、GemDrop=117、LEATHERGET=69、Pickup=64。ITEMS.DAT 中通用 Pickup 与许多 *Get 最终使用 media/sounds/ui/pickup.wav。
7. 金币表现
金币类使用同样的三声音阶段,但有自己的实现:
sub_5A7D90:播放槽 25,创建DROP路径,附着MEDIA/PARTICLES/DROPS/GOLD_TRAIL.LAYOUT。sub_5A7900:到达落点时播放槽 26。sub_5A78B0:拾取时播放槽 27。
GOLD_TRAIL.LAYOUT 用 MULTIPARTICLE/COINS.DDS,渲染方式 Alpha,粒子寿命 0.15,发射率 3,所以金币飞行时会留下短促硬币拖尾。玩家 BASE.DAT 的 GOLD_SOUND 为 GOLDPICKUP;MEDIA/SOUNDS/ITEMS.DAT 中 GoldPickup 指向 media/sounds/item/drop/dropgold.wav,音量 0.15、频率变化 5000、半径 10。
8. Godot 复刻建议
建议把逻辑落点与表现路径分开:
- 使用 C-0178 算出的世界落点;装备不要另做逻辑散点。
- 按独立装备/金币计数器设置延迟;延迟期间隐藏显示节点。
- 延迟结束时创建三点 Catmull–Rom 路径,以
20 units/s的路径进度推进。 - 终点用碰撞地面法线修正展示姿态,但不要改掉落归属或拾取判据。
- 优先实现
NODROPPARTICLES、DROPPARTICLE;没有覆盖时再跑四分支默认选择。 - 粒子应保留每套 LAYOUT 的多个 emitter 与 billboard 类型,而不是合并成单光柱。
- 在 launch/land/take 三个事件分别发声;保持“背包失败仍已播放 take 音”的原版时序。
9. 验证物与剩余边界
机器可读登记: E:/Torchlight2-Remaster-Project/原版游戏分析/reversed_cpp/REGISTRY/loot_drop_presentation.json。
强制回归门: E:/Torchlight2-Remaster-Project/tests/truth/test_loot_drop_presentation.py。测试会核对发行 EXE 身份、虚表指针、字段与常量指令、四条静态资源路径、粒子布局内容、数据覆盖数量和声音槽。
以下仍在 C-2 范围外,未冒充已解:
unit+0x8A3决定0.25秒 stagger 的来源;NetPickupItemMsg(49)/NetPickupNewItemMsg(50)的完整字段布局;- 掉落物在地面的超时消失规则;
- 飞行旋转所用 unit type
8/21/33/42的全部正式枚举名。
这些边界不影响本文四项表现结论。