《火炬之光 2》掉落物表现层逆向报告

对应 HANDOFF_RE_BACKLOG.md 的 C-2 / M5-1。
发行程序:E:/Torchlight 2/Torchlight2.exe,imagebase 0x400000,SHA-256 186472C3057B38F4CDFF4696959A943C396AE6166995F7418997B5EA853E8A5E
本文只补 C-0178/0179 没有进入的表现层:飞出动画、所谓“光柱”、品质特效、拾取音。

结论

原版装备掉落不是刚体抛射。引擎先按逐件延迟隐藏模型,延迟到期后由 sub_58EB70 建立一条名为 DROP 的三控制点 CPathsub_57D2A0 每帧把路径进度 增加 dt × 20,通过 Catmull–Rom 曲线取样位置,到达路径总长时播放落地音并结束。

品质表现也不是“一根光柱按稀有度换色”。sub_5808F0 在四套完整粒子布局之间选择:

选择资源主要可见成分
魔法/带附魔效果MAGIC-ITEM.LAYOUT蓝色直立 Light、蓝色碎片、火花、gleam
普通GENERIC-ITEM.LAYOUTgleam 与上升火花;没有光柱
任务物品QUEST-ITEM.LAYOUT黄色光、尖刺、爆闪、火花和明确命名的 Beam
特殊/唯一类别UNIQUE-ITEM.LAYOUT橙色碎片、橙色 LightFloor、火花、gleam

数据还可以用 NODROPPARTICLES 完全禁用掉落粒子,或用 DROPPARTICLE 指定任意 LAYOUT 覆盖默认选择。因此复刻时应实例化原始粒子布局组合,不能只做一个按 rarity 改颜色的统一光柱材质。

声音分成三个明确阶段:FALL_SOUNDLAND_SOUNDTAKE_SOUND 分别装入声音库槽 25、26、27;飞出开始、路径结束、拾取时各触发一次。

1. 先订正 C-0178 的两个旧标签

CEquipment 主虚表起点是 0x2174864。直接读取指针得到:

虚表偏移指针位置目标实际职责
+4600x2174A30sub_5A4C00模型/部件显隐切换
+4680x2174A38sub_5A51D0按地面法线调整模型朝向
+5320x2174A78sub_58EB70完整装备掉落表现初始化

对应表字节从 0x2174A30 开始为:

text
00 4C 5A 00  30 D4 57 00  D0 51 5A 00  ...  70 EB 58 00

所以旧记录中的“+460 是落地弹跳、+532 只负责延迟显形”都不准确。+460 只是切换可见部件;+532 本身就是后文完整的路径、姿态、粒子和飞出音初始化函数。

2. 延迟与飞出状态

与表现直接相关的 CEquipment 字段如下:

偏移类型用途指令证据
+0x4ACfloat逐件掉落延迟0x583017: fcom [esi+4ACh]0x583032: fst [esi+4ACh]
+0x288CPath*DROP 路径0x57D2B6: mov ecx,[ebx+288h]
+0x28Cfloat路径进度0x57D2E4: fadd [ebx+28Ch]
+0x290byte飞行/延迟状态0x58300C: cmp byte ptr [esi+290h],0
+0x360Matrix4旋转所用起始姿态0x57D3F3: lea ecx,[ebx+360h]
+0x43C粒子实例指针掉落品质粒子0x580A9E: mov ecx,[esi+43Ch]
+0x1F0声音库25/26/27 三声音槽0x592B29: mov ecx,[esi+1F0h]

sub_582FF0 是逐帧外层更新。其关键控制流是:

cpp
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 的原始指令为:

text
0x583041  8B 82 14 02 00 00    mov eax,[edx+214h]
0x583047  FF D0                call eax

0x214 即十进制 532。延迟尚未结束时则从 [edx+0x1CC]+460,传入 (0,1)

3. sub_58EB70:装备掉落初始化

3.1 延迟分支

+0x4AC > 0,函数只调用 vtable+460(0,1) 隐藏模型,将 +0x290 置为 1, 然后返回。真正的飞出初始化由上一节的逐帧函数在延迟变为负数时再次调用。

延迟由 C-0178 已确认的发放循环按“第几件装备”递增;装备和金币各自使用独立计数器。 因此同一点生成的多件装备虽然逻辑落点重合,视觉上仍会依次飞出。

3.2 初始化步骤

延迟到期后的顺序是:

  1. 调用 sub_58DEB0 整理渲染状态。
  2. [0,360] 内随机初始 yaw,并乘 0.017453292 转为弧度。
  3. 通过 vtable+460(1,1) 显示模型,调用 sub_5808F0 附着品质粒子。
  4. 创建或重置名为 DROPCPath,清 +0x28C,置 +0x290=1
  5. 向路径加入起点、中点和落点三个控制点;中间弧度带 3.5..4.5 的随机抬升/偏转, 每点以 -60/+60 为角度边界传给 sub_675690
  6. sub_5F5240 向地面扫描并求法线,再经 vtable+468/sub_5A51D0 贴合斜坡。
  7. sub_577610 对武器类别作额外展示姿态校正,保存起始矩阵到 +0x360
  8. 播放声音槽 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:每帧推进与落地

函数先要求:

text
+0x290 != 0
+0x288 != nullptr
+0x4AC <= 0

之后执行:

cpp
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_674C40

20.0f 取自 0x2142624。关键指令:

text
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_674C40

unit type 8 / 21 / 33 / 42 还会按 min(progress / total_length, 1) × 2π 在整段飞行中旋转一周。本文不强行给这四个数补 未证实的正式枚举名;控制流本身已钉死。

5. sub_5808F0:品质粒子选择

5.1 数据覆盖优先级

函数首先读取装备持有的原始 DAT group(CEquipment+0x1B0):

  1. NODROPPARTICLES=true:跳过全部掉落粒子。
  2. DROPPARTICLE 非空:装入指定 LAYOUT,覆盖默认品质选择。
  3. 否则进入默认四分支。

发行 MEDIA/UNITS 普查结果:NODROPPARTICLES 只有 1 个 DAT;DROPPARTICLE 出现于 31 个 DAT,共 5 个不同路径,其中 27 个使用任务物品的 greenlights 布局。

5.2 默认四分支

cpp
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

直接证据:

text
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 直接写死:

text
0x107C01C  MAGIC-ITEM.LAYOUT     -> 静态表项 21
0x107C031  GENERIC-ITEM.LAYOUT   -> 静态表项 22
0x107C046  QUEST-ITEM.LAYOUT     -> 静态表项 23
0x107C05B  UNIQUE-ITEM.LAYOUT    -> 静态表项 24

5.3 “光柱”的真实构成

  • GENERIC-ITEMXING6.DDS 的 Gleam 和 Rising sparkles。没有 Beam,也没有直立 Light。
  • MAGIC-ITEMGLOWBLUE.DDSBillboard UpIS LIGHT=true 的 Light,另有蓝色 碎片、BLUEPARTICLE 上升火花与 gleam。
  • QUEST-ITEMGLOWYELLOWBLUESTREAKSTAR3MULTI-SPARKLE CLEARFLASH09,再加一个明确名为 Beam 的 Particle。Beam 使用 RAY33.DDSRENDER TYPE=Billboard Forward CameraORIGIN=Bottom CenterHEIGHT=10
  • UNIQUE-ITEMSTREAKFRAGMENT_4X4ORANGEFAST 火花、XING6,以及 GLOWORANGE.DDSBillboard Up LightFloor。这是橙色地面亮光,不是同一个 Beam。

6. 三阶段声音

装备 DAT 加载函数 CEquipment__loadFromDat/sub_5923E0 读取三个键:

DAT 键声音槽装载指令播放时机
FALL_SOUND250x592B31: push 19hsub_58EB70 飞出开始
LAND_SOUND260x592BCD: push 1Ahsub_57D2A0 路径结束
TAKE_SOUND270x592C69: push 1Bhsub_577830 拾取

三处随后均调用 sub_665FB0 将解析出的声音装进 CEquipment+0x1F0 的 sound bank。 实际播放通过 sub_664F10 按槽位、位置与声音配置分派。

有一个容易复刻错的时序:sub_53B110 在把装备加入背包之前就调用 sub_577830(item,0)。若背包已满,物品随后会被放回拾取者脚下,但拾取音已经播放。 所以声音对应“拾取动作被接受并从地面摘除”,不对应“背包最终成功容纳”。

发行 MEDIA/UNITS 有 624 个 DAT 显式写 TAKE_SOUND,共 16 种值;最高频是 ClothGet=279GemDrop=117LEATHERGET=69Pickup=64ITEMS.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.LAYOUTMULTIPARTICLE/COINS.DDS,渲染方式 Alpha,粒子寿命 0.15,发射率 3,所以金币飞行时会留下短促硬币拖尾。玩家 BASE.DATGOLD_SOUNDGOLDPICKUPMEDIA/SOUNDS/ITEMS.DATGoldPickup 指向 media/sounds/item/drop/dropgold.wav,音量 0.15、频率变化 5000、半径 10

8. Godot 复刻建议

建议把逻辑落点与表现路径分开:

  1. 使用 C-0178 算出的世界落点;装备不要另做逻辑散点。
  2. 按独立装备/金币计数器设置延迟;延迟期间隐藏显示节点。
  3. 延迟结束时创建三点 Catmull–Rom 路径,以 20 units/s 的路径进度推进。
  4. 终点用碰撞地面法线修正展示姿态,但不要改掉落归属或拾取判据。
  5. 优先实现 NODROPPARTICLESDROPPARTICLE;没有覆盖时再跑四分支默认选择。
  6. 粒子应保留每套 LAYOUT 的多个 emitter 与 billboard 类型,而不是合并成单光柱。
  7. 在 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 的全部正式枚举名。

这些边界不影响本文四项表现结论。