fix(client/netproto): align 1.13c monster/NPC mode decoding and prevent initial death state (#551) #553

Closed
troytt wants to merge 0 commits from fix/issue-551-npc-initial-death-mode into main
Owner

Summary

Closes #551

Fixes monsters and camp NPCs (e.g. Rogue Scout act1hire, classId=564, and Charsi classId=154) initially or intermittently rendering in a death/corpse pose (MONMODE_DEATH = 0 / MONMODE_DEAD = 12).

1.13c Ground Truth Verification & Changes

  1. 0xAC AssignNPC Bitstream Mode & Initial Facing (D2Game.dll 0x6fcaca40 / D2Client.dll 0x6fb5e420):
    • In D2Game.dll (0x6fcacac9..0x6fcacb01) and D2Client.dll (0x6fb5e462), the first 4 bits of the 0xAC bitstream at pkt[13] encode the initial monster mode nMode (0=DT, 8=SQ, 9=KB, 12=DD, or 1=NU for all other alive modes), applied via D2Common_11090 (UNITS_ChangeCurrentMode).
    • Initial monster facing direction is seeded deterministically via 0x6ac690c5 in D2CLIENT_InitMonsterUnit (0x6fafd512..0x6fafd59c).
  2. 0x69 / 0x6A / 0x6B State Command Mapping & lifePct Guard (D2Game.dll 0x6fd13620 & 0x6fd1a718 / D2Client.dll 0x6fafdc70 & 0x6fb82e68):
    • Maps pkt[5] (0..29) through the 30-entry MONSTER_CMD_TO_MODE table at D2Client.dll 0x6fb82e68 (0x06 -> 3 GH, 0x07 -> 1 NU, 0x08 -> 0 DT, 0x09 -> 12 DD, 0x0A/0x0B -> 4 A1, 0x0C/0x0D -> 8 SQ, 0x10/0x11 -> 5 A2, 0x04/0x05 -> 7 SC, etc.).
    • Per D2Game.dll 0x6fd13829 and D2Client.dll 0x6fafe16c / 0x6fafe298, pkt[10]/pkt[11] only carries lifePct on 0x06 (GH) or 0x08/0x09 (DT/DD), preventing non-death commands with paramByte = 0 (such as Charsi's 0x6B anvil sequence 0x0C) from zeroing out unit.lifePct.
  3. Canonical Mode Defaults & Movement Transitions (ClientUnit, ClientWorld, OnlineSession, unit-drawables, world-renderer):
    • Alive monsters (lifePct > 0) and players default to mode = 1 (NU) instead of 0 (DT), while dead monsters (lifePct <= 0) default to 12 (DD).
    • UnitMove (0x67/0x68/0x6D/0x0F/0x0D) and OnlineSession.tick() / buildRenderableUnits() synchronize movement modes (2 walk, 15/3 run, 9/19 knockback) and return to 1 (NU) upon stopping.

Verification

  • npx vitest run tests/netproto/d2gs-s2c.test.ts tests/client/client-world.test.ts tests/client/unit-facing-motion.test.ts tests/client/drlg-map-motion.test.ts tests/client/spawn-position-539.test.ts
  • npx tsc --noEmit
## Summary Closes #551 Fixes monsters and camp NPCs (e.g. Rogue Scout `act1hire`, `classId=564`, and Charsi `classId=154`) initially or intermittently rendering in a death/corpse pose (`MONMODE_DEATH = 0` / `MONMODE_DEAD = 12`). ## 1.13c Ground Truth Verification & Changes 1. **`0xAC AssignNPC` Bitstream Mode & Initial Facing (`D2Game.dll` `0x6fcaca40` / `D2Client.dll` `0x6fb5e420`)**: - In `D2Game.dll` (`0x6fcacac9..0x6fcacb01`) and `D2Client.dll` (`0x6fb5e462`), the first 4 bits of the `0xAC` bitstream at `pkt[13]` encode the initial monster mode `nMode` (`0=DT`, `8=SQ`, `9=KB`, `12=DD`, or `1=NU` for all other alive modes), applied via `D2Common_11090` (`UNITS_ChangeCurrentMode`). - Initial monster facing direction is seeded deterministically via `0x6ac690c5` in `D2CLIENT_InitMonsterUnit` (`0x6fafd512..0x6fafd59c`). 2. **`0x69` / `0x6A` / `0x6B` State Command Mapping & `lifePct` Guard (`D2Game.dll` `0x6fd13620` & `0x6fd1a718` / `D2Client.dll` `0x6fafdc70` & `0x6fb82e68`)**: - Maps `pkt[5]` (`0..29`) through the 30-entry `MONSTER_CMD_TO_MODE` table at `D2Client.dll` `0x6fb82e68` (`0x06 -> 3 GH`, `0x07 -> 1 NU`, `0x08 -> 0 DT`, `0x09 -> 12 DD`, `0x0A/0x0B -> 4 A1`, `0x0C/0x0D -> 8 SQ`, `0x10/0x11 -> 5 A2`, `0x04/0x05 -> 7 SC`, etc.). - Per `D2Game.dll` `0x6fd13829` and `D2Client.dll` `0x6fafe16c` / `0x6fafe298`, `pkt[10]`/`pkt[11]` only carries `lifePct` on `0x06` (`GH`) or `0x08`/`0x09` (`DT`/`DD`), preventing non-death commands with `paramByte = 0` (such as Charsi's `0x6B` anvil sequence `0x0C`) from zeroing out `unit.lifePct`. 3. **Canonical Mode Defaults & Movement Transitions (`ClientUnit`, `ClientWorld`, `OnlineSession`, `unit-drawables`, `world-renderer`)**: - Alive monsters (`lifePct > 0`) and players default to `mode = 1` (`NU`) instead of `0` (`DT`), while dead monsters (`lifePct <= 0`) default to `12` (`DD`). - `UnitMove` (`0x67`/`0x68`/`0x6D`/`0x0F`/`0x0D`) and `OnlineSession.tick()` / `buildRenderableUnits()` synchronize movement modes (`2` walk, `15`/`3` run, `9`/`19` knockback) and return to `1` (`NU`) upon stopping. ## Verification - `npx vitest run tests/netproto/d2gs-s2c.test.ts tests/client/client-world.test.ts tests/client/unit-facing-motion.test.ts tests/client/drlg-map-motion.test.ts tests/client/spawn-position-539.test.ts` - `npx tsc --noEmit`
troytt added 1 commit 2026-10-02 08:22:05 +00:00
59435527b0 fix(client/netproto): align 1.13c monster/NPC mode decoding and prevent initial death state (#551)
- Decode the initial 4-bit bitstream in 0xAC AssignNPC as monster mode nMode (D2Game.dll 0x6fcacac9, D2Client.dll 0x6fb5e462) and seed initial 1.13c facing via 0x6ac690c5 (D2Client.dll 0x6fafd512)
- Map 0x69/0x6A/0x6B state command byte pkt[5] via 1.13c D2Client.dll 0x6fb82e68 (MONSTER_CMD_TO_MODE, inverse of D2Game.dll 0x6fd1a718) and only apply pkt[10]/pkt[11] as lifePct on 0x06 (GH) or 0x08/0x09 (DT/DD) so non-death commands (e.g. Charsi 0x6B anvil sequence 0x0C) never zero out lifePct
- Default alive monsters and players to mode=1 (NU) instead of mode=0 (DT) across ClientUnit, ClientWorld, OnlineSession, buildSceneDrawables, and drawUnitSprite
- Transition monster/player modes across UnitMove and OnlineSession interpolator ticks (walk=2, run=15/3, knockback=9/19, stop=1)
- Add 1.13c unit and live .d2cap regression tests

Closes #551
troytt force-pushed fix/issue-551-npc-initial-death-mode from 59435527b0 to 45aa95ce2d 2026-10-02 08:33:02 +00:00 Compare
troytt closed this pull request 2026-10-02 08:33:59 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
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#553
No description provided.