fix(client/netproto): 部分怪物与营地NPC(如罗格斥候)初始渲染为死亡/尸体状态 #551

Closed
opened 2026-10-02 07:44:13 +00:00 by troytt · 0 comments
Owner

问题描述

在联机 D2GS 会话中,部分营地 NPC(例如罗格营地的罗格斥候 rogue1, hcIdx = 152, token RG)以及野外新刷出的怪物,在初始生成(0xAC AssignNPC)时直接渲染为倒在血泊中的死亡尸体画面(DT / DD 最后一帧),且被判定为 isCorpse = true(在小地图隐藏、无法点击交互)。

参考截图:https://screenshot-v2.corp.google.com/5rnl53i18d5pg

1.13c 权威反汇编根因分析 (D2Game.dll & D2Client.dll)

  1. 1.13c 动作模式枚举 (MonMode.txt / PlrMode.txt vs ObjMode.txt):

    • 怪物 (MonMode.txt, unitType = 1):0 = DT (Death 死亡动画), 1 = NU (Neutral 站立待机), 2 = WL (Walk 行走), 3 = GH (Get Hit 受击), 12 = DD (Dead 尸体), 15 = RN (Run 奔跑)。
    • 玩家 (PlrMode.txt, unitType = 0):0 = DT (Death), 1 = NU (Neutral), 2 = WL, 3 = RN, 5 = TN (Town Neutral), 6 = TW (Town Walk), 17 = DD (Dead)。
    • 仅场景物件 (ObjMode.txt, unitType = 2) 使用 0 = NU。对于玩家与怪物,0 是死亡 (DT),1 才是待机 (NU)(验证于 D2Client.dll @ 0x6fafdca4..0x6fafddb4)。
  2. 0xAC AssignNPC 首个 4-bit nMode 字段被丢弃 (src/netproto/d2gs/s2c/units.ts):

    • 服务端 D2Game.dll @ 0x6fcacac9..0x6fcacb01 在构造 0xAC 比特流 (pkt[13..]) 时,通过 FOG_WriteBits(&bitStream, mode, 4) 写入的首个 4-bit 字段是怪物的初始动作模式 nMode(若处于 8=SQ、9=KB、12=DD、0=DT 则原样写入,其余所有存活状态一律写入 1 = MONMODE_NEUTRAL)。
    • 客户端 D2Client.dll @ 0x6fb5e462..0x6fb5e469 -> 0x6fb5787c -> 0x6fafd345 读取该 4-bit nMode 并调用 D2Common_11090 (UNITS_ChangeCurrentMode)。
    • 现有 decode0xACAssignNpc 将 bitReader.readBits(4) 误注为 // initial facing direction 并丢弃返回值,未在 UnitSnapshot 上设置 mode。
  3. ClientUnit 默认 mode = 0 (DT) 导致存活怪物被判定为尸体 (isCorpse = true):

    • ClientUnit 构造函数无条件执行 this.mode = snapshot.mode ?? 0,导致所有新生成的怪物/NPC 和玩家默认 mode = 0 (DT 死亡)。
    • OnlineSession.buildRenderableUnits (src/client/session/online-session.ts:1438) 计算 const isCorpse = u.unitType === 'monster' && (u.mode === 0 || u.mode === 12 || u.lifePct <= 0) 时恒为 true。
    • WorldRenderer.drawSceneUnitDrawable 针对 isCorpse = true 强制选取 dd/dt 动作并冻结在最后一帧(frameCount - 1)。像营地罗格(token RG,与野外黑暗罗格共用含 DT/DD 的 DCC/COF 资源)及野外怪物因此直接显示为血泊中的尸体;阿卡拉/恰西等无 DT/DD COF 的城镇商人虽回退到 nu,但动画被冻结在末帧、小地图标记被跳过、鼠标拾取被过滤。
  4. 0x69 SetNPCMode / 0x6A / 0x6B 状态命令索引表 (D2Client.dll 0x6fb82e68) 与移动模式切换 (UnitMove):

    • D2Game.dll (0x6fd13620, 表 0x6fd1a718) 与 D2Client.dll (0x6fb5f390 -> 0x6fafdc70, 表 0x6fb82e68) 中,0x69/0x6A/0x6B 的 pkt[5] 是 0..29 的状态命令索引(例如 0x06 -> 3 GH, 0x07 -> 1 NU, 0x08 -> 0 DT, 0x09 -> 12 DD, 0x0A/0x0B -> 4 A1),而非直接的 MonMode.txt ID。
    • 0x67 NPCMove / 0x68 NPCMoveToTarget / 0x6D NPCStop 触发 UnitMove 及 RemoteEntityInterpolator 插值起止时,需同步将存活怪物的 mode 在 2 (WL) / 15 (RN) 与 1 (NU) 之间切换。

修复方案

  • 在 decode0xACAssignNpc 中解析首个 4-bit nMode(对 lifePct > 0 的存活单位保证非死亡模式 1 = NU,对 lifePct <= 0 的死亡单位设为 0/12),在 decode0x59AssignPlayer 中设置初始 mode: 1。
  • 在 decode0x69SetNpcMode、decode0x6ANpcAction、decode0x6BNpcAction2 中按 D2Client.dll 0x6fb82e68 权威映射表转换 pkt[5],并补齐 0x6A/0x6B 的 lifePct 与坐标字段。
  • 在 ClientUnit、ClientWorld、OnlineSession、unit-drawables.ts、world-renderer.ts 中将玩家/怪物的默认待机 mode 统一为 1 (NU),并在 UnitMove 与插值器停止时同步更新行走/奔跑/待机模式。
## 问题描述 在联机 D2GS 会话中,部分营地 NPC(例如罗格营地的罗格斥候 `rogue1`, `hcIdx = 152`, token `RG`)以及野外新刷出的怪物,在初始生成(`0xAC AssignNPC`)时直接渲染为倒在血泊中的死亡尸体画面(`DT` / `DD` 最后一帧),且被判定为 `isCorpse = true`(在小地图隐藏、无法点击交互)。 参考截图:https://screenshot-v2.corp.google.com/5rnl53i18d5pg ## 1.13c 权威反汇编根因分析 (`D2Game.dll` & `D2Client.dll`) 1. **1.13c 动作模式枚举 (`MonMode.txt` / `PlrMode.txt` vs `ObjMode.txt`)**: - 怪物 (`MonMode.txt`, `unitType = 1`):`0 = DT` (Death 死亡动画), `1 = NU` (Neutral 站立待机), `2 = WL` (Walk 行走), `3 = GH` (Get Hit 受击), `12 = DD` (Dead 尸体), `15 = RN` (Run 奔跑)。 - 玩家 (`PlrMode.txt`, `unitType = 0`):`0 = DT` (Death), `1 = NU` (Neutral), `2 = WL`, `3 = RN`, `5 = TN` (Town Neutral), `6 = TW` (Town Walk), `17 = DD` (Dead)。 - 仅场景物件 (`ObjMode.txt`, `unitType = 2`) 使用 `0 = NU`。对于玩家与怪物,**`0` 是死亡 (`DT`),`1` 才是待机 (`NU`)**(验证于 `D2Client.dll` @ `0x6fafdca4..0x6fafddb4`)。 2. **`0xAC AssignNPC` 首个 4-bit `nMode` 字段被丢弃 (`src/netproto/d2gs/s2c/units.ts`)**: - 服务端 `D2Game.dll` @ `0x6fcacac9..0x6fcacb01` 在构造 `0xAC` 比特流 (`pkt[13..]`) 时,通过 `FOG_WriteBits(&bitStream, mode, 4)` 写入的首个 4-bit 字段是怪物的初始动作模式 `nMode`(若处于 `8=SQ`、`9=KB`、`12=DD`、`0=DT` 则原样写入,其余所有存活状态一律写入 `1 = MONMODE_NEUTRAL`)。 - 客户端 `D2Client.dll` @ `0x6fb5e462..0x6fb5e469` -> `0x6fb5787c` -> `0x6fafd345` 读取该 4-bit `nMode` 并调用 `D2Common_11090` (`UNITS_ChangeCurrentMode`)。 - 现有 `decode0xACAssignNpc` 将 `bitReader.readBits(4)` 误注为 `// initial facing direction` 并丢弃返回值,未在 `UnitSnapshot` 上设置 `mode`。 3. **`ClientUnit` 默认 `mode = 0` (`DT`) 导致存活怪物被判定为尸体 (`isCorpse = true`)**: - `ClientUnit` 构造函数无条件执行 `this.mode = snapshot.mode ?? 0`,导致所有新生成的怪物/NPC 和玩家默认 `mode = 0` (`DT` 死亡)。 - `OnlineSession.buildRenderableUnits` (`src/client/session/online-session.ts:1438`) 计算 `const isCorpse = u.unitType === 'monster' && (u.mode === 0 || u.mode === 12 || u.lifePct <= 0)` 时恒为 `true`。 - `WorldRenderer.drawSceneUnitDrawable` 针对 `isCorpse = true` 强制选取 `dd`/`dt` 动作并冻结在最后一帧(`frameCount - 1`)。像营地罗格(token `RG`,与野外黑暗罗格共用含 `DT`/`DD` 的 DCC/COF 资源)及野外怪物因此直接显示为血泊中的尸体;阿卡拉/恰西等无 `DT`/`DD` COF 的城镇商人虽回退到 `nu`,但动画被冻结在末帧、小地图标记被跳过、鼠标拾取被过滤。 4. **`0x69 SetNPCMode` / `0x6A` / `0x6B` 状态命令索引表 (`D2Client.dll` `0x6fb82e68`) 与移动模式切换 (`UnitMove`)**: - `D2Game.dll` (`0x6fd13620`, 表 `0x6fd1a718`) 与 `D2Client.dll` (`0x6fb5f390` -> `0x6fafdc70`, 表 `0x6fb82e68`) 中,`0x69`/`0x6A`/`0x6B` 的 `pkt[5]` 是 `0..29` 的状态命令索引(例如 `0x06 -> 3 GH`, `0x07 -> 1 NU`, `0x08 -> 0 DT`, `0x09 -> 12 DD`, `0x0A/0x0B -> 4 A1`),而非直接的 `MonMode.txt` ID。 - `0x67 NPCMove` / `0x68 NPCMoveToTarget` / `0x6D NPCStop` 触发 `UnitMove` 及 `RemoteEntityInterpolator` 插值起止时,需同步将存活怪物的 `mode` 在 `2 (WL)` / `15 (RN)` 与 `1 (NU)` 之间切换。 ## 修复方案 - 在 `decode0xACAssignNpc` 中解析首个 4-bit `nMode`(对 `lifePct > 0` 的存活单位保证非死亡模式 `1 = NU`,对 `lifePct <= 0` 的死亡单位设为 `0`/`12`),在 `decode0x59AssignPlayer` 中设置初始 `mode: 1`。 - 在 `decode0x69SetNpcMode`、`decode0x6ANpcAction`、`decode0x6BNpcAction2` 中按 `D2Client.dll` `0x6fb82e68` 权威映射表转换 `pkt[5]`,并补齐 `0x6A`/`0x6B` 的 `lifePct` 与坐标字段。 - 在 `ClientUnit`、`ClientWorld`、`OnlineSession`、`unit-drawables.ts`、`world-renderer.ts` 中将玩家/怪物的默认待机 `mode` 统一为 `1 (NU)`,并在 `UnitMove` 与插值器停止时同步更新行走/奔跑/待机模式。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#551
No description provided.