Files
Erupe/network/mhfpacket/msg_build_test.go
Houmgaor 6d18de01eb test: increase total coverage from 40.7% to 46.1%
Add comprehensive mhfpacket Parse/Build/Opcode tests covering nearly
all packet types (78.3% -> 95.7%). Add channelserver tests for
BackportQuest and GuildIcon Scan/Value round-trips.
2026-02-08 16:17:17 +01:00

1513 lines
41 KiB
Go

package mhfpacket
import (
"bytes"
"io"
"testing"
"erupe-ce/common/byteframe"
"erupe-ce/network/clientctx"
)
// TestBuildParseDuplicateObject verifies Build/Parse round-trip for MsgSysDuplicateObject.
// This packet carries object ID, 3D position (float32 x/y/z), and owner character ID.
func TestBuildParseDuplicateObject(t *testing.T) {
tests := []struct {
name string
objID uint32
x, y, z float32
unk0 uint32
ownerCharID uint32
}{
{"typical values", 42, 1.5, 2.5, 3.5, 0, 12345},
{"zero values", 0, 0, 0, 0, 0, 0},
{"large values", 0xFFFFFFFF, -100.25, 200.75, -300.125, 0xDEADBEEF, 0xCAFEBABE},
{"negative coords", 1, -1.0, -2.0, -3.0, 100, 200},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysDuplicateObject{
ObjID: tt.objID,
X: tt.x,
Y: tt.y,
Z: tt.z,
Unk0: tt.unk0,
OwnerCharID: tt.ownerCharID,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysDuplicateObject{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.ObjID != original.ObjID {
t.Errorf("ObjID = %d, want %d", parsed.ObjID, original.ObjID)
}
if parsed.X != original.X {
t.Errorf("X = %f, want %f", parsed.X, original.X)
}
if parsed.Y != original.Y {
t.Errorf("Y = %f, want %f", parsed.Y, original.Y)
}
if parsed.Z != original.Z {
t.Errorf("Z = %f, want %f", parsed.Z, original.Z)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.OwnerCharID != original.OwnerCharID {
t.Errorf("OwnerCharID = %d, want %d", parsed.OwnerCharID, original.OwnerCharID)
}
})
}
}
// TestBuildParsePositionObject verifies Build/Parse round-trip for MsgSysPositionObject.
// This packet updates an object's 3D position (float32 x/y/z).
func TestBuildParsePositionObject(t *testing.T) {
tests := []struct {
name string
objID uint32
x, y, z float32
}{
{"origin", 1, 0, 0, 0},
{"typical position", 100, 50.5, 75.25, -10.125},
{"max object id", 0xFFFFFFFF, 999.999, -999.999, 0.001},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysPositionObject{
ObjID: tt.objID,
X: tt.x,
Y: tt.y,
Z: tt.z,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysPositionObject{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.ObjID != original.ObjID {
t.Errorf("ObjID = %d, want %d", parsed.ObjID, original.ObjID)
}
if parsed.X != original.X {
t.Errorf("X = %f, want %f", parsed.X, original.X)
}
if parsed.Y != original.Y {
t.Errorf("Y = %f, want %f", parsed.Y, original.Y)
}
if parsed.Z != original.Z {
t.Errorf("Z = %f, want %f", parsed.Z, original.Z)
}
})
}
}
// TestBuildParseCastedBinary verifies Build/Parse round-trip for MsgSysCastedBinary.
// This packet carries broadcast data with a length-prefixed payload.
func TestBuildParseCastedBinary(t *testing.T) {
tests := []struct {
name string
charID uint32
broadcastType uint8
messageType uint8
rawDataPayload []byte
}{
{"small payload", 12345, 1, 2, []byte{0xAA, 0xBB, 0xCC}},
{"empty payload", 0, 0, 0, []byte{}},
{"single byte payload", 0xDEADBEEF, 255, 128, []byte{0xFF}},
{"larger payload", 42, 3, 4, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysCastedBinary{
CharID: tt.charID,
BroadcastType: tt.broadcastType,
MessageType: tt.messageType,
RawDataPayload: tt.rawDataPayload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysCastedBinary{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.CharID != original.CharID {
t.Errorf("CharID = %d, want %d", parsed.CharID, original.CharID)
}
if parsed.BroadcastType != original.BroadcastType {
t.Errorf("BroadcastType = %d, want %d", parsed.BroadcastType, original.BroadcastType)
}
if parsed.MessageType != original.MessageType {
t.Errorf("MessageType = %d, want %d", parsed.MessageType, original.MessageType)
}
if !bytes.Equal(parsed.RawDataPayload, original.RawDataPayload) {
t.Errorf("RawDataPayload = %v, want %v", parsed.RawDataPayload, original.RawDataPayload)
}
})
}
}
// TestBuildParseLoadRegister verifies Build/Parse round-trip for MsgSysLoadRegister.
// This packet reads AckHandle, RegisterID, Unk1, and fixed zero padding.
func TestBuildParseLoadRegister(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
registerID uint32
unk1 uint8
}{
{"typical", 0x11223344, 100, 1},
{"zero values", 0, 0, 0},
{"max values", 0xFFFFFFFF, 0xFFFFFFFF, 255},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysLoadRegister{
AckHandle: tt.ackHandle,
RegisterID: tt.registerID,
Unk1: tt.unk1,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysLoadRegister{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.RegisterID != original.RegisterID {
t.Errorf("RegisterID = %d, want %d", parsed.RegisterID, original.RegisterID)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
})
}
}
// TestBuildParseOperateRegister verifies Build/Parse round-trip for MsgSysOperateRegister.
// This packet carries a semaphore ID and a length-prefixed raw data payload.
func TestBuildParseOperateRegister(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
semaphoreID uint32
payload []byte
}{
{"typical", 1, 42, []byte{0x01, 0x02, 0x03}},
{"empty payload", 0, 0, []byte{}},
{"large payload", 0xFFFFFFFF, 0xDEADBEEF, make([]byte, 256)},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysOperateRegister{
AckHandle: tt.ackHandle,
SemaphoreID: tt.semaphoreID,
RawDataPayload: tt.payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysOperateRegister{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.SemaphoreID != original.SemaphoreID {
t.Errorf("SemaphoreID = %d, want %d", parsed.SemaphoreID, original.SemaphoreID)
}
if !bytes.Equal(parsed.RawDataPayload, original.RawDataPayload) {
t.Errorf("RawDataPayload length = %d, want %d", len(parsed.RawDataPayload), len(original.RawDataPayload))
}
})
}
}
// TestBuildParseNotifyUserBinary verifies Build/Parse round-trip for MsgSysNotifyUserBinary.
func TestBuildParseNotifyUserBinary(t *testing.T) {
tests := []struct {
name string
charID uint32
binaryType uint8
}{
{"typical", 12345, 1},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 255},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysNotifyUserBinary{
CharID: tt.charID,
BinaryType: tt.binaryType,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysNotifyUserBinary{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.CharID != original.CharID {
t.Errorf("CharID = %d, want %d", parsed.CharID, original.CharID)
}
if parsed.BinaryType != original.BinaryType {
t.Errorf("BinaryType = %d, want %d", parsed.BinaryType, original.BinaryType)
}
})
}
}
// TestBuildParseTime verifies Build/Parse round-trip for MsgSysTime.
// This packet carries a boolean flag and a Unix timestamp.
func TestBuildParseTime(t *testing.T) {
tests := []struct {
name string
getRemoteTime bool
timestamp uint32
}{
{"request remote time", true, 1577105879},
{"no request", false, 0},
{"max timestamp", true, 0xFFFFFFFF},
{"typical timestamp", false, 1700000000},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysTime{
GetRemoteTime: tt.getRemoteTime,
Timestamp: tt.timestamp,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysTime{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.GetRemoteTime != original.GetRemoteTime {
t.Errorf("GetRemoteTime = %v, want %v", parsed.GetRemoteTime, original.GetRemoteTime)
}
if parsed.Timestamp != original.Timestamp {
t.Errorf("Timestamp = %d, want %d", parsed.Timestamp, original.Timestamp)
}
})
}
}
// TestBuildParseUpdateObjectBinary verifies Build/Parse round-trip for MsgSysUpdateObjectBinary.
func TestBuildParseUpdateObjectBinary(t *testing.T) {
tests := []struct {
name string
unk0 uint32
unk1 uint32
}{
{"typical", 42, 100},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysUpdateObjectBinary{
Unk0: tt.unk0,
Unk1: tt.unk1,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysUpdateObjectBinary{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
})
}
}
// TestBuildParseArrangeGuildMember verifies Build/Parse round-trip for MsgMhfArrangeGuildMember.
// This packet uses a length-prefixed slice of uint32 character IDs.
func TestBuildParseArrangeGuildMember(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
guildID uint32
charIDs []uint32
}{
{"single member", 1, 100, []uint32{12345}},
{"multiple members", 0x12345678, 200, []uint32{111, 222, 333, 444}},
{"no members", 42, 300, []uint32{}},
{"many members", 999, 400, []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfArrangeGuildMember{
AckHandle: tt.ackHandle,
GuildID: tt.guildID,
CharIDs: tt.charIDs,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfArrangeGuildMember{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.GuildID != original.GuildID {
t.Errorf("GuildID = %d, want %d", parsed.GuildID, original.GuildID)
}
if len(parsed.CharIDs) != len(original.CharIDs) {
t.Fatalf("CharIDs length = %d, want %d", len(parsed.CharIDs), len(original.CharIDs))
}
for i, id := range parsed.CharIDs {
if id != original.CharIDs[i] {
t.Errorf("CharIDs[%d] = %d, want %d", i, id, original.CharIDs[i])
}
}
})
}
}
// TestBuildParseEnumerateGuildMember verifies Build/Parse round-trip for MsgMhfEnumerateGuildMember.
func TestBuildParseEnumerateGuildMember(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint16
unk1 uint32
guildID uint32
}{
{"typical", 1, 0x0001, 0, 100},
{"zero", 0, 0, 0, 0},
{"large values", 0xFFFFFFFF, 0xFFFF, 0xDEADBEEF, 0xCAFEBABE},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfEnumerateGuildMember{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
GuildID: tt.guildID,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfEnumerateGuildMember{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
if parsed.GuildID != original.GuildID {
t.Errorf("GuildID = %d, want %d", parsed.GuildID, original.GuildID)
}
})
}
}
// TestBuildParseStateCampaign verifies Build/Parse round-trip for MsgMhfStateCampaign.
func TestBuildParseStateCampaign(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint8
unk1 uint8
unk2 uint16
}{
{"typical", 1, 10, 20, 300},
{"zero", 0, 0, 0, 0},
{"max", 0xFFFFFFFF, 255, 255, 0xFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfStateCampaign{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
Unk2: tt.unk2,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfStateCampaign{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
if parsed.Unk2 != original.Unk2 {
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, original.Unk2)
}
})
}
}
// TestBuildParseApplyCampaign verifies Build/Parse round-trip for MsgMhfApplyCampaign.
func TestBuildParseApplyCampaign(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint8
unk1 uint8
unk2 uint16
}{
{"typical", 0x55667788, 5, 10, 500},
{"zero", 0, 0, 0, 0},
{"max", 0xFFFFFFFF, 255, 255, 0xFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfApplyCampaign{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
Unk2: tt.unk2,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfApplyCampaign{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
if parsed.Unk2 != original.Unk2 {
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, original.Unk2)
}
})
}
}
// TestBuildParseEnumerateCampaign verifies Build/Parse round-trip for MsgMhfEnumerateCampaign.
func TestBuildParseEnumerateCampaign(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint8
unk1 uint8
unk2 uint16
}{
{"typical", 42, 1, 2, 300},
{"zero", 0, 0, 0, 0},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfEnumerateCampaign{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
Unk2: tt.unk2,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfEnumerateCampaign{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
if parsed.Unk2 != original.Unk2 {
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, original.Unk2)
}
})
}
}
// TestBuildParseEnumerateEvent verifies Build/Parse round-trip for MsgMhfEnumerateEvent.
func TestBuildParseEnumerateEvent(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint16
unk1 uint16
}{
{"typical", 0x11223344, 0, 0},
{"nonzero", 42, 100, 200},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfEnumerateEvent{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfEnumerateEvent{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
})
}
}
// TestBuildParseAddUdTacticsPoint verifies Build/Parse round-trip for MsgMhfAddUdTacticsPoint.
func TestBuildParseAddUdTacticsPoint(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint16
unk1 uint32
}{
{"typical", 1, 100, 50000},
{"zero", 0, 0, 0},
{"max", 0xFFFFFFFF, 0xFFFF, 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfAddUdTacticsPoint{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfAddUdTacticsPoint{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
})
}
}
// TestBuildParseApplyDistItem verifies Build/Parse round-trip for MsgMhfApplyDistItem.
// This packet has mixed field sizes (uint32, uint8, uint32, uint32, uint32).
func TestBuildParseApplyDistItem(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
distributionType uint8
distributionID uint32
unk2 uint32
unk3 uint32
}{
{"typical", 0x12345678, 1, 42, 100, 200},
{"zero", 0, 0, 0, 0, 0},
{"max", 0xFFFFFFFF, 255, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfApplyDistItem{
AckHandle: tt.ackHandle,
DistributionType: tt.distributionType,
DistributionID: tt.distributionID,
Unk2: tt.unk2,
Unk3: tt.unk3,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfApplyDistItem{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.DistributionType != original.DistributionType {
t.Errorf("DistributionType = %d, want %d", parsed.DistributionType, original.DistributionType)
}
if parsed.DistributionID != original.DistributionID {
t.Errorf("DistributionID = %d, want %d", parsed.DistributionID, original.DistributionID)
}
if parsed.Unk2 != original.Unk2 {
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, original.Unk2)
}
if parsed.Unk3 != original.Unk3 {
t.Errorf("Unk3 = %d, want %d", parsed.Unk3, original.Unk3)
}
})
}
}
// TestBuildParseEnumerateDistItem verifies Build/Parse round-trip for MsgMhfEnumerateDistItem.
func TestBuildParseEnumerateDistItem(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk0 uint8
unk1 uint16
unk2 uint16
}{
{"typical", 0xAABBCCDD, 5, 100, 200},
{"zero", 0, 0, 0, 0},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfEnumerateDistItem{
AckHandle: tt.ackHandle,
Unk0: tt.unk0,
Unk1: tt.unk1,
Unk2: tt.unk2,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfEnumerateDistItem{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
if parsed.Unk1 != original.Unk1 {
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
}
if parsed.Unk2 != original.Unk2 {
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, original.Unk2)
}
})
}
}
// TestBuildParseAcquireExchangeShop verifies Build/Parse round-trip for MsgMhfAcquireExchangeShop.
// This packet has a separate DataSize field and a length-prefixed raw data payload.
func TestBuildParseAcquireExchangeShop(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
payload []byte
}{
{"small payload", 1, []byte{0x01, 0x02, 0x03, 0x04}},
{"empty payload", 0, []byte{}},
{"larger payload", 0xDEADBEEF, []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22}},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfAcquireExchangeShop{
AckHandle: tt.ackHandle,
DataSize: uint16(len(tt.payload)),
RawDataPayload: tt.payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfAcquireExchangeShop{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.DataSize != original.DataSize {
t.Errorf("DataSize = %d, want %d", parsed.DataSize, original.DataSize)
}
if !bytes.Equal(parsed.RawDataPayload, original.RawDataPayload) {
t.Errorf("RawDataPayload = %v, want %v", parsed.RawDataPayload, original.RawDataPayload)
}
})
}
}
// TestBuildParseDisplayedAchievement verifies Build/Parse round-trip for MsgMhfDisplayedAchievement.
func TestBuildParseDisplayedAchievement(t *testing.T) {
tests := []struct {
name string
unk0 uint8
}{
{"zero", 0},
{"typical", 42},
{"max", 255},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfDisplayedAchievement{
Unk0: tt.unk0,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfDisplayedAchievement{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
})
}
}
// TestBuildParseAddKouryouPoint verifies Build/Parse round-trip for MsgMhfAddKouryouPoint.
func TestBuildParseAddKouryouPoint(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
kouryouPoints uint32
}{
{"typical", 1, 5000},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfAddKouryouPoint{
AckHandle: tt.ackHandle,
KouryouPoints: tt.kouryouPoints,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfAddKouryouPoint{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.KouryouPoints != original.KouryouPoints {
t.Errorf("KouryouPoints = %d, want %d", parsed.KouryouPoints, original.KouryouPoints)
}
})
}
}
// TestBuildParseCheckDailyCafepoint verifies Build/Parse round-trip for MsgMhfCheckDailyCafepoint.
func TestBuildParseCheckDailyCafepoint(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
unk uint32
}{
{"typical", 0x11223344, 100},
{"zero", 0, 0},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgMhfCheckDailyCafepoint{
AckHandle: tt.ackHandle,
Unk: tt.unk,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfCheckDailyCafepoint{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.Unk != original.Unk {
t.Errorf("Unk = %d, want %d", parsed.Unk, original.Unk)
}
})
}
}
// TestBuildParsePing verifies Build/Parse round-trip for MsgSysPing.
func TestBuildParsePing(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
}{
{"typical", 0x12345678},
{"zero", 0},
{"max", 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysPing{
AckHandle: tt.ackHandle,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysPing{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
})
}
}
// TestBuildParseDeleteObject verifies Build/Parse round-trip for MsgSysDeleteObject.
func TestBuildParseDeleteObject(t *testing.T) {
tests := []struct {
name string
objID uint32
}{
{"typical", 42},
{"zero", 0},
{"max", 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysDeleteObject{
ObjID: tt.objID,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysDeleteObject{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.ObjID != original.ObjID {
t.Errorf("ObjID = %d, want %d", parsed.ObjID, original.ObjID)
}
})
}
}
// TestBuildParseNotifyRegister verifies Build/Parse round-trip for MsgSysNotifyRegister.
func TestBuildParseNotifyRegister(t *testing.T) {
tests := []struct {
name string
registerID uint32
}{
{"typical", 100},
{"zero", 0},
{"max", 0xFFFFFFFF},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysNotifyRegister{
RegisterID: tt.registerID,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysNotifyRegister{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.RegisterID != original.RegisterID {
t.Errorf("RegisterID = %d, want %d", parsed.RegisterID, original.RegisterID)
}
})
}
}
// TestBuildParseUnlockStage verifies Build/Parse round-trip for MsgSysUnlockStage.
func TestBuildParseUnlockStage(t *testing.T) {
tests := []struct {
name string
unk0 uint16
}{
{"zero (hardcoded)", 0},
{"nonzero", 0x1234},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysUnlockStage{
Unk0: tt.unk0,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysUnlockStage{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.Unk0 != original.Unk0 {
t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0)
}
})
}
}
// TestBuildParseUnlockGlobalSema verifies Build/Parse round-trip for MsgSysUnlockGlobalSema.
func TestBuildParseUnlockGlobalSema(t *testing.T) {
tests := []struct {
name string
ackHandle uint32
}{
{"typical", 0xAABBCCDD},
{"zero", 0},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &MsgSysUnlockGlobalSema{
AckHandle: tt.ackHandle,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysUnlockGlobalSema{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
})
}
}
// TestBuildParseStageDestruct verifies Build/Parse round-trip for MsgSysStageDestruct.
// This packet has no fields at all.
func TestBuildParseStageDestruct(t *testing.T) {
ctx := &clientctx.ClientContext{}
original := &MsgSysStageDestruct{}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
if len(bf.Data()) != 0 {
t.Errorf("Build() wrote %d bytes, want 0", len(bf.Data()))
}
parsed := &MsgSysStageDestruct{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
}
// TestBuildParseCastedBinaryPayloadIntegrity verifies that a large payload is preserved
// exactly through Build/Parse for MsgSysCastedBinary.
func TestBuildParseCastedBinaryPayloadIntegrity(t *testing.T) {
ctx := &clientctx.ClientContext{}
// Build a payload with recognizable pattern
payload := make([]byte, 1024)
for i := range payload {
payload[i] = byte(i % 256)
}
original := &MsgSysCastedBinary{
CharID: 0x12345678,
BroadcastType: 0x03,
MessageType: 0x07,
RawDataPayload: payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysCastedBinary{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if len(parsed.RawDataPayload) != len(payload) {
t.Fatalf("Payload length = %d, want %d", len(parsed.RawDataPayload), len(payload))
}
for i, b := range parsed.RawDataPayload {
if b != payload[i] {
t.Errorf("Payload byte %d = 0x%02X, want 0x%02X", i, b, payload[i])
break // Only report first mismatch
}
}
}
// TestBuildParseOperateRegisterPayloadIntegrity verifies payload integrity through
// Build/Parse for MsgSysOperateRegister.
func TestBuildParseOperateRegisterPayloadIntegrity(t *testing.T) {
ctx := &clientctx.ClientContext{}
payload := make([]byte, 512)
for i := range payload {
payload[i] = byte((i * 7) % 256) // Non-trivial pattern
}
original := &MsgSysOperateRegister{
AckHandle: 0xAABBCCDD,
SemaphoreID: 42,
RawDataPayload: payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysOperateRegister{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if !bytes.Equal(parsed.RawDataPayload, payload) {
t.Errorf("Payload mismatch: got %d bytes, want %d bytes", len(parsed.RawDataPayload), len(payload))
}
}
// TestBuildParseArrangeGuildMemberEmptySlice ensures that an empty CharIDs slice
// round-trips correctly (the uint16 count field should be 0).
func TestBuildParseArrangeGuildMemberEmptySlice(t *testing.T) {
ctx := &clientctx.ClientContext{}
original := &MsgMhfArrangeGuildMember{
AckHandle: 1,
GuildID: 100,
CharIDs: []uint32{},
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
// Verify the wire size: uint32 + uint32 + uint16 = 10 bytes
if len(bf.Data()) != 10 {
t.Errorf("Build() wrote %d bytes, want 10 for empty CharIDs", len(bf.Data()))
}
bf.Seek(0, io.SeekStart)
parsed := &MsgMhfArrangeGuildMember{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if len(parsed.CharIDs) != 0 {
t.Errorf("CharIDs length = %d, want 0", len(parsed.CharIDs))
}
}
// TestBuildBinaryFormat verifies the exact binary output format of a Build call
// for MsgSysDuplicateObject to ensure correct endianness and field ordering.
func TestBuildBinaryFormat(t *testing.T) {
ctx := &clientctx.ClientContext{}
pkt := &MsgSysDuplicateObject{
ObjID: 0x00000001,
X: 0,
Y: 0,
Z: 0,
Unk0: 0x00000002,
OwnerCharID: 0x00000003,
}
bf := byteframe.NewByteFrame()
if err := pkt.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
data := bf.Data()
// Expected: 6 fields * 4 bytes = 24 bytes
if len(data) != 24 {
t.Fatalf("Build() wrote %d bytes, want 24", len(data))
}
// ObjID = 0x00000001 in big-endian
if data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x00 || data[3] != 0x01 {
t.Errorf("ObjID bytes = %X, want 00000001", data[0:4])
}
// Unk0 = 0x00000002 at offset 16 (after ObjID + 3 floats)
if data[16] != 0x00 || data[17] != 0x00 || data[18] != 0x00 || data[19] != 0x02 {
t.Errorf("Unk0 bytes = %X, want 00000002", data[16:20])
}
// OwnerCharID = 0x00000003 at offset 20
if data[20] != 0x00 || data[21] != 0x00 || data[22] != 0x00 || data[23] != 0x03 {
t.Errorf("OwnerCharID bytes = %X, want 00000003", data[20:24])
}
}
// TestBuildParseTimeBooleanEncoding verifies that the boolean field in MsgSysTime
// is encoded/decoded correctly for both true and false.
func TestBuildParseTimeBooleanEncoding(t *testing.T) {
ctx := &clientctx.ClientContext{}
for _, val := range []bool{true, false} {
t.Run("GetRemoteTime="+boolStr(val), func(t *testing.T) {
original := &MsgSysTime{
GetRemoteTime: val,
Timestamp: 1234567890,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
// Check raw byte: true=1, false=0
data := bf.Data()
if val && data[0] != 1 {
t.Errorf("Boolean true encoded as %d, want 1", data[0])
}
if !val && data[0] != 0 {
t.Errorf("Boolean false encoded as %d, want 0", data[0])
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysTime{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.GetRemoteTime != val {
t.Errorf("GetRemoteTime = %v, want %v", parsed.GetRemoteTime, val)
}
})
}
}
func boolStr(b bool) string {
if b {
return "true"
}
return "false"
}
// TestBuildParseSysAckBufferSmall verifies MsgSysAck round-trip with buffer response
// using the normal (non-extended) size field.
func TestBuildParseSysAckBufferSmall(t *testing.T) {
ctx := &clientctx.ClientContext{}
payload := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
original := &MsgSysAck{
AckHandle: 0xDEADBEEF,
IsBufferResponse: true,
ErrorCode: 0,
AckData: payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysAck{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle)
}
if parsed.IsBufferResponse != original.IsBufferResponse {
t.Errorf("IsBufferResponse = %v, want %v", parsed.IsBufferResponse, original.IsBufferResponse)
}
if parsed.ErrorCode != original.ErrorCode {
t.Errorf("ErrorCode = %d, want %d", parsed.ErrorCode, original.ErrorCode)
}
if !bytes.Equal(parsed.AckData, original.AckData) {
t.Errorf("AckData = %v, want %v", parsed.AckData, original.AckData)
}
}
// TestBuildParseSysAckExtendedSize verifies MsgSysAck round-trip with a payload
// large enough to trigger the extended size field (>= 0xFFFF bytes).
func TestBuildParseSysAckExtendedSize(t *testing.T) {
ctx := &clientctx.ClientContext{}
payload := make([]byte, 0x10000) // 65536 bytes, triggers extended size
for i := range payload {
payload[i] = byte(i % 256)
}
original := &MsgSysAck{
AckHandle: 42,
IsBufferResponse: true,
ErrorCode: 0,
AckData: payload,
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysAck{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if len(parsed.AckData) != len(payload) {
t.Fatalf("AckData length = %d, want %d", len(parsed.AckData), len(payload))
}
if !bytes.Equal(parsed.AckData, payload) {
t.Error("AckData content mismatch after extended size round-trip")
}
}
// TestBuildParseSysAckNonBuffer verifies MsgSysAck round-trip with non-buffer response
// (exactly 4 bytes of data always read in Parse).
func TestBuildParseSysAckNonBuffer(t *testing.T) {
ctx := &clientctx.ClientContext{}
original := &MsgSysAck{
AckHandle: 100,
IsBufferResponse: false,
ErrorCode: 5,
AckData: []byte{0xAA, 0xBB, 0xCC, 0xDD},
}
bf := byteframe.NewByteFrame()
if err := original.Build(bf, ctx); err != nil {
t.Fatalf("Build() error = %v", err)
}
bf.Seek(0, io.SeekStart)
parsed := &MsgSysAck{}
if err := parsed.Parse(bf, ctx); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if parsed.AckHandle != original.AckHandle {
t.Errorf("AckHandle = %d, want %d", parsed.AckHandle, original.AckHandle)
}
if parsed.IsBufferResponse != false {
t.Errorf("IsBufferResponse = %v, want false", parsed.IsBufferResponse)
}
if parsed.ErrorCode != 5 {
t.Errorf("ErrorCode = %d, want 5", parsed.ErrorCode)
}
// Non-buffer always reads exactly 4 bytes
if len(parsed.AckData) != 4 {
t.Errorf("AckData length = %d, want 4", len(parsed.AckData))
}
if !bytes.Equal(parsed.AckData, []byte{0xAA, 0xBB, 0xCC, 0xDD}) {
t.Errorf("AckData = %v, want [AA BB CC DD]", parsed.AckData)
}
}