359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
/**
|
|
* D2GS S->C Unit Assign, Remove, and Mode Decoders (1.13c).
|
|
* Verified against D2Client.dll 0x6fb5df70 (0x09), 0x6fb5ca40 (0x0A), 0x6fb5f5f0 (0x0C),
|
|
* 0x6fb5f560 (0x0E), 0x6fb5daa0 (0x11), 0x6fb5df40 (0x51), 0x6fb5df20 (0x59),
|
|
* 0x6fb5f390 (0x69), and 0x6fb5e420 (0xAC).
|
|
*/
|
|
|
|
import { BitReader } from '../../core/bit-reader.ts'
|
|
import { ByteReader } from '../../core/byte-reader.ts'
|
|
import type { UnitType } from '../../domain/ids.ts'
|
|
import type { ServerEvent } from '../../domain/server-event.ts'
|
|
|
|
function clampUnitType(raw: number): UnitType {
|
|
return (raw >= 0 && raw <= 5 ? raw : 0) as UnitType
|
|
}
|
|
|
|
/** 0x09 AssignWarp (11B, D2Client.dll 0x6fb5df70) */
|
|
export function decode0x09AssignWarp(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x09
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
const warpId = r.u8()
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
return {
|
|
type: 'UnitAssign',
|
|
unit: {
|
|
unitType,
|
|
id,
|
|
classId: warpId,
|
|
warpId,
|
|
x,
|
|
y,
|
|
},
|
|
}
|
|
}
|
|
|
|
/** 0x0A RemoveGroundUnit (6B, D2Client.dll 0x6fb5ca40) */
|
|
export function decode0x0ARemoveUnit(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x0A
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
return {
|
|
type: 'UnitRemove',
|
|
unitType,
|
|
id,
|
|
}
|
|
}
|
|
|
|
/** 0x0C NPCGetHit (9B, D2Client.dll 0x6fb5f5f0) */
|
|
export function decode0x0CNpcGetHit(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x0C
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
const mode = r.u16LE()
|
|
const lifePct = r.u8()
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType,
|
|
id,
|
|
mode,
|
|
lifePct,
|
|
}
|
|
}
|
|
|
|
/** 0x0E SetObjectState (12B, D2Client.dll 0x6fb5f560) */
|
|
export function decode0x0ESetObjectState(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x0E
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
r.u8() // portalFlags
|
|
r.u8() // canChangeBack
|
|
const mode = r.u32LE()
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType,
|
|
id,
|
|
mode,
|
|
}
|
|
}
|
|
|
|
/** 0x11 ReportKill (8B, D2Client.dll 0x6fb5daa0) */
|
|
export function decode0x11ReportKill(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x11
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
r.u16LE()
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType,
|
|
id,
|
|
mode: unitType === 0 ? 17 : resolveMonsterCmdMode(0x09),
|
|
lifePct: 0,
|
|
}
|
|
}
|
|
|
|
/** 0x51 AssignObject (14B, D2Client.dll 0x6fb5df40) */
|
|
export function decode0x51AssignObject(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x51
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
const classId = r.u16LE()
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
const mode = r.u8()
|
|
const flags = r.u8()
|
|
return {
|
|
type: 'UnitAssign',
|
|
unit: {
|
|
unitType,
|
|
id,
|
|
classId,
|
|
x,
|
|
y,
|
|
mode,
|
|
flags,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 1.13c D2Client.dll `0x6fb82e68` (`30` entries) mapping monster state-command byte (`pkt[5]` in
|
|
* `0x67`, `0x68`, `0x69`, `0x6A`, `0x6B`) to canonical `MonMode.txt` ID (`0..16`).
|
|
* Inverse of D2Game.dll `0x6fd1a718` (`g_aMonModePacketTable`).
|
|
*/
|
|
export const MONSTER_CMD_TO_MODE: readonly number[] = [
|
|
2, // 0x00: WL (0x68 walk)
|
|
2, // 0x01: WL (0x67 walk)
|
|
16, // 0x02: special
|
|
16, // 0x03: special
|
|
7, // 0x04: SC (Skill1 / Cast no target)
|
|
7, // 0x05: SC (Skill1 / Cast with target)
|
|
3, // 0x06: GH (Get Hit)
|
|
1, // 0x07: NU (Neutral / 0x6D stop)
|
|
0, // 0x08: DT (Death animation)
|
|
12, // 0x09: DD (Dead corpse)
|
|
4, // 0x0A: A1 (Attack 1 with target)
|
|
4, // 0x0B: A1 (Attack 1 no target)
|
|
8, // 0x0C: SQ (Sequence no target)
|
|
8, // 0x0D: SQ (Sequence with target)
|
|
9, // 0x0E: KB (Knockback no target)
|
|
9, // 0x0F: KB (Knockback with target)
|
|
5, // 0x10: A2 (Attack 2 with target)
|
|
5, // 0x11: A2 (Attack 2 no target)
|
|
6, // 0x12: BL (Block)
|
|
16, // 0x13: special
|
|
13, // 0x14: S2 (Skill 2)
|
|
14, // 0x15: S3 (Skill 3)
|
|
14, // 0x16: S3 (Skill 3)
|
|
15, // 0x17: RN (0x67 run)
|
|
15, // 0x18: RN (0x68 run)
|
|
8, // 0x19: SQ (Sequence)
|
|
10, // 0x1A: S1 (no target)
|
|
10, // 0x1B: S1 (with target)
|
|
11, // 0x1C: S2 (no target)
|
|
11, // 0x1D: S2 (with target)
|
|
]
|
|
|
|
function resolveMonsterCmdMode(rawCmd: number): number {
|
|
const mapped = MONSTER_CMD_TO_MODE[rawCmd]
|
|
return mapped !== undefined ? mapped : rawCmd
|
|
}
|
|
|
|
/**
|
|
* In 1.13c (`D2Game.dll 0x6fd13620` & `D2Client.dll 0x6fafdc70`):
|
|
* - `pkt[10]` (`0x69`/`0x6B`) / `pkt[11]` (`0x6A`) carries `lifePct & 0x7f` ONLY when `rawCmd === 0x06` (`MONMODE_GETHIT`, `0x6fd13829` / `0x6fafe16c`).
|
|
* - When `rawCmd === 0x08` (`MONMODE_DEATH`) or `0x09` (`MONMODE_DEAD`), `D2Client.dll` (`0x6fafe298`) sets HP to `0`.
|
|
* - For all other commands (e.g. `0x0C` `MONMODE_SEQUENCE` when Charsi hammers the anvil), the byte is `0` on the wire and must NOT overwrite `unit.lifePct` with `0`.
|
|
*/
|
|
function resolveMonsterCmdLifePct(rawCmd: number, paramByte: number): number | undefined {
|
|
if (rawCmd === 0x08 || rawCmd === 0x09) {
|
|
return 0
|
|
}
|
|
if (rawCmd === 0x06) {
|
|
if (paramByte === 0) return undefined
|
|
const masked = paramByte & 0x7f
|
|
return masked > 0 ? masked : 128
|
|
}
|
|
return paramByte > 0 && paramByte <= 0x80 ? paramByte : undefined
|
|
}
|
|
|
|
/** 0x59 AssignPlayer (26B, D2Client.dll 0x6fb5df20 -> 0x6fac9275) */
|
|
export function decode0x59AssignPlayer(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x59
|
|
const id = r.u32LE()
|
|
const classId = r.u8()
|
|
const name = r.fixedCString(16)
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
return {
|
|
type: 'UnitAssign',
|
|
unit: {
|
|
unitType: 0,
|
|
id,
|
|
classId,
|
|
name,
|
|
x,
|
|
y,
|
|
mode: 1,
|
|
},
|
|
isSelf: x !== 0 || y !== 0,
|
|
}
|
|
}
|
|
|
|
/** 0x69 SetNPCMode (12B, D2Client.dll 0x6fb5f390 -> 0x6fafdc70) */
|
|
export function decode0x69SetNpcMode(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x69
|
|
const id = r.u32LE()
|
|
const rawCmd = r.u8()
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
const paramByte = r.u8()
|
|
r.u8() // animSpeed
|
|
const lifePct = resolveMonsterCmdLifePct(rawCmd, paramByte)
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType: 1,
|
|
id,
|
|
mode: resolveMonsterCmdMode(rawCmd),
|
|
at: { x, y },
|
|
...(lifePct !== undefined ? { lifePct } : {}),
|
|
}
|
|
}
|
|
|
|
/** 0x6A NPCAction (12B, D2Client.dll 0x6fb5f340 -> 0x6fafdc70) */
|
|
export function decode0x6ANpcAction(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x6A
|
|
const id = r.u32LE()
|
|
const rawCmd = r.u8()
|
|
r.u8() // targetType
|
|
r.u32LE() // targetId
|
|
const paramByte = r.u8()
|
|
const lifePct = resolveMonsterCmdLifePct(rawCmd, paramByte)
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType: 1,
|
|
id,
|
|
mode: resolveMonsterCmdMode(rawCmd),
|
|
...(lifePct !== undefined ? { lifePct } : {}),
|
|
}
|
|
}
|
|
|
|
/** 0x6B NPCAction2 (16B, D2Client.dll 0x6fb5f7c0 -> 0x6fb5f390 -> 0x6fafdc70) */
|
|
export function decode0x6BNpcAction2(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x6B
|
|
const id = r.u32LE()
|
|
const rawCmd = r.u8()
|
|
r.u16LE() // targetX
|
|
r.u16LE() // targetY
|
|
const paramByte = r.u8()
|
|
r.u8() // animSpeed
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
const lifePct = resolveMonsterCmdLifePct(rawCmd, paramByte)
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType: 1,
|
|
id,
|
|
mode: resolveMonsterCmdMode(rawCmd),
|
|
at: { x, y },
|
|
...(lifePct !== undefined ? { lifePct } : {}),
|
|
}
|
|
}
|
|
|
|
/** 0x76 PlayerInSight (6B, D2Client.dll 0x6fb5d370) */
|
|
export function decode0x76UnitInSight(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0x76
|
|
const unitType = clampUnitType(r.u8())
|
|
const id = r.u32LE()
|
|
return {
|
|
type: 'UnitMode',
|
|
unitType,
|
|
id,
|
|
mode: 1,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 0xAC AssignNPC (variable pkt[12], D2Game.dll 0x6fcaca40 -> D2Client.dll 0x6fb5e420).
|
|
*
|
|
* Bitstream layout starting at offset 13 (`0x6fb5e462`):
|
|
* - 4 bits: initial monster mode `nMode` (`D2Game.dll 0x6fcacac9`: `0=DT`, `8=SQ`, `9=KB`, `12=DD`,
|
|
* or `1=NU` for all other alive modes; applied via `D2Common_11090` at `0x6fb5787c`/`0x6fafd345`)
|
|
* - 1 bit: `hasAnimComp` (`0x6fb5e47f`)
|
|
* - 1 bit: `hasMonsterFlags` (`0x6fb5e4fc`, followed by 5 boss flag bits when `hasAnimComp === 0`)
|
|
*/
|
|
export function decode0xACAssignNpc(pkt: Uint8Array): ServerEvent {
|
|
const r = new ByteReader(pkt)
|
|
r.u8() // 0xAC
|
|
const id = r.u32LE()
|
|
const classId = r.u16LE()
|
|
const x = r.u16LE()
|
|
const y = r.u16LE()
|
|
const lifePct = r.u8()
|
|
const packetLen = r.u8()
|
|
|
|
let rawMode: number | undefined
|
|
let flags = 0
|
|
if (packetLen > 13 && pkt.byteLength >= 13) {
|
|
const bitReader = new BitReader(pkt.subarray(13, Math.min(pkt.byteLength, packetLen)))
|
|
if (bitReader.remainingBits >= 4) {
|
|
rawMode = bitReader.readBits(4) // 4-bit initial monster mode (D2Client.dll 0x6fb5e462)
|
|
if (bitReader.remainingBits >= 1) {
|
|
const hasAnimComp = bitReader.readBit()
|
|
// Note: if hasAnimComp === 1, 16 component values follow (widths depend on MonStats2).
|
|
// If hasAnimComp === 0, we can directly read the monster flag bits at 0x6fb5e4fc:
|
|
if (hasAnimComp === 0 && bitReader.remainingBits >= 1) {
|
|
const hasMonsterFlags = bitReader.readBit()
|
|
if (hasMonsterFlags === 1 && bitReader.remainingBits >= 5) {
|
|
if (bitReader.readBit()) flags |= 0x04 // champion
|
|
if (bitReader.readBit()) flags |= 0x08 // unique
|
|
if (bitReader.readBit()) flags |= 0x02 // superunique
|
|
if (bitReader.readBit()) flags |= 0x10 // minion
|
|
if (bitReader.readBit()) flags |= 0x40 // ghostly
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const mode =
|
|
lifePct <= 0
|
|
? rawMode === 0 || rawMode === 12
|
|
? rawMode
|
|
: 12
|
|
: rawMode !== undefined && rawMode > 0 && rawMode !== 12
|
|
? rawMode
|
|
: 1
|
|
|
|
// D2Client.dll 0x6fafd512..0x6fafd59c (D2CLIENT_InitMonsterUnit): initial facing seeded by unit id
|
|
const direction = Math.imul(id, 0x6ac690c5) & 0x3f
|
|
|
|
return {
|
|
type: 'UnitAssign',
|
|
unit: {
|
|
unitType: 1,
|
|
id,
|
|
classId,
|
|
x,
|
|
y,
|
|
mode,
|
|
direction,
|
|
lifePct,
|
|
flags,
|
|
},
|
|
}
|
|
}
|