127 lines
5.1 KiB
TypeScript
127 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { ByteReader } from '../../src/netproto/core/byte-reader.ts'
|
|
import { validateC2sPacket } from '../../src/netproto/d2gs/framing-c2s.ts'
|
|
import {
|
|
C2S_PACKET_SIZES,
|
|
encodeC2sBeltPut,
|
|
encodeC2sBeltRemove,
|
|
encodeC2sBeltSwap,
|
|
encodeC2sBeltUse,
|
|
encodeC2sClickButton,
|
|
encodeC2sHostile,
|
|
encodeC2sIdentifyItem,
|
|
encodeC2sMercItem,
|
|
encodeC2sNpcEntityAction,
|
|
encodeC2sNpcMercHire,
|
|
encodeC2sNpcMercResurrect,
|
|
encodeC2sPartyAction,
|
|
encodeC2sQuestMessage,
|
|
encodeC2sResurrect,
|
|
encodeC2sScrollToBook,
|
|
encodeC2sSocketItem,
|
|
encodeC2sSwapContainerItem,
|
|
encodeC2sSwapEquippedItem,
|
|
encodeC2sWarpTo,
|
|
encodeClientCommand,
|
|
type ClientCommand,
|
|
} from '../../src/netproto/index.ts'
|
|
|
|
describe('D2GS C->S 1.13c packet encoders & ClientCommand dispatch (M1)', () => {
|
|
it('encodes all 18 M1 C2S opcodes with exact 1.13c byte layout and C2S_PACKET_SIZES validation', () => {
|
|
// 0x09 WarpTo (9B): [0x09][unitType:u32=5][warpId:u32]
|
|
const pkt09 = encodeC2sWarpTo(101)
|
|
expect(pkt09.byteLength).toBe(C2S_PACKET_SIZES[0x09])
|
|
expect(() => validateC2sPacket(pkt09)).not.toThrow()
|
|
{
|
|
const r = new ByteReader(pkt09)
|
|
expect(r.u8()).toBe(0x09)
|
|
expect(r.u32LE()).toBe(5)
|
|
expect(r.u32LE()).toBe(101)
|
|
}
|
|
|
|
// 0x1D SwapEquippedItem (9B): [0x1D][itemId:u32][bodyLoc:u32]
|
|
const pkt1d = encodeC2sSwapEquippedItem(200, 4)
|
|
expect(pkt1d.byteLength).toBe(C2S_PACKET_SIZES[0x1d])
|
|
expect(() => validateC2sPacket(pkt1d)).not.toThrow()
|
|
|
|
// 0x1F SwapContainerItem (17B): [0x1F][cursorItemId:u32][replacedItemId:u32][x:u32][y:u32]
|
|
const pkt1f = encodeC2sSwapContainerItem(201, 202, 5, 3)
|
|
expect(pkt1f.byteLength).toBe(C2S_PACKET_SIZES[0x1f])
|
|
expect(() => validateC2sPacket(pkt1f)).not.toThrow()
|
|
{
|
|
const r = new ByteReader(pkt1f)
|
|
expect(r.u8()).toBe(0x1f)
|
|
expect(r.u32LE()).toBe(201)
|
|
expect(r.u32LE()).toBe(202)
|
|
expect(r.u32LE()).toBe(5)
|
|
expect(r.u32LE()).toBe(3)
|
|
}
|
|
|
|
// 0x23 BeltPut (9B), 0x24 BeltRemove (5B), 0x25 BeltSwap (9B), 0x26 BeltUse (13B)
|
|
const pkt23 = encodeC2sBeltPut(301, 2)
|
|
const pkt24 = encodeC2sBeltRemove(301)
|
|
const pkt25 = encodeC2sBeltSwap(302, 301)
|
|
const pkt26 = encodeC2sBeltUse(301, true)
|
|
expect(pkt23.byteLength).toBe(C2S_PACKET_SIZES[0x23])
|
|
expect(pkt24.byteLength).toBe(C2S_PACKET_SIZES[0x24])
|
|
expect(pkt25.byteLength).toBe(C2S_PACKET_SIZES[0x25])
|
|
expect(pkt26.byteLength).toBe(C2S_PACKET_SIZES[0x26])
|
|
expect(new DataView(pkt26.buffer).getUint32(5, true)).toBe(1)
|
|
|
|
// 0x27 IdentifyItem (9B), 0x28 SocketItem (9B), 0x29 ScrollToBook (9B)
|
|
const pkt27 = encodeC2sIdentifyItem(401, 402)
|
|
const pkt28 = encodeC2sSocketItem(403, 404)
|
|
const pkt29 = encodeC2sScrollToBook(405, 406)
|
|
expect(pkt27.byteLength).toBe(C2S_PACKET_SIZES[0x27])
|
|
expect(pkt28.byteLength).toBe(C2S_PACKET_SIZES[0x28])
|
|
expect(pkt29.byteLength).toBe(C2S_PACKET_SIZES[0x29])
|
|
|
|
// 0x31 QuestMessage (9B), 0x36 NpcMercHire (9B), 0x38 NpcEntityAction (13B), 0x62 NpcMercResurrect (5B)
|
|
const pkt31 = encodeC2sQuestMessage(50, 120)
|
|
const pkt36 = encodeC2sNpcMercHire(50, 9)
|
|
const pkt38 = encodeC2sNpcEntityAction(3, 50, 0)
|
|
const pkt62 = encodeC2sNpcMercResurrect(50)
|
|
expect(pkt31.byteLength).toBe(C2S_PACKET_SIZES[0x31])
|
|
expect(pkt36.byteLength).toBe(C2S_PACKET_SIZES[0x36])
|
|
expect(pkt38.byteLength).toBe(C2S_PACKET_SIZES[0x38])
|
|
expect(pkt62.byteLength).toBe(C2S_PACKET_SIZES[0x62])
|
|
|
|
// 0x41 Resurrect (1B), 0x4F ClickButton (7B), 0x5D Hostile (7B), 0x5E PartyAction (6B), 0x61 MercItem (3B)
|
|
const pkt41 = encodeC2sResurrect()
|
|
const pkt4f = encodeC2sClickButton(0x02, 0)
|
|
const pkt5d = encodeC2sHostile(77, true)
|
|
const pkt5e = encodeC2sPartyAction(6, 77)
|
|
const pkt61 = encodeC2sMercItem(3)
|
|
expect(pkt41.byteLength).toBe(C2S_PACKET_SIZES[0x41])
|
|
expect(pkt4f.byteLength).toBe(C2S_PACKET_SIZES[0x4f])
|
|
expect(pkt5d.byteLength).toBe(C2S_PACKET_SIZES[0x5d])
|
|
expect(pkt5e.byteLength).toBe(C2S_PACKET_SIZES[0x5e])
|
|
expect(pkt61.byteLength).toBe(C2S_PACKET_SIZES[0x61])
|
|
})
|
|
|
|
it('dispatches all M1 ClientCommand variants via encodeClientCommand', () => {
|
|
const commands: ClientCommand[] = [
|
|
{ type: 'WarpTo', warpId: 10 },
|
|
{ type: 'TakeWarp', warpId: 10 },
|
|
{ type: 'SwapContainerItem', cursorItemId: 1, replacedItemId: 2, x: 0, y: 0 },
|
|
{ type: 'BeltSwap', cursorItemId: 3, beltItemId: 4 },
|
|
{ type: 'ScrollToBook', scrollId: 5, bookId: 6 },
|
|
{ type: 'QuestMessage', npcId: 7, messageId: 8 },
|
|
{ type: 'MercHire', npcId: 7, mercId: 9 },
|
|
{ type: 'NpcEntityAction', action: 1, npcId: 7, complement: 0 },
|
|
{ type: 'Resurrect' },
|
|
{ type: 'ClickButton', buttonId: 0x04, amount: 500 },
|
|
{ type: 'Hostile', targetPlayerId: 12, hostile: true },
|
|
{ type: 'PartyAction', action: 7, targetPlayerId: 12 },
|
|
{ type: 'MercItem', slot: 4 },
|
|
{ type: 'MercResurrect', npcId: 7 },
|
|
]
|
|
|
|
for (const cmd of commands) {
|
|
const pkts = encodeClientCommand(cmd)
|
|
expect(pkts).toHaveLength(1)
|
|
expect(() => validateC2sPacket(pkts[0]!)).not.toThrow()
|
|
}
|
|
})
|
|
})
|