mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
1413 lines
41 KiB
Go
1413 lines
41 KiB
Go
package mhfpacket
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"testing"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
cfg "erupe-ce/config"
|
|
"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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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 manual-build/Parse round-trip for MsgSysLoadRegister.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
func TestBuildParseLoadRegister(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
registerID uint32
|
|
values uint8
|
|
}{
|
|
{"typical", 0x11223344, 100, 1},
|
|
{"zero values", 0, 0, 0},
|
|
{"max values", 0xFFFFFFFF, 0xFFFFFFFF, 255},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.registerID)
|
|
bf.WriteUint8(tt.values)
|
|
bf.WriteUint8(0) // Zeroed
|
|
bf.WriteUint16(0) // Zeroed
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgSysLoadRegister{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.RegisterID != tt.registerID {
|
|
t.Errorf("RegisterID = %d, want %d", parsed.RegisterID, tt.registerID)
|
|
}
|
|
if parsed.Values != tt.values {
|
|
t.Errorf("Values = %d, want %d", parsed.Values, tt.values)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseOperateRegister verifies manual-build/Parse round-trip for MsgSysOperateRegister.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
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{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.semaphoreID)
|
|
bf.WriteUint16(0) // Zeroed
|
|
bf.WriteUint16(uint16(len(tt.payload)))
|
|
bf.WriteBytes(tt.payload)
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgSysOperateRegister{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.SemaphoreID != tt.semaphoreID {
|
|
t.Errorf("SemaphoreID = %d, want %d", parsed.SemaphoreID, tt.semaphoreID)
|
|
}
|
|
if !bytes.Equal(parsed.RawDataPayload, tt.payload) {
|
|
t.Errorf("RawDataPayload length = %d, want %d", len(parsed.RawDataPayload), len(tt.payload))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := &MsgSysUpdateObjectBinary{
|
|
ObjectHandleID: 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.ObjectHandleID != original.ObjectHandleID {
|
|
t.Errorf("Unk0 = %d, want %d", parsed.ObjectHandleID, original.ObjectHandleID)
|
|
}
|
|
if parsed.Unk1 != original.Unk1 {
|
|
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseArrangeGuildMember verifies manual-build/Parse round-trip for MsgMhfArrangeGuildMember.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
// Parse reads: uint32 AckHandle, uint32 GuildID, uint8 zeroed, uint8 charCount, then charCount * uint32.
|
|
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{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.guildID)
|
|
bf.WriteUint8(0) // Zeroed
|
|
bf.WriteUint8(uint8(len(tt.charIDs)))
|
|
for _, id := range tt.charIDs {
|
|
bf.WriteUint32(id)
|
|
}
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfArrangeGuildMember{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.GuildID != tt.guildID {
|
|
t.Errorf("GuildID = %d, want %d", parsed.GuildID, tt.guildID)
|
|
}
|
|
if len(parsed.CharIDs) != len(tt.charIDs) {
|
|
t.Fatalf("CharIDs length = %d, want %d", len(parsed.CharIDs), len(tt.charIDs))
|
|
}
|
|
for i, id := range parsed.CharIDs {
|
|
if id != tt.charIDs[i] {
|
|
t.Errorf("CharIDs[%d] = %d, want %d", i, id, tt.charIDs[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseEnumerateGuildMember verifies manual-build/Parse round-trip for MsgMhfEnumerateGuildMember.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
// Parse reads: uint32 AckHandle, uint8 zeroed, uint8 always1, uint32 AllianceID, uint32 GuildID.
|
|
func TestBuildParseEnumerateGuildMember(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
allianceID uint32
|
|
guildID uint32
|
|
}{
|
|
{"typical", 1, 0, 100},
|
|
{"zero", 0, 0, 0},
|
|
{"large values", 0xFFFFFFFF, 0xDEADBEEF, 0xCAFEBABE},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint8(0) // Zeroed
|
|
bf.WriteUint8(1) // Always 1
|
|
bf.WriteUint32(tt.allianceID)
|
|
bf.WriteUint32(tt.guildID)
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfEnumerateGuildMember{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.AllianceID != tt.allianceID {
|
|
t.Errorf("AllianceID = %d, want %d", parsed.AllianceID, tt.allianceID)
|
|
}
|
|
if parsed.GuildID != tt.guildID {
|
|
t.Errorf("GuildID = %d, want %d", parsed.GuildID, tt.guildID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseStateCampaign verifies manual-build/Parse round-trip for MsgMhfStateCampaign.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
func TestBuildParseStateCampaign(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
campaignID uint32
|
|
unk1 uint16
|
|
}{
|
|
{"typical", 1, 10, 300},
|
|
{"zero", 0, 0, 0},
|
|
{"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.campaignID)
|
|
bf.WriteUint16(tt.unk1)
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfStateCampaign{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.CampaignID != tt.campaignID {
|
|
t.Errorf("CampaignID = %d, want %d", parsed.CampaignID, tt.campaignID)
|
|
}
|
|
if parsed.NullPadding != tt.unk1 {
|
|
t.Errorf("NullPadding = %d, want %d", parsed.NullPadding, tt.unk1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseApplyCampaign verifies manual-build/Parse round-trip for MsgMhfApplyCampaign.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
func TestBuildParseApplyCampaign(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
campaignID uint32
|
|
code string
|
|
}{
|
|
{"typical", 0x55667788, 5, "TESTCODE"},
|
|
{"zero", 0, 0, ""},
|
|
{"max", 0xFFFFFFFF, 0xFFFFFFFF, "MAXCODE"},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.campaignID)
|
|
bf.WriteUint16(0) // zeroed
|
|
codeBytes := make([]byte, 16)
|
|
copy(codeBytes, []byte(tt.code))
|
|
bf.WriteBytes(codeBytes)
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfApplyCampaign{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.CampaignID != tt.campaignID {
|
|
t.Errorf("CampaignID = %d, want %d", parsed.CampaignID, tt.campaignID)
|
|
}
|
|
if parsed.Code != tt.code {
|
|
t.Errorf("Code = %q, want %q", parsed.Code, tt.code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseEnumerateCampaign verifies Build/Parse round-trip for MsgMhfEnumerateCampaign.
|
|
func TestBuildParseEnumerateCampaign(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
unk0 uint16
|
|
unk1 uint16
|
|
}{
|
|
{"typical", 42, 1, 2},
|
|
{"zero", 0, 0, 0},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := &MsgMhfEnumerateCampaign{
|
|
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 := &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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseEnumerateEvent verifies Build/Parse round-trip for MsgMhfEnumerateEvent.
|
|
func TestBuildParseEnumerateEvent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
}{
|
|
{"typical", 0x11223344},
|
|
{"nonzero", 42},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := &MsgMhfEnumerateEvent{
|
|
AckHandle: tt.ackHandle,
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
// Build is NOT IMPLEMENTED; manually write the binary representation
|
|
bf.WriteUint32(original.AckHandle)
|
|
bf.WriteUint16(0) // Zeroed (discarded by Parse)
|
|
bf.WriteUint16(0) // Zeroed (discarded by Parse)
|
|
|
|
_, _ = 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseAddUdTacticsPoint verifies Build/Parse round-trip for MsgMhfAddUdTacticsPoint.
|
|
func TestBuildParseAddUdTacticsPoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
questID uint16
|
|
tacticsPoints uint32
|
|
}{
|
|
{"typical", 1, 100, 50000},
|
|
{"zero", 0, 0, 0},
|
|
{"max", 0xFFFFFFFF, 0xFFFF, 0xFFFFFFFF},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := &MsgMhfAddUdTacticsPoint{
|
|
AckHandle: tt.ackHandle,
|
|
QuestID: tt.questID,
|
|
TacticsPoints: tt.tacticsPoints,
|
|
}
|
|
|
|
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.QuestID != original.QuestID {
|
|
t.Errorf("QuestID = %d, want %d", parsed.QuestID, original.QuestID)
|
|
}
|
|
if parsed.TacticsPoints != original.TacticsPoints {
|
|
t.Errorf("TacticsPoints = %d, want %d", parsed.TacticsPoints, original.TacticsPoints)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseApplyDistItem verifies manual-build/Parse round-trip for MsgMhfApplyDistItem.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
// Note: Unk2 and Unk3 are conditionally parsed based on RealClientMode (G8+ and G10+).
|
|
// Default test config is ZZ, so both Unk2 and Unk3 are read.
|
|
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{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint8(tt.distributionType)
|
|
bf.WriteUint32(tt.distributionID)
|
|
bf.WriteUint32(tt.unk2) // Read when RealClientMode >= G8
|
|
bf.WriteUint32(tt.unk3) // Read when RealClientMode >= G10
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfApplyDistItem{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.DistributionType != tt.distributionType {
|
|
t.Errorf("DistributionType = %d, want %d", parsed.DistributionType, tt.distributionType)
|
|
}
|
|
if parsed.DistributionID != tt.distributionID {
|
|
t.Errorf("DistributionID = %d, want %d", parsed.DistributionID, tt.distributionID)
|
|
}
|
|
if parsed.Unk2 != tt.unk2 {
|
|
t.Errorf("Unk2 = %d, want %d", parsed.Unk2, tt.unk2)
|
|
}
|
|
if parsed.Unk3 != tt.unk3 {
|
|
t.Errorf("Unk3 = %d, want %d", parsed.Unk3, tt.unk3)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildParseEnumerateDistItem verifies Build/Parse round-trip for MsgMhfEnumerateDistItem.
|
|
func TestBuildParseEnumerateDistItem(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
distType uint8
|
|
unk1 uint8
|
|
unk2 uint16
|
|
}{
|
|
{"typical", 0xAABBCCDD, 5, 100, 200},
|
|
{"zero", 0, 0, 0, 0},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := &MsgMhfEnumerateDistItem{
|
|
AckHandle: tt.ackHandle,
|
|
DistType: tt.distType,
|
|
Unk1: tt.unk1,
|
|
MaxCount: tt.unk2,
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
// Build is NOT IMPLEMENTED; manually write the binary representation
|
|
bf.WriteUint32(original.AckHandle)
|
|
bf.WriteUint8(original.DistType)
|
|
bf.WriteUint8(original.Unk1)
|
|
bf.WriteUint16(original.MaxCount)
|
|
bf.WriteUint8(0) // Unk3 length (for Z1+ client mode)
|
|
|
|
_, _ = 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.DistType != original.DistType {
|
|
t.Errorf("DistType = %d, want %d", parsed.DistType, original.DistType)
|
|
}
|
|
if parsed.Unk1 != original.Unk1 {
|
|
t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1)
|
|
}
|
|
if parsed.MaxCount != original.MaxCount {
|
|
t.Errorf("Unk2 = %d, want %d", parsed.MaxCount, original.MaxCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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{RealClientMode: cfg.ZZ}
|
|
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 Parse for MsgMhfDisplayedAchievement.
|
|
// This struct has no exported fields; Parse only discards a single zeroed byte.
|
|
func TestBuildParseDisplayedAchievement(t *testing.T) {
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(0) // Zeroed (discarded by Parse)
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
|
|
parsed := &MsgMhfDisplayedAchievement{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// 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{RealClientMode: cfg.ZZ}
|
|
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 manual-build/Parse round-trip for MsgMhfCheckDailyCafepoint.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
func TestBuildParseCheckDailyCafepoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ackHandle uint32
|
|
unk uint32
|
|
}{
|
|
{"typical", 0x11223344, 100},
|
|
{"zero", 0, 0},
|
|
}
|
|
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(tt.ackHandle)
|
|
bf.WriteUint32(tt.unk)
|
|
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
parsed := &MsgMhfCheckDailyCafepoint{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if parsed.AckHandle != tt.ackHandle {
|
|
t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle)
|
|
}
|
|
if parsed.Unk != tt.unk {
|
|
t.Errorf("Unk = %d, want %d", parsed.Unk, tt.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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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 Parse for MsgSysUnlockStage.
|
|
// This struct has no exported fields; Parse only discards a single zeroed uint16.
|
|
func TestBuildParseUnlockStage(t *testing.T) {
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(0) // Zeroed (discarded by Parse)
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
|
|
parsed := &MsgSysUnlockStage{}
|
|
if err := parsed.Parse(bf, ctx); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// 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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
|
|
// 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
|
|
// manual-build/Parse for MsgSysOperateRegister.
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
func TestBuildParseOperateRegisterPayloadIntegrity(t *testing.T) {
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
|
|
payload := make([]byte, 512)
|
|
for i := range payload {
|
|
payload[i] = byte((i * 7) % 256) // Non-trivial pattern
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(0xAABBCCDD) // AckHandle
|
|
bf.WriteUint32(42) // SemaphoreID
|
|
bf.WriteUint16(0) // Zeroed
|
|
bf.WriteUint16(uint16(len(payload))) // dataSize
|
|
bf.WriteBytes(payload)
|
|
|
|
_, _ = 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 uint8 count field should be 0).
|
|
// Build is NOT IMPLEMENTED, so we manually write the binary representation.
|
|
// Parse reads: uint32 AckHandle, uint32 GuildID, uint8 zeroed, uint8 charCount.
|
|
func TestBuildParseArrangeGuildMemberEmptySlice(t *testing.T) {
|
|
ctx := &clientctx.ClientContext{RealClientMode: cfg.ZZ}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(1) // AckHandle
|
|
bf.WriteUint32(100) // GuildID
|
|
bf.WriteUint8(0) // Zeroed
|
|
bf.WriteUint8(0) // charCount = 0
|
|
|
|
// Verify the wire size: uint32 + uint32 + uint8 + uint8 = 10 bytes
|
|
if len(bf.Data()) != 10 {
|
|
t.Errorf("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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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{RealClientMode: cfg.ZZ}
|
|
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)
|
|
}
|
|
}
|