diff --git a/network/mhfpacket/msg_build_test.go b/network/mhfpacket/msg_build_test.go new file mode 100644 index 000000000..f90c296e1 --- /dev/null +++ b/network/mhfpacket/msg_build_test.go @@ -0,0 +1,1512 @@ +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) + } +} diff --git a/network/mhfpacket/msg_opcode_coverage_test.go b/network/mhfpacket/msg_opcode_coverage_test.go new file mode 100644 index 000000000..b88070d17 --- /dev/null +++ b/network/mhfpacket/msg_opcode_coverage_test.go @@ -0,0 +1,301 @@ +package mhfpacket + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// callBuildSafe calls Build on the packet, recovering from panics. +// Returns the error from Build, or nil if it panicked (panic is acceptable +// for "Not implemented" stubs). +func callBuildSafe(pkt MHFPacket, bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) (err error, panicked bool) { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + err = pkt.Build(bf, ctx) + return err, false +} + +// callParseSafe calls Parse on the packet, recovering from panics. +func callParseSafe(pkt MHFPacket, bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) (err error, panicked bool) { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + err = pkt.Parse(bf, ctx) + return err, false +} + +// TestBuildCoverage_NotImplemented exercises Build() on packet types whose Build +// method is not yet covered. These stubs either return errors.New("NOT IMPLEMENTED") +// or panic("Not implemented"). Both are acceptable outcomes that indicate the +// method was reached. +func TestBuildCoverage_NotImplemented(t *testing.T) { + tests := []struct { + name string + pkt MHFPacket + }{ + // msg_ca_exchange_item.go + {"MsgCaExchangeItem", &MsgCaExchangeItem{}}, + // msg_head.go + {"MsgHead", &MsgHead{}}, + // msg_mhf_acquire_cafe_item.go + {"MsgMhfAcquireCafeItem", &MsgMhfAcquireCafeItem{}}, + // msg_mhf_acquire_monthly_item.go + {"MsgMhfAcquireMonthlyItem", &MsgMhfAcquireMonthlyItem{}}, + // msg_mhf_acquire_ud_item.go + {"MsgMhfAcquireUdItem", &MsgMhfAcquireUdItem{}}, + // msg_mhf_announce.go + {"MsgMhfAnnounce", &MsgMhfAnnounce{}}, + // msg_mhf_check_monthly_item.go + {"MsgMhfCheckMonthlyItem", &MsgMhfCheckMonthlyItem{}}, + // msg_mhf_check_weekly_stamp.go + {"MsgMhfCheckWeeklyStamp", &MsgMhfCheckWeeklyStamp{}}, + // msg_mhf_enumerate_festa_member.go + {"MsgMhfEnumerateFestaMember", &MsgMhfEnumerateFestaMember{}}, + // msg_mhf_enumerate_inv_guild.go + {"MsgMhfEnumerateInvGuild", &MsgMhfEnumerateInvGuild{}}, + // msg_mhf_enumerate_item.go + {"MsgMhfEnumerateItem", &MsgMhfEnumerateItem{}}, + // msg_mhf_enumerate_order.go + {"MsgMhfEnumerateOrder", &MsgMhfEnumerateOrder{}}, + // msg_mhf_enumerate_quest.go + {"MsgMhfEnumerateQuest", &MsgMhfEnumerateQuest{}}, + // msg_mhf_enumerate_ranking.go + {"MsgMhfEnumerateRanking", &MsgMhfEnumerateRanking{}}, + // msg_mhf_enumerate_shop.go + {"MsgMhfEnumerateShop", &MsgMhfEnumerateShop{}}, + // msg_mhf_enumerate_warehouse.go + {"MsgMhfEnumerateWarehouse", &MsgMhfEnumerateWarehouse{}}, + // msg_mhf_exchange_fpoint_2_item.go + {"MsgMhfExchangeFpoint2Item", &MsgMhfExchangeFpoint2Item{}}, + // msg_mhf_exchange_item_2_fpoint.go + {"MsgMhfExchangeItem2Fpoint", &MsgMhfExchangeItem2Fpoint{}}, + // msg_mhf_exchange_weekly_stamp.go + {"MsgMhfExchangeWeeklyStamp", &MsgMhfExchangeWeeklyStamp{}}, + // msg_mhf_generate_ud_guild_map.go + {"MsgMhfGenerateUdGuildMap", &MsgMhfGenerateUdGuildMap{}}, + // msg_mhf_get_boost_time.go + {"MsgMhfGetBoostTime", &MsgMhfGetBoostTime{}}, + // msg_mhf_get_boost_time_limit.go + {"MsgMhfGetBoostTimeLimit", &MsgMhfGetBoostTimeLimit{}}, + // msg_mhf_get_cafe_duration.go + {"MsgMhfGetCafeDuration", &MsgMhfGetCafeDuration{}}, + // msg_mhf_get_cafe_duration_bonus_info.go + {"MsgMhfGetCafeDurationBonusInfo", &MsgMhfGetCafeDurationBonusInfo{}}, + // msg_mhf_get_cog_info.go + {"MsgMhfGetCogInfo", &MsgMhfGetCogInfo{}}, + // msg_mhf_get_gacha_point.go + {"MsgMhfGetGachaPoint", &MsgMhfGetGachaPoint{}}, + // msg_mhf_get_gem_info.go + {"MsgMhfGetGemInfo", &MsgMhfGetGemInfo{}}, + // msg_mhf_get_kiju_info.go + {"MsgMhfGetKijuInfo", &MsgMhfGetKijuInfo{}}, + // msg_mhf_get_myhouse_info.go + {"MsgMhfGetMyhouseInfo", &MsgMhfGetMyhouseInfo{}}, + // msg_mhf_get_notice.go + {"MsgMhfGetNotice", &MsgMhfGetNotice{}}, + // msg_mhf_get_tower_info.go + {"MsgMhfGetTowerInfo", &MsgMhfGetTowerInfo{}}, + // msg_mhf_get_ud_info.go + {"MsgMhfGetUdInfo", &MsgMhfGetUdInfo{}}, + // msg_mhf_get_ud_schedule.go + {"MsgMhfGetUdSchedule", &MsgMhfGetUdSchedule{}}, + // msg_mhf_get_weekly_schedule.go + {"MsgMhfGetWeeklySchedule", &MsgMhfGetWeeklySchedule{}}, + // msg_mhf_guild_huntdata.go + {"MsgMhfGuildHuntdata", &MsgMhfGuildHuntdata{}}, + // msg_mhf_info_joint.go + {"MsgMhfInfoJoint", &MsgMhfInfoJoint{}}, + // msg_mhf_load_deco_myset.go + {"MsgMhfLoadDecoMyset", &MsgMhfLoadDecoMyset{}}, + // msg_mhf_load_guild_adventure.go + {"MsgMhfLoadGuildAdventure", &MsgMhfLoadGuildAdventure{}}, + // msg_mhf_load_guild_cooking.go + {"MsgMhfLoadGuildCooking", &MsgMhfLoadGuildCooking{}}, + // msg_mhf_load_hunter_navi.go + {"MsgMhfLoadHunterNavi", &MsgMhfLoadHunterNavi{}}, + // msg_mhf_load_otomo_airou.go + {"MsgMhfLoadOtomoAirou", &MsgMhfLoadOtomoAirou{}}, + // msg_mhf_load_partner.go + {"MsgMhfLoadPartner", &MsgMhfLoadPartner{}}, + // msg_mhf_load_plate_box.go + {"MsgMhfLoadPlateBox", &MsgMhfLoadPlateBox{}}, + // msg_mhf_load_plate_data.go + {"MsgMhfLoadPlateData", &MsgMhfLoadPlateData{}}, + // msg_mhf_post_notice.go + {"MsgMhfPostNotice", &MsgMhfPostNotice{}}, + // msg_mhf_post_tower_info.go + {"MsgMhfPostTowerInfo", &MsgMhfPostTowerInfo{}}, + // msg_mhf_reserve10f.go + {"MsgMhfReserve10F", &MsgMhfReserve10F{}}, + // msg_mhf_server_command.go + {"MsgMhfServerCommand", &MsgMhfServerCommand{}}, + // msg_mhf_set_loginwindow.go + {"MsgMhfSetLoginwindow", &MsgMhfSetLoginwindow{}}, + // msg_mhf_shut_client.go + {"MsgMhfShutClient", &MsgMhfShutClient{}}, + // msg_mhf_stampcard_stamp.go + {"MsgMhfStampcardStamp", &MsgMhfStampcardStamp{}}, + // msg_sys_add_object.go + {"MsgSysAddObject", &MsgSysAddObject{}}, + // msg_sys_back_stage.go + {"MsgSysBackStage", &MsgSysBackStage{}}, + // msg_sys_cast_binary.go + {"MsgSysCastBinary", &MsgSysCastBinary{}}, + // msg_sys_create_semaphore.go + {"MsgSysCreateSemaphore", &MsgSysCreateSemaphore{}}, + // msg_sys_create_stage.go + {"MsgSysCreateStage", &MsgSysCreateStage{}}, + // msg_sys_del_object.go + {"MsgSysDelObject", &MsgSysDelObject{}}, + // msg_sys_disp_object.go + {"MsgSysDispObject", &MsgSysDispObject{}}, + // msg_sys_echo.go + {"MsgSysEcho", &MsgSysEcho{}}, + // msg_sys_enter_stage.go + {"MsgSysEnterStage", &MsgSysEnterStage{}}, + // msg_sys_enumerate_client.go + {"MsgSysEnumerateClient", &MsgSysEnumerateClient{}}, + // msg_sys_extend_threshold.go + {"MsgSysExtendThreshold", &MsgSysExtendThreshold{}}, + // msg_sys_get_stage_binary.go + {"MsgSysGetStageBinary", &MsgSysGetStageBinary{}}, + // msg_sys_hide_object.go + {"MsgSysHideObject", &MsgSysHideObject{}}, + // msg_sys_leave_stage.go + {"MsgSysLeaveStage", &MsgSysLeaveStage{}}, + // msg_sys_lock_stage.go + {"MsgSysLockStage", &MsgSysLockStage{}}, + // msg_sys_login.go + {"MsgSysLogin", &MsgSysLogin{}}, + // msg_sys_move_stage.go + {"MsgSysMoveStage", &MsgSysMoveStage{}}, + // msg_sys_set_stage_binary.go + {"MsgSysSetStageBinary", &MsgSysSetStageBinary{}}, + // msg_sys_set_stage_pass.go + {"MsgSysSetStagePass", &MsgSysSetStagePass{}}, + // msg_sys_set_status.go + {"MsgSysSetStatus", &MsgSysSetStatus{}}, + // msg_sys_wait_stage_binary.go + {"MsgSysWaitStageBinary", &MsgSysWaitStageBinary{}}, + + // Reserve files - sys reserves + {"MsgSysReserve01", &MsgSysReserve01{}}, + {"MsgSysReserve02", &MsgSysReserve02{}}, + {"MsgSysReserve03", &MsgSysReserve03{}}, + {"MsgSysReserve04", &MsgSysReserve04{}}, + {"MsgSysReserve05", &MsgSysReserve05{}}, + {"MsgSysReserve06", &MsgSysReserve06{}}, + {"MsgSysReserve07", &MsgSysReserve07{}}, + {"MsgSysReserve0C", &MsgSysReserve0C{}}, + {"MsgSysReserve0D", &MsgSysReserve0D{}}, + {"MsgSysReserve0E", &MsgSysReserve0E{}}, + {"MsgSysReserve4A", &MsgSysReserve4A{}}, + {"MsgSysReserve4B", &MsgSysReserve4B{}}, + {"MsgSysReserve4C", &MsgSysReserve4C{}}, + {"MsgSysReserve4D", &MsgSysReserve4D{}}, + {"MsgSysReserve4E", &MsgSysReserve4E{}}, + {"MsgSysReserve4F", &MsgSysReserve4F{}}, + {"MsgSysReserve55", &MsgSysReserve55{}}, + {"MsgSysReserve56", &MsgSysReserve56{}}, + {"MsgSysReserve57", &MsgSysReserve57{}}, + {"MsgSysReserve5C", &MsgSysReserve5C{}}, + {"MsgSysReserve5E", &MsgSysReserve5E{}}, + {"MsgSysReserve5F", &MsgSysReserve5F{}}, + {"MsgSysReserve71", &MsgSysReserve71{}}, + {"MsgSysReserve72", &MsgSysReserve72{}}, + {"MsgSysReserve73", &MsgSysReserve73{}}, + {"MsgSysReserve74", &MsgSysReserve74{}}, + {"MsgSysReserve75", &MsgSysReserve75{}}, + {"MsgSysReserve76", &MsgSysReserve76{}}, + {"MsgSysReserve77", &MsgSysReserve77{}}, + {"MsgSysReserve78", &MsgSysReserve78{}}, + {"MsgSysReserve79", &MsgSysReserve79{}}, + {"MsgSysReserve7A", &MsgSysReserve7A{}}, + {"MsgSysReserve7B", &MsgSysReserve7B{}}, + {"MsgSysReserve7C", &MsgSysReserve7C{}}, + {"MsgSysReserve7E", &MsgSysReserve7E{}}, + {"MsgSysReserve180", &MsgSysReserve180{}}, + {"MsgSysReserve188", &MsgSysReserve188{}}, + {"MsgSysReserve18B", &MsgSysReserve18B{}}, + {"MsgSysReserve18E", &MsgSysReserve18E{}}, + {"MsgSysReserve18F", &MsgSysReserve18F{}}, + {"MsgSysReserve192", &MsgSysReserve192{}}, + {"MsgSysReserve193", &MsgSysReserve193{}}, + {"MsgSysReserve194", &MsgSysReserve194{}}, + {"MsgSysReserve19B", &MsgSysReserve19B{}}, + {"MsgSysReserve19E", &MsgSysReserve19E{}}, + {"MsgSysReserve19F", &MsgSysReserve19F{}}, + {"MsgSysReserve1A4", &MsgSysReserve1A4{}}, + {"MsgSysReserve1A6", &MsgSysReserve1A6{}}, + {"MsgSysReserve1A7", &MsgSysReserve1A7{}}, + {"MsgSysReserve1A8", &MsgSysReserve1A8{}}, + {"MsgSysReserve1A9", &MsgSysReserve1A9{}}, + {"MsgSysReserve1AA", &MsgSysReserve1AA{}}, + {"MsgSysReserve1AB", &MsgSysReserve1AB{}}, + {"MsgSysReserve1AC", &MsgSysReserve1AC{}}, + {"MsgSysReserve1AD", &MsgSysReserve1AD{}}, + {"MsgSysReserve1AE", &MsgSysReserve1AE{}}, + {"MsgSysReserve1AF", &MsgSysReserve1AF{}}, + } + + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, panicked := callBuildSafe(tt.pkt, bf, ctx) + if panicked { + // Build panicked with "Not implemented" - this is acceptable + // and still exercises the code path for coverage. + return + } + if err == nil { + // Build succeeded (some packets may have implemented Build) + return + } + // Build returned an error, which is expected for NOT IMPLEMENTED stubs + if err.Error() != "NOT IMPLEMENTED" { + t.Errorf("Build() returned unexpected error: %v", err) + } + }) + } +} + +// TestParseCoverage_NotImplemented exercises Parse() on packet types whose Parse +// method returns "NOT IMPLEMENTED" and is not yet covered by existing tests. +func TestParseCoverage_NotImplemented(t *testing.T) { + tests := []struct { + name string + pkt MHFPacket + }{ + // msg_mhf_acquire_tournament.go - Parse returns NOT IMPLEMENTED + {"MsgMhfAcquireTournament", &MsgMhfAcquireTournament{}}, + // msg_mhf_entry_tournament.go - Parse returns NOT IMPLEMENTED + {"MsgMhfEntryTournament", &MsgMhfEntryTournament{}}, + // msg_mhf_update_guild.go - Parse returns NOT IMPLEMENTED + {"MsgMhfUpdateGuild", &MsgMhfUpdateGuild{}}, + } + + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, panicked := callParseSafe(tt.pkt, bf, ctx) + if panicked { + return + } + if err == nil { + return + } + if err.Error() != "NOT IMPLEMENTED" { + t.Errorf("Parse() returned unexpected error: %v", err) + } + }) + } +} diff --git a/network/mhfpacket/msg_parse_large_test.go b/network/mhfpacket/msg_parse_large_test.go new file mode 100644 index 000000000..254a7efa3 --- /dev/null +++ b/network/mhfpacket/msg_parse_large_test.go @@ -0,0 +1,1259 @@ +package mhfpacket + +import ( + "bytes" + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// TestParseLargeMsgMhfUpdateWarehouseItem tests Parse for MsgMhfUpdateWarehouse with item box type. +func TestParseLargeMsgMhfUpdateWarehouseItem(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11223344) // AckHandle + bf.WriteUint8(0) // BoxType = item + bf.WriteUint8(3) // BoxIndex + bf.WriteUint16(2) // changes count + // Item stack 1 + bf.WriteUint32(100) // ID + bf.WriteUint16(0) // Index + bf.WriteUint16(1001) // ItemID + bf.WriteUint16(5) // Quantity + bf.WriteUint16(0) // Unk + // Item stack 2 + bf.WriteUint32(200) // ID + bf.WriteUint16(1) // Index + bf.WriteUint16(2002) // ItemID + bf.WriteUint16(10) // Quantity + bf.WriteUint16(0) // Unk + bf.WriteUint16(0) // trailing Unk + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateWarehouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x11223344 { + t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle) + } + if pkt.BoxType != "item" { + t.Errorf("BoxType = %q, want %q", pkt.BoxType, "item") + } + if pkt.BoxIndex != 3 { + t.Errorf("BoxIndex = %d, want 3", pkt.BoxIndex) + } + if len(pkt.Updates) != 2 { + t.Fatalf("Updates len = %d, want 2", len(pkt.Updates)) + } + if pkt.Updates[0].ID != 100 || pkt.Updates[0].ItemID != 1001 || pkt.Updates[0].Quantity != 5 { + t.Errorf("Updates[0] = %+v", pkt.Updates[0]) + } + if pkt.Updates[1].ID != 200 || pkt.Updates[1].ItemID != 2002 || pkt.Updates[1].Quantity != 10 { + t.Errorf("Updates[1] = %+v", pkt.Updates[1]) + } +} + +// TestParseLargeMsgMhfUpdateWarehouseEquip tests Parse for MsgMhfUpdateWarehouse with equip box type. +func TestParseLargeMsgMhfUpdateWarehouseEquip(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint8(1) // BoxType = equip + bf.WriteUint8(0) // BoxIndex + bf.WriteUint16(1) // changes count + // Equip stack + bf.WriteUint32(42) // ID + bf.WriteUint16(5) // Index + bf.WriteUint16(3) // EquipType + bf.WriteUint16(500) // ItemID + equipData := make([]byte, 56) + for i := range equipData { + equipData[i] = byte(i) + } + bf.WriteBytes(equipData) // Data (56 bytes) + bf.WriteUint16(0) // trailing Unk + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateWarehouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.BoxType != "equip" { + t.Errorf("BoxType = %q, want %q", pkt.BoxType, "equip") + } + if len(pkt.Updates) != 1 { + t.Fatalf("Updates len = %d, want 1", len(pkt.Updates)) + } + if pkt.Updates[0].ID != 42 { + t.Errorf("Updates[0].ID = %d, want 42", pkt.Updates[0].ID) + } + if pkt.Updates[0].EquipType != 3 { + t.Errorf("Updates[0].EquipType = %d, want 3", pkt.Updates[0].EquipType) + } + if pkt.Updates[0].ItemID != 500 { + t.Errorf("Updates[0].ItemID = %d, want 500", pkt.Updates[0].ItemID) + } + if !bytes.Equal(pkt.Updates[0].Data, equipData) { + t.Errorf("Updates[0].Data mismatch") + } +} + +// TestParseLargeMsgMhfUpdateWarehouseEmpty tests Parse for MsgMhfUpdateWarehouse with zero changes. +func TestParseLargeMsgMhfUpdateWarehouseEmpty(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // BoxType = item + bf.WriteUint8(0) // BoxIndex + bf.WriteUint16(0) // changes count = 0 + bf.WriteUint16(0) // trailing Unk + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateWarehouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + if len(pkt.Updates) != 0 { + t.Errorf("Updates len = %d, want 0", len(pkt.Updates)) + } +} + +// TestParseLargeMsgSysUpdateRightBuild tests Build for MsgSysUpdateRight (no Parse implementation). +func TestParseLargeMsgSysUpdateRightBuild(t *testing.T) { + ctx := &clientctx.ClientContext{} + original := &MsgSysUpdateRight{ + ClientRespAckHandle: 0x12345678, + Bitfield: 0xDEADBEEF, + Rights: nil, + UnkSize: 0, + } + + bf := byteframe.NewByteFrame() + if err := original.Build(bf, ctx); err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Verify binary output manually: + // uint32 ClientRespAckHandle + uint32 Bitfield + uint16 Rights count(0) + uint16 padding(0) + ps.Uint16 empty string(uint16(1) + 0x00) + data := bf.Data() + if len(data) < 12 { + t.Fatalf("Build() wrote %d bytes, want at least 12", len(data)) + } + + bf.Seek(0, io.SeekStart) + if bf.ReadUint32() != 0x12345678 { + t.Error("ClientRespAckHandle mismatch") + } + if bf.ReadUint32() != 0xDEADBEEF { + t.Error("Bitfield mismatch") + } + if bf.ReadUint16() != 0 { + t.Error("Rights count should be 0") + } +} + +// TestParseLargeMsgMhfOperateWarehouse tests Parse for MsgMhfOperateWarehouse. +func TestParseLargeMsgMhfOperateWarehouse(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint8(1) // Operation + bf.WriteUint8(0) // BoxType = item + bf.WriteUint8(2) // BoxIndex + bf.WriteUint8(8) // lenName (unused but read) + bf.WriteUint16(0) // Unk + bf.WriteBytes([]byte("TestBox")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfOperateWarehouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.Operation != 1 { + t.Errorf("Operation = %d, want 1", pkt.Operation) + } + if pkt.BoxType != "item" { + t.Errorf("BoxType = %q, want %q", pkt.BoxType, "item") + } + if pkt.BoxIndex != 2 { + t.Errorf("BoxIndex = %d, want 2", pkt.BoxIndex) + } + if pkt.Name != "TestBox" { + t.Errorf("Name = %q, want %q", pkt.Name, "TestBox") + } +} + +// TestParseLargeMsgMhfOperateWarehouseEquip tests Parse for MsgMhfOperateWarehouse with equip box type. +func TestParseLargeMsgMhfOperateWarehouseEquip(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(42) // AckHandle + bf.WriteUint8(2) // Operation + bf.WriteUint8(1) // BoxType = equip + bf.WriteUint8(0) // BoxIndex + bf.WriteUint8(5) // lenName + bf.WriteUint16(0) // Unk + bf.WriteBytes([]byte("Arms")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfOperateWarehouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.BoxType != "equip" { + t.Errorf("BoxType = %q, want %q", pkt.BoxType, "equip") + } + if pkt.Name != "Arms" { + t.Errorf("Name = %q, want %q", pkt.Name, "Arms") + } +} + +// TestParseLargeMsgMhfUpdateGuildItem tests Parse for MsgMhfUpdateGuildItem. +func TestParseLargeMsgMhfUpdateGuildItem(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(500) // GuildId + bf.WriteUint16(2) // Amount + bf.WriteUint16(0) // Unk1 + // Item 1 + bf.WriteUint32(10) // Unk0 + bf.WriteUint16(1001) // ItemId + bf.WriteUint16(5) // Amount + bf.WriteUint32(0) // Unk1 + // Item 2 + bf.WriteUint32(20) // Unk0 + bf.WriteUint16(2002) // ItemId + bf.WriteUint16(99) // Amount + bf.WriteUint32(0) // Unk1 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateGuildItem{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.GuildId != 500 { + t.Errorf("GuildId = %d, want 500", pkt.GuildId) + } + if pkt.Amount != 2 { + t.Errorf("Amount = %d, want 2", pkt.Amount) + } + if len(pkt.Items) != 2 { + t.Fatalf("Items len = %d, want 2", len(pkt.Items)) + } + if pkt.Items[0].ItemId != 1001 || pkt.Items[0].Amount != 5 { + t.Errorf("Items[0] = %+v", pkt.Items[0]) + } + if pkt.Items[1].ItemId != 2002 || pkt.Items[1].Amount != 99 { + t.Errorf("Items[1] = %+v", pkt.Items[1]) + } +} + +// TestParseLargeMsgMhfUpdateGuildItemEmpty tests Parse for MsgMhfUpdateGuildItem with zero items. +func TestParseLargeMsgMhfUpdateGuildItemEmpty(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(0) // GuildId + bf.WriteUint16(0) // Amount = 0 + bf.WriteUint16(0) // Unk1 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateGuildItem{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + if len(pkt.Items) != 0 { + t.Errorf("Items len = %d, want 0", len(pkt.Items)) + } +} + +// TestParseLargeMsgMhfUpdateUnionItem tests Parse for MsgMhfUpdateUnionItem. +func TestParseLargeMsgMhfUpdateUnionItem(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xDEADBEEF) // AckHandle + bf.WriteUint16(3) // Amount + bf.WriteUint16(0) // Unk1 + // Item 1 + bf.WriteUint32(1) // Unk0 + bf.WriteUint16(100) // ItemId + bf.WriteUint16(10) // Amount + bf.WriteUint32(0) // Unk1 + // Item 2 + bf.WriteUint32(2) // Unk0 + bf.WriteUint16(200) // ItemId + bf.WriteUint16(20) // Amount + bf.WriteUint32(0) // Unk1 + // Item 3 + bf.WriteUint32(3) // Unk0 + bf.WriteUint16(300) // ItemId + bf.WriteUint16(30) // Amount + bf.WriteUint32(0) // Unk1 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateUnionItem{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xDEADBEEF { + t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle) + } + if pkt.Amount != 3 { + t.Errorf("Amount = %d, want 3", pkt.Amount) + } + if len(pkt.Items) != 3 { + t.Fatalf("Items len = %d, want 3", len(pkt.Items)) + } + for i, expected := range []struct { + itemID uint16 + amount uint16 + }{{100, 10}, {200, 20}, {300, 30}} { + if pkt.Items[i].ItemId != expected.itemID { + t.Errorf("Items[%d].ItemId = %d, want %d", i, pkt.Items[i].ItemId, expected.itemID) + } + if pkt.Items[i].Amount != expected.amount { + t.Errorf("Items[%d].Amount = %d, want %d", i, pkt.Items[i].Amount, expected.amount) + } + } +} + +// TestParseLargeMsgMhfUpdateBeatLevel tests Parse for MsgMhfUpdateBeatLevel. +func TestParseLargeMsgMhfUpdateBeatLevel(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(100) // Unk1 + bf.WriteUint32(200) // Unk2 + monsterData := make([]byte, 120) + for i := range monsterData { + monsterData[i] = byte(i % 256) + } + bf.WriteBytes(monsterData) // MonsterData (120 bytes) + bf.WriteUint8(5) // Unk3 + bf.WriteUint32(300) // Unk4 + bf.WriteUint16(400) // Unk5 + bf.WriteUint8(6) // Unk6 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateBeatLevel{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Unk1 != 100 { + t.Errorf("Unk1 = %d, want 100", pkt.Unk1) + } + if pkt.Unk2 != 200 { + t.Errorf("Unk2 = %d, want 200", pkt.Unk2) + } + if !bytes.Equal(pkt.MonsterData, monsterData) { + t.Error("MonsterData mismatch") + } + if pkt.Unk3 != 5 { + t.Errorf("Unk3 = %d, want 5", pkt.Unk3) + } + if pkt.Unk4 != 300 { + t.Errorf("Unk4 = %d, want 300", pkt.Unk4) + } + if pkt.Unk5 != 400 { + t.Errorf("Unk5 = %d, want 400", pkt.Unk5) + } + if pkt.Unk6 != 6 { + t.Errorf("Unk6 = %d, want 6", pkt.Unk6) + } +} + +// TestParseLargeMsgMhfLoadHouse tests Parse for MsgMhfLoadHouse. +func TestParseLargeMsgMhfLoadHouse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + charID uint32 + destination uint8 + checkPass bool + password string + }{ + {"with password", 0xAABBCCDD, 12345, 1, true, "pass123"}, + {"no password", 0x11111111, 0, 0, false, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.charID) + bf.WriteUint8(tt.destination) + bf.WriteBool(tt.checkPass) + bf.WriteUint16(0) // Unk (hardcoded 0) + bf.WriteUint8(uint8(len(tt.password) + 1)) // Password length + bf.WriteBytes([]byte(tt.password)) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfLoadHouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ackHandle) + } + if pkt.CharID != tt.charID { + t.Errorf("CharID = %d, want %d", pkt.CharID, tt.charID) + } + if pkt.Destination != tt.destination { + t.Errorf("Destination = %d, want %d", pkt.Destination, tt.destination) + } + if pkt.CheckPass != tt.checkPass { + t.Errorf("CheckPass = %v, want %v", pkt.CheckPass, tt.checkPass) + } + if pkt.Password != tt.password { + t.Errorf("Password = %q, want %q", pkt.Password, tt.password) + } + }) + } +} + +// TestParseLargeMsgMhfSendMail tests Parse for MsgMhfSendMail. +func TestParseLargeMsgMhfSendMail(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(99999) // RecipientID + bf.WriteUint16(6) // SubjectLength + bf.WriteUint16(12) // BodyLength + bf.WriteUint32(5) // Quantity + bf.WriteUint16(1001) // ItemID + bf.WriteBytes([]byte("Hello")) + bf.WriteUint8(0) // null terminator for Subject + bf.WriteBytes([]byte("Hello World")) + bf.WriteUint8(0) // null terminator for Body + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfSendMail{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.RecipientID != 99999 { + t.Errorf("RecipientID = %d, want 99999", pkt.RecipientID) + } + if pkt.SubjectLength != 6 { + t.Errorf("SubjectLength = %d, want 6", pkt.SubjectLength) + } + if pkt.BodyLength != 12 { + t.Errorf("BodyLength = %d, want 12", pkt.BodyLength) + } + if pkt.Quantity != 5 { + t.Errorf("Quantity = %d, want 5", pkt.Quantity) + } + if pkt.ItemID != 1001 { + t.Errorf("ItemID = %d, want 1001", pkt.ItemID) + } + if pkt.Subject != "Hello" { + t.Errorf("Subject = %q, want %q", pkt.Subject, "Hello") + } + if pkt.Body != "Hello World" { + t.Errorf("Body = %q, want %q", pkt.Body, "Hello World") + } +} + +// TestParseLargeMsgMhfApplyBbsArticle tests Parse for MsgMhfApplyBbsArticle. +func TestParseLargeMsgMhfApplyBbsArticle(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xCAFEBABE) // AckHandle + bf.WriteUint32(42) // Unk0 + + // Unk1: 16 bytes + unk1 := make([]byte, 16) + for i := range unk1 { + unk1[i] = byte(i + 1) + } + bf.WriteBytes(unk1) + + // Name: 32 bytes (padded with nulls) - uses bfutil.UpToNull + nameBytes := make([]byte, 32) + copy(nameBytes, "Hunter") + bf.WriteBytes(nameBytes) + + // Title: 128 bytes (padded with nulls) + titleBytes := make([]byte, 128) + copy(titleBytes, "My Post Title") + bf.WriteBytes(titleBytes) + + // Description: 256 bytes (padded with nulls) + descBytes := make([]byte, 256) + copy(descBytes, "This is a description") + bf.WriteBytes(descBytes) + + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfApplyBbsArticle{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xCAFEBABE { + t.Errorf("AckHandle = 0x%X, want 0xCAFEBABE", pkt.AckHandle) + } + if pkt.Unk0 != 42 { + t.Errorf("Unk0 = %d, want 42", pkt.Unk0) + } + if !bytes.Equal(pkt.Unk1, unk1) { + t.Error("Unk1 mismatch") + } + if pkt.Name != "Hunter" { + t.Errorf("Name = %q, want %q", pkt.Name, "Hunter") + } + if pkt.Title != "My Post Title" { + t.Errorf("Title = %q, want %q", pkt.Title, "My Post Title") + } + if pkt.Description != "This is a description" { + t.Errorf("Description = %q, want %q", pkt.Description, "This is a description") + } +} + +// TestParseLargeMsgMhfChargeFesta tests Parse for MsgMhfChargeFesta. +func TestParseLargeMsgMhfChargeFesta(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11223344) // AckHandle + bf.WriteUint32(100) // FestaID + bf.WriteUint32(200) // GuildID + bf.WriteUint16(3) // soul count + bf.WriteUint16(10) // soul value 1 + bf.WriteUint16(20) // soul value 2 + bf.WriteUint16(30) // soul value 3 + bf.WriteUint8(0) // Unk + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfChargeFesta{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x11223344 { + t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle) + } + if pkt.FestaID != 100 { + t.Errorf("FestaID = %d, want 100", pkt.FestaID) + } + if pkt.GuildID != 200 { + t.Errorf("GuildID = %d, want 200", pkt.GuildID) + } + if pkt.Souls != 60 { + t.Errorf("Souls = %d, want 60 (10+20+30)", pkt.Souls) + } +} + +// TestParseLargeMsgMhfChargeFestaZeroSouls tests Parse for MsgMhfChargeFesta with zero soul entries. +func TestParseLargeMsgMhfChargeFestaZeroSouls(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(0) // FestaID + bf.WriteUint32(0) // GuildID + bf.WriteUint16(0) // soul count = 0 + bf.WriteUint8(0) // Unk + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfChargeFesta{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + if pkt.Souls != 0 { + t.Errorf("Souls = %d, want 0", pkt.Souls) + } +} + +// TestParseLargeMsgMhfEnumerateGuild tests Parse for MsgMhfEnumerateGuild. +func TestParseLargeMsgMhfEnumerateGuild(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xDEADBEEF) // AckHandle + bf.WriteUint8(0x01) // Type = ENUMERATE_GUILD_TYPE_GUILD_NAME + bf.WriteUint8(3) // Page + bf.WriteBool(true) // Sorting + bf.WriteUint8(0) // Unk (skipped) + // Some raw data payload followed by 2 trailing bytes (the seek goes to len-2) + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.WriteUint16(0) // trailing 2 bytes (seek target) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfEnumerateGuild{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xDEADBEEF { + t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle) + } + if pkt.Type != ENUMERATE_GUILD_TYPE_GUILD_NAME { + t.Errorf("Type = %d, want %d", pkt.Type, ENUMERATE_GUILD_TYPE_GUILD_NAME) + } + if pkt.Page != 3 { + t.Errorf("Page = %d, want 3", pkt.Page) + } + if !pkt.Sorting { + t.Error("Sorting = false, want true") + } + // RawDataPayload is DataFromCurrent() at the point after reading the 4 header fields + // It should contain the remaining bytes: 0xAA, 0xBB, 0xCC, 0x00, 0x00 + if len(pkt.RawDataPayload) != 5 { + t.Errorf("RawDataPayload len = %d, want 5", len(pkt.RawDataPayload)) + } +} + +// TestParseLargeMsgMhfOperateJoint tests Parse for MsgMhfOperateJoint. +func TestParseLargeMsgMhfOperateJoint(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(100) // AllianceID + bf.WriteUint32(200) // GuildID + bf.WriteUint8(0x01) // Action = OPERATE_JOINT_DISBAND + // Some extra data followed by 2 trailing bytes + bf.WriteBytes([]byte{0x01, 0x02}) + bf.WriteUint16(0) // trailing bytes needed for seek + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfOperateJoint{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.AllianceID != 100 { + t.Errorf("AllianceID = %d, want 100", pkt.AllianceID) + } + if pkt.GuildID != 200 { + t.Errorf("GuildID = %d, want 200", pkt.GuildID) + } + if pkt.Action != OPERATE_JOINT_DISBAND { + t.Errorf("Action = %d, want %d", pkt.Action, OPERATE_JOINT_DISBAND) + } + if pkt.UnkData == nil { + t.Fatal("UnkData is nil") + } +} + +// TestParseLargeMsgMhfOperationInvGuild tests Parse for MsgMhfOperationInvGuild. +func TestParseLargeMsgMhfOperationInvGuild(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint8(1) // Operation + bf.WriteUint8(5) // ActiveHours + bf.WriteUint8(7) // DaysActive + bf.WriteUint8(3) // PlayStyle + bf.WriteUint8(2) // GuildRequest + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfOperationInvGuild{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.Operation != 1 { + t.Errorf("Operation = %d, want 1", pkt.Operation) + } + if pkt.ActiveHours != 5 { + t.Errorf("ActiveHours = %d, want 5", pkt.ActiveHours) + } + if pkt.DaysActive != 7 { + t.Errorf("DaysActive = %d, want 7", pkt.DaysActive) + } + if pkt.PlayStyle != 3 { + t.Errorf("PlayStyle = %d, want 3", pkt.PlayStyle) + } + if pkt.GuildRequest != 2 { + t.Errorf("GuildRequest = %d, want 2", pkt.GuildRequest) + } +} + +// TestParseLargeMsgMhfSaveMercenary tests Parse for MsgMhfSaveMercenary. +func TestParseLargeMsgMhfSaveMercenary(t *testing.T) { + mercData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xCAFEBABE) // AckHandle + bf.WriteUint32(0) // lenData (skipped) + bf.WriteUint32(5000) // GCP + bf.WriteUint32(42) // PactMercID + bf.WriteUint32(uint32(len(mercData))) // dataSize + bf.WriteUint32(0) // Merc index (skipped) + bf.WriteBytes(mercData) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfSaveMercenary{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xCAFEBABE { + t.Errorf("AckHandle = 0x%X, want 0xCAFEBABE", pkt.AckHandle) + } + if pkt.GCP != 5000 { + t.Errorf("GCP = %d, want 5000", pkt.GCP) + } + if pkt.PactMercID != 42 { + t.Errorf("PactMercID = %d, want 42", pkt.PactMercID) + } + if !bytes.Equal(pkt.MercData, mercData) { + t.Errorf("MercData = %v, want %v", pkt.MercData, mercData) + } +} + +// TestParseLargeMsgMhfUpdateHouse tests Parse for MsgMhfUpdateHouse. +func TestParseLargeMsgMhfUpdateHouse(t *testing.T) { + tests := []struct { + name string + state uint8 + password string + }{ + {"with password", 1, "secret"}, + {"empty password", 0, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint8(tt.state) // State + bf.WriteUint8(1) // Unk1 + bf.WriteUint16(0) // Unk2 + bf.WriteUint8(uint8(len(tt.password) + 1)) // Password length + bf.WriteBytes([]byte(tt.password)) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateHouse{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.State != tt.state { + t.Errorf("State = %d, want %d", pkt.State, tt.state) + } + if pkt.Unk1 != 1 { + t.Errorf("Unk1 = %d, want 1", pkt.Unk1) + } + if pkt.Unk2 != 0 { + t.Errorf("Unk2 = %d, want 0", pkt.Unk2) + } + if pkt.Password != tt.password { + t.Errorf("Password = %q, want %q", pkt.Password, tt.password) + } + }) + } +} + +// TestParseLargeMsgSysCreateAcquireSemaphore tests Parse for MsgSysCreateAcquireSemaphore. +func TestParseLargeMsgSysCreateAcquireSemaphore(t *testing.T) { + semID := "stage_001" + semBytes := make([]byte, len(semID)+1) // include space for null if needed + copy(semBytes, semID) + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xDEADBEEF) // AckHandle + bf.WriteUint16(100) // Unk0 + bf.WriteUint8(4) // PlayerCount + bf.WriteUint8(uint8(len(semBytes))) // SemaphoreIDLength + bf.WriteBytes(semBytes) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCreateAcquireSemaphore{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xDEADBEEF { + t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle) + } + if pkt.Unk0 != 100 { + t.Errorf("Unk0 = %d, want 100", pkt.Unk0) + } + if pkt.PlayerCount != 4 { + t.Errorf("PlayerCount = %d, want 4", pkt.PlayerCount) + } + if pkt.SemaphoreID != semID { + t.Errorf("SemaphoreID = %q, want %q", pkt.SemaphoreID, semID) + } +} + +// TestParseLargeMsgMhfOperateGuild tests Parse for MsgMhfOperateGuild. +func TestParseLargeMsgMhfOperateGuild(t *testing.T) { + dataPayload := []byte{0x10, 0x20, 0x30, 0x40, 0x50} + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint32(999) // GuildID + bf.WriteUint8(0x09) // Action = OPERATE_GUILD_UPDATE_COMMENT + bf.WriteUint8(uint8(len(dataPayload))) // dataLen + bf.WriteBytes([]byte{0x01, 0x02, 0x03, 0x04}) // Data1 (always 4 bytes) + bf.WriteBytes(dataPayload) // Data2 (dataLen bytes) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfOperateGuild{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.GuildID != 999 { + t.Errorf("GuildID = %d, want 999", pkt.GuildID) + } + if pkt.Action != OPERATE_GUILD_UPDATE_COMMENT { + t.Errorf("Action = %d, want %d", pkt.Action, OPERATE_GUILD_UPDATE_COMMENT) + } + if pkt.Data1 == nil { + t.Fatal("Data1 is nil") + } + if pkt.Data2 == nil { + t.Fatal("Data2 is nil") + } + data2Bytes := pkt.Data2.Data() + if !bytes.Equal(data2Bytes, dataPayload) { + t.Errorf("Data2 = %v, want %v", data2Bytes, dataPayload) + } +} + +// TestParseLargeMsgMhfReadBeatLevel tests Parse for MsgMhfReadBeatLevel. +func TestParseLargeMsgMhfReadBeatLevel(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(1) // Unk0 + bf.WriteUint32(4) // ValidIDCount + + // Write 16 uint32 IDs + ids := [16]uint32{0x74, 0x6B, 0x02, 0x24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + for _, id := range ids { + bf.WriteUint32(id) + } + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfReadBeatLevel{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Unk0 != 1 { + t.Errorf("Unk0 = %d, want 1", pkt.Unk0) + } + if pkt.ValidIDCount != 4 { + t.Errorf("ValidIDCount = %d, want 4", pkt.ValidIDCount) + } + for i, id := range ids { + if pkt.IDs[i] != id { + t.Errorf("IDs[%d] = 0x%X, want 0x%X", i, pkt.IDs[i], id) + } + } +} + +// TestParseLargeMsgMhfUpdateGuildMessageBoard tests Parse for MsgMhfUpdateGuildMessageBoard. +func TestParseLargeMsgMhfUpdateGuildMessageBoard(t *testing.T) { + t.Run("non-5 MessageOp", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint32(3) // MessageOp (not 5, so Request is read) + bf.WriteBytes([]byte{0x01, 0x02, 0x03}) // Request data + bf.WriteUint16(0) // trailing 2 bytes for seek + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateGuildMessageBoard{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.MessageOp != 3 { + t.Errorf("MessageOp = %d, want 3", pkt.MessageOp) + } + if len(pkt.Request) != 5 { + t.Errorf("Request len = %d, want 5", len(pkt.Request)) + } + }) + + t.Run("MessageOp 5 (no request)", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11111111) // AckHandle + bf.WriteUint32(5) // MessageOp = 5, no Request read + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateGuildMessageBoard{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.MessageOp != 5 { + t.Errorf("MessageOp = %d, want 5", pkt.MessageOp) + } + if pkt.Request != nil { + t.Errorf("Request should be nil when MessageOp=5, got %v", pkt.Request) + } + }) +} + +// TestParseLargeMsgSysCreateObject tests Parse for MsgSysCreateObject. +func TestParseLargeMsgSysCreateObject(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + x, y, z float32 + unk0 uint32 + }{ + {"origin", 1, 0.0, 0.0, 0.0, 0}, + {"typical", 0x12345678, 1.5, 2.5, 3.5, 42}, + {"negative coords", 0xFFFFFFFF, -100.25, 200.75, -300.125, 0xDEADBEEF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteFloat32(tt.x) + bf.WriteFloat32(tt.y) + bf.WriteFloat32(tt.z) + bf.WriteUint32(tt.unk0) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCreateObject{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ackHandle) + } + if pkt.X != tt.x { + t.Errorf("X = %f, want %f", pkt.X, tt.x) + } + if pkt.Y != tt.y { + t.Errorf("Y = %f, want %f", pkt.Y, tt.y) + } + if pkt.Z != tt.z { + t.Errorf("Z = %f, want %f", pkt.Z, tt.z) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + }) + } +} + +// TestParseLargeMsgSysLockGlobalSema tests Parse for MsgSysLockGlobalSema. +func TestParseLargeMsgSysLockGlobalSema(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xDEADBEEF) // AckHandle + bf.WriteUint16(8) // UserIDLength + bf.WriteUint16(11) // ServerChannelIDLength + bf.WriteBytes([]byte("user123")) + bf.WriteUint8(0) // null terminator + bf.WriteBytes([]byte("channel_01")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLockGlobalSema{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xDEADBEEF { + t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle) + } + if pkt.UserIDLength != 8 { + t.Errorf("UserIDLength = %d, want 8", pkt.UserIDLength) + } + if pkt.ServerChannelIDLength != 11 { + t.Errorf("ServerChannelIDLength = %d, want 11", pkt.ServerChannelIDLength) + } + if pkt.UserIDString != "user123" { + t.Errorf("UserIDString = %q, want %q", pkt.UserIDString, "user123") + } + if pkt.ServerChannelIDString != "channel_01" { + t.Errorf("ServerChannelIDString = %q, want %q", pkt.ServerChannelIDString, "channel_01") + } +} + +// TestParseLargeMsgMhfCreateGuild tests Parse for MsgMhfCreateGuild. +func TestParseLargeMsgMhfCreateGuild(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint8(1) // Unk0 + bf.WriteUint8(2) // Unk1 + bf.WriteUint16(10) // len (unused) + bf.WriteBytes([]byte("TestGuild")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfCreateGuild{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Unk0 != 1 { + t.Errorf("Unk0 = %d, want 1", pkt.Unk0) + } + if pkt.Unk1 != 2 { + t.Errorf("Unk1 = %d, want 2", pkt.Unk1) + } + if pkt.Name != "TestGuild" { + t.Errorf("Name = %q, want %q", pkt.Name, "TestGuild") + } +} + +// TestParseLargeMsgMhfCreateJoint tests Parse for MsgMhfCreateJoint. +func TestParseLargeMsgMhfCreateJoint(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xCAFEBABE) // AckHandle + bf.WriteUint32(500) // GuildID + bf.WriteUint32(15) // len (unused) + bf.WriteBytes([]byte("Alliance01")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfCreateJoint{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xCAFEBABE { + t.Errorf("AckHandle = 0x%X, want 0xCAFEBABE", pkt.AckHandle) + } + if pkt.GuildID != 500 { + t.Errorf("GuildID = %d, want 500", pkt.GuildID) + } + if pkt.Name != "Alliance01" { + t.Errorf("Name = %q, want %q", pkt.Name, "Alliance01") + } +} + +// TestParseLargeMsgMhfGetUdTacticsRemainingPoint tests Parse for MsgMhfGetUdTacticsRemainingPoint. +func TestParseLargeMsgMhfGetUdTacticsRemainingPoint(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(100) // Unk0 + bf.WriteUint32(200) // Unk1 + bf.WriteUint32(300) // Unk2 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfGetUdTacticsRemainingPoint{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Unk0 != 100 { + t.Errorf("Unk0 = %d, want 100", pkt.Unk0) + } + if pkt.Unk1 != 200 { + t.Errorf("Unk1 = %d, want 200", pkt.Unk1) + } + if pkt.Unk2 != 300 { + t.Errorf("Unk2 = %d, want 300", pkt.Unk2) + } +} + +// TestParseLargeMsgMhfPostCafeDurationBonusReceived tests Parse for MsgMhfPostCafeDurationBonusReceived. +func TestParseLargeMsgMhfPostCafeDurationBonusReceived(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint32(3) // count + bf.WriteUint32(1001) // CafeBonusID[0] + bf.WriteUint32(1002) // CafeBonusID[1] + bf.WriteUint32(1003) // CafeBonusID[2] + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfPostCafeDurationBonusReceived{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if len(pkt.CafeBonusID) != 3 { + t.Fatalf("CafeBonusID len = %d, want 3", len(pkt.CafeBonusID)) + } + expected := []uint32{1001, 1002, 1003} + for i, v := range expected { + if pkt.CafeBonusID[i] != v { + t.Errorf("CafeBonusID[%d] = %d, want %d", i, pkt.CafeBonusID[i], v) + } + } +} + +// TestParseLargeMsgMhfPostCafeDurationBonusReceivedEmpty tests Parse with zero IDs. +func TestParseLargeMsgMhfPostCafeDurationBonusReceivedEmpty(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(0) // count = 0 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfPostCafeDurationBonusReceived{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + if len(pkt.CafeBonusID) != 0 { + t.Errorf("CafeBonusID len = %d, want 0", len(pkt.CafeBonusID)) + } +} + +// TestParseLargeMsgMhfRegistGuildAdventureDiva tests Parse for MsgMhfRegistGuildAdventureDiva. +func TestParseLargeMsgMhfRegistGuildAdventureDiva(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(5) // Destination + bf.WriteUint32(1000) // Charge + bf.WriteUint32(42) // CharID (skipped) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfRegistGuildAdventureDiva{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Destination != 5 { + t.Errorf("Destination = %d, want 5", pkt.Destination) + } + if pkt.Charge != 1000 { + t.Errorf("Charge = %d, want 1000", pkt.Charge) + } +} + +// TestParseLargeMsgMhfStateFestaG tests Parse for MsgMhfStateFestaG. +func TestParseLargeMsgMhfStateFestaG(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xDEADBEEF) // AckHandle + bf.WriteUint32(100) // FestaID + bf.WriteUint32(200) // GuildID + bf.WriteUint16(0) // Hardcoded 0 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfStateFestaG{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xDEADBEEF { + t.Errorf("AckHandle = 0x%X, want 0xDEADBEEF", pkt.AckHandle) + } + if pkt.FestaID != 100 { + t.Errorf("FestaID = %d, want 100", pkt.FestaID) + } + if pkt.GuildID != 200 { + t.Errorf("GuildID = %d, want 200", pkt.GuildID) + } +} + +// TestParseLargeMsgMhfStateFestaU tests Parse for MsgMhfStateFestaU. +func TestParseLargeMsgMhfStateFestaU(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xCAFEBABE) // AckHandle + bf.WriteUint32(300) // FestaID + bf.WriteUint32(400) // GuildID + bf.WriteUint16(0) // Hardcoded 0 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfStateFestaU{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xCAFEBABE { + t.Errorf("AckHandle = 0x%X, want 0xCAFEBABE", pkt.AckHandle) + } + if pkt.FestaID != 300 { + t.Errorf("FestaID = %d, want 300", pkt.FestaID) + } + if pkt.GuildID != 400 { + t.Errorf("GuildID = %d, want 400", pkt.GuildID) + } +} + +// TestParseLargeMsgSysEnumerateStage tests Parse for MsgSysEnumerateStage. +func TestParseLargeMsgSysEnumerateStage(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11223344) // AckHandle + bf.WriteUint8(1) // Unk0 + bf.WriteUint8(0) // skipped byte + bf.WriteBytes([]byte("quest_")) + bf.WriteUint8(0) // null terminator + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysEnumerateStage{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x11223344 { + t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle) + } + if pkt.Unk0 != 1 { + t.Errorf("Unk0 = %d, want 1", pkt.Unk0) + } + if pkt.StagePrefix != "quest_" { + t.Errorf("StagePrefix = %q, want %q", pkt.StagePrefix, "quest_") + } +} + +// TestParseLargeMsgSysReserveStage tests Parse for MsgSysReserveStage. +func TestParseLargeMsgSysReserveStage(t *testing.T) { + stageID := "stage_42" + stageBytes := make([]byte, len(stageID)+1) // padded with null at end + copy(stageBytes, stageID) + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint8(0x11) // Ready + bf.WriteUint8(uint8(len(stageBytes))) // stageIDLength + bf.WriteBytes(stageBytes) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysReserveStage{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.Ready != 0x11 { + t.Errorf("Ready = 0x%X, want 0x11", pkt.Ready) + } + if pkt.StageID != stageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, stageID) + } +} diff --git a/network/mhfpacket/msg_parse_medium_test.go b/network/mhfpacket/msg_parse_medium_test.go new file mode 100644 index 000000000..7e3514bf0 --- /dev/null +++ b/network/mhfpacket/msg_parse_medium_test.go @@ -0,0 +1,810 @@ +package mhfpacket + +import ( + "bytes" + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// --- 5-stmt packets (medium complexity) --- + +// TestParseMediumVoteFesta verifies Parse for MsgMhfVoteFesta. +// Fields: AckHandle(u32), Unk(u32), GuildID(u32), TrialID(u32) +func TestParseMediumVoteFesta(t *testing.T) { + tests := []struct { + name string + ack uint32 + unk uint32 + guildID uint32 + trialID uint32 + }{ + {"typical", 0x11223344, 1, 500, 42}, + {"zero", 0, 0, 0, 0}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint32(tt.unk) + bf.WriteUint32(tt.guildID) + bf.WriteUint32(tt.trialID) + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfVoteFesta{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.Unk != tt.unk { + t.Errorf("Unk = %d, want %d", pkt.Unk, tt.unk) + } + if pkt.GuildID != tt.guildID { + t.Errorf("GuildID = %d, want %d", pkt.GuildID, tt.guildID) + } + if pkt.TrialID != tt.trialID { + t.Errorf("TrialID = %d, want %d", pkt.TrialID, tt.trialID) + } + }) + } +} + +// TestParseMediumAcquireSemaphore verifies Parse for MsgSysAcquireSemaphore. +// Fields: AckHandle(u32), SemaphoreIDLength(u8), SemaphoreID(string via bfutil.UpToNull) +func TestParseMediumAcquireSemaphore(t *testing.T) { + tests := []struct { + name string + ack uint32 + semaphoreID string + }{ + {"typical", 0xAABBCCDD, "quest_semaphore"}, + {"short", 1, "s"}, + {"empty", 0, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + // SemaphoreIDLength includes the null terminator in the read + idBytes := []byte(tt.semaphoreID) + idBytes = append(idBytes, 0x00) // null terminator + bf.WriteUint8(uint8(len(idBytes))) + bf.WriteBytes(idBytes) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysAcquireSemaphore{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.SemaphoreID != tt.semaphoreID { + t.Errorf("SemaphoreID = %q, want %q", pkt.SemaphoreID, tt.semaphoreID) + } + }) + } +} + +// TestParseMediumCheckSemaphore verifies Parse for MsgSysCheckSemaphore. +// Fields: AckHandle(u32), semaphoreIDLength(u8), SemaphoreID(string via bfutil.UpToNull) +func TestParseMediumCheckSemaphore(t *testing.T) { + tests := []struct { + name string + ack uint32 + semaphoreID string + }{ + {"typical", 0x12345678, "global_semaphore"}, + {"short id", 42, "x"}, + {"empty id", 0, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + idBytes := []byte(tt.semaphoreID) + idBytes = append(idBytes, 0x00) + bf.WriteUint8(uint8(len(idBytes))) + bf.WriteBytes(idBytes) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysCheckSemaphore{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.SemaphoreID != tt.semaphoreID { + t.Errorf("SemaphoreID = %q, want %q", pkt.SemaphoreID, tt.semaphoreID) + } + }) + } +} + +// TestParseMediumGetUserBinary verifies Parse for MsgSysGetUserBinary. +// Fields: AckHandle(u32), CharID(u32), BinaryType(u8) +func TestParseMediumGetUserBinary(t *testing.T) { + tests := []struct { + name string + ack uint32 + charID uint32 + binaryType uint8 + }{ + {"typical", 0xDEADBEEF, 12345, 1}, + {"zero", 0, 0, 0}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF, 255}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint32(tt.charID) + bf.WriteUint8(tt.binaryType) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysGetUserBinary{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.CharID != tt.charID { + t.Errorf("CharID = %d, want %d", pkt.CharID, tt.charID) + } + if pkt.BinaryType != tt.binaryType { + t.Errorf("BinaryType = %d, want %d", pkt.BinaryType, tt.binaryType) + } + }) + } +} + +// TestParseMediumSetObjectBinary verifies Parse for MsgSysSetObjectBinary. +// Fields: ObjID(u32), DataSize(u16), RawDataPayload([]byte of DataSize) +func TestParseMediumSetObjectBinary(t *testing.T) { + tests := []struct { + name string + objID uint32 + payload []byte + }{ + {"typical", 42, []byte{0x01, 0x02, 0x03, 0x04}}, + {"empty", 0, []byte{}}, + {"large", 0xCAFEBABE, []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.objID) + bf.WriteUint16(uint16(len(tt.payload))) + bf.WriteBytes(tt.payload) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysSetObjectBinary{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.ObjID != tt.objID { + t.Errorf("ObjID = %d, want %d", pkt.ObjID, tt.objID) + } + if pkt.DataSize != uint16(len(tt.payload)) { + t.Errorf("DataSize = %d, want %d", pkt.DataSize, len(tt.payload)) + } + if !bytes.Equal(pkt.RawDataPayload, tt.payload) { + t.Errorf("RawDataPayload = %v, want %v", pkt.RawDataPayload, tt.payload) + } + }) + } +} + +// TestParseMediumSetUserBinary verifies Parse for MsgSysSetUserBinary. +// Fields: BinaryType(u8), DataSize(u16), RawDataPayload([]byte of DataSize) +func TestParseMediumSetUserBinary(t *testing.T) { + tests := []struct { + name string + binaryType uint8 + payload []byte + }{ + {"typical", 1, []byte{0xDE, 0xAD, 0xBE, 0xEF}}, + {"empty", 0, []byte{}}, + {"max type", 255, []byte{0x01}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(tt.binaryType) + bf.WriteUint16(uint16(len(tt.payload))) + bf.WriteBytes(tt.payload) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysSetUserBinary{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.BinaryType != tt.binaryType { + t.Errorf("BinaryType = %d, want %d", pkt.BinaryType, tt.binaryType) + } + if pkt.DataSize != uint16(len(tt.payload)) { + t.Errorf("DataSize = %d, want %d", pkt.DataSize, len(tt.payload)) + } + if !bytes.Equal(pkt.RawDataPayload, tt.payload) { + t.Errorf("RawDataPayload = %v, want %v", pkt.RawDataPayload, tt.payload) + } + }) + } +} + +// --- 4-stmt packets --- + +// TestParseMediumGetUdRanking verifies Parse for MsgMhfGetUdRanking. +// Fields: AckHandle(u32), Unk0(u8) +func TestParseMediumGetUdRanking(t *testing.T) { + tests := []struct { + name string + ack uint32 + unk0 uint8 + }{ + {"typical", 0x11223344, 5}, + {"zero", 0, 0}, + {"max", 0xFFFFFFFF, 255}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint8(tt.unk0) + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetUdRanking{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + }) + } +} + +// TestParseMediumGetUdTacticsRanking verifies Parse for MsgMhfGetUdTacticsRanking. +// Fields: AckHandle(u32), GuildID(u32) +func TestParseMediumGetUdTacticsRanking(t *testing.T) { + tests := []struct { + name string + ack uint32 + guildID uint32 + }{ + {"typical", 0xAABBCCDD, 500}, + {"zero", 0, 0}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint32(tt.guildID) + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetUdTacticsRanking{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.GuildID != tt.guildID { + t.Errorf("GuildID = %d, want %d", pkt.GuildID, tt.guildID) + } + }) + } +} + +// TestParseMediumRegistGuildTresure verifies Parse for MsgMhfRegistGuildTresure. +// Fields: AckHandle(u32), DataLen(u16), Data([]byte), trailing u32 (discarded) +func TestParseMediumRegistGuildTresure(t *testing.T) { + tests := []struct { + name string + ack uint32 + data []byte + }{ + {"typical", 0x12345678, []byte{0x01, 0x02, 0x03}}, + {"empty data", 1, []byte{}}, + {"larger data", 0xDEADBEEF, []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint16(uint16(len(tt.data))) + bf.WriteBytes(tt.data) + bf.WriteUint32(0) // trailing uint32 that is read and discarded + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfRegistGuildTresure{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if !bytes.Equal(pkt.Data, tt.data) { + t.Errorf("Data = %v, want %v", pkt.Data, tt.data) + } + }) + } +} + +// TestParseMediumUpdateMyhouseInfo verifies Parse for MsgMhfUpdateMyhouseInfo. +// Fields: AckHandle(u32), Unk0([]byte of 0x16A bytes) +func TestParseMediumUpdateMyhouseInfo(t *testing.T) { + t.Run("typical", func(t *testing.T) { + bf := byteframe.NewByteFrame() + ack := uint32(0xCAFEBABE) + bf.WriteUint32(ack) + + // 0x16A = 362 bytes + payload := make([]byte, 0x16A) + for i := range payload { + payload[i] = byte(i % 256) + } + bf.WriteBytes(payload) + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateMyhouseInfo{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + if len(pkt.Unk0) != 0x16A { + t.Errorf("Unk0 length = %d, want %d", len(pkt.Unk0), 0x16A) + } + if !bytes.Equal(pkt.Unk0, payload) { + t.Error("Unk0 content mismatch") + } + }) + + t.Run("zero values", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) + bf.WriteBytes(make([]byte, 0x16A)) + + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateMyhouseInfo{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0 { + t.Errorf("AckHandle = 0x%X, want 0", pkt.AckHandle) + } + if len(pkt.Unk0) != 0x16A { + t.Errorf("Unk0 length = %d, want %d", len(pkt.Unk0), 0x16A) + } + }) +} + +// TestParseMediumRightsReload verifies Parse for MsgSysRightsReload. +// Fields: AckHandle(u32), Unk0(byte/u8) +func TestParseMediumRightsReload(t *testing.T) { + tests := []struct { + name string + ack uint32 + unk0 byte + }{ + {"typical", 0x55667788, 1}, + {"zero", 0, 0}, + {"max", 0xFFFFFFFF, 255}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ack) + bf.WriteUint8(tt.unk0) + + bf.Seek(0, io.SeekStart) + pkt := &MsgSysRightsReload{} + if err := pkt.Parse(bf, nil); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + }) + } +} + +// --- 3-stmt packets (AckHandle-only Parse) --- + +// TestParseMediumAckHandleOnlyBatch tests Parse for all 3-stmt packets that only +// read a single AckHandle uint32. These are verified to parse correctly and +// return the expected AckHandle value. +func TestParseMediumAckHandleOnlyBatch(t *testing.T) { + packets := []struct { + name string + pkt MHFPacket + // getAck extracts the AckHandle from the parsed packet + getAck func() uint32 + }{ + { + "MsgMhfGetUdBonusQuestInfo", + &MsgMhfGetUdBonusQuestInfo{}, + nil, + }, + { + "MsgMhfGetUdDailyPresentList", + &MsgMhfGetUdDailyPresentList{}, + nil, + }, + { + "MsgMhfGetUdGuildMapInfo", + &MsgMhfGetUdGuildMapInfo{}, + nil, + }, + { + "MsgMhfGetUdMonsterPoint", + &MsgMhfGetUdMonsterPoint{}, + nil, + }, + { + "MsgMhfGetUdMyRanking", + &MsgMhfGetUdMyRanking{}, + nil, + }, + { + "MsgMhfGetUdNormaPresentList", + &MsgMhfGetUdNormaPresentList{}, + nil, + }, + { + "MsgMhfGetUdRankingRewardList", + &MsgMhfGetUdRankingRewardList{}, + nil, + }, + { + "MsgMhfGetUdSelectedColorInfo", + &MsgMhfGetUdSelectedColorInfo{}, + nil, + }, + { + "MsgMhfGetUdShopCoin", + &MsgMhfGetUdShopCoin{}, + nil, + }, + { + "MsgMhfGetUdTacticsBonusQuest", + &MsgMhfGetUdTacticsBonusQuest{}, + nil, + }, + { + "MsgMhfGetUdTacticsFirstQuestBonus", + &MsgMhfGetUdTacticsFirstQuestBonus{}, + nil, + }, + { + "MsgMhfGetUdTacticsFollower", + &MsgMhfGetUdTacticsFollower{}, + nil, + }, + { + "MsgMhfGetUdTacticsLog", + &MsgMhfGetUdTacticsLog{}, + nil, + }, + { + "MsgMhfGetUdTacticsPoint", + &MsgMhfGetUdTacticsPoint{}, + nil, + }, + { + "MsgMhfGetUdTacticsRewardList", + &MsgMhfGetUdTacticsRewardList{}, + nil, + }, + { + "MsgMhfReceiveCafeDurationBonus", + &MsgMhfReceiveCafeDurationBonus{}, + nil, + }, + { + "MsgSysDeleteSemaphore", + &MsgSysDeleteSemaphore{}, + nil, + }, + { + "MsgSysReleaseSemaphore", + &MsgSysReleaseSemaphore{}, + nil, + }, + } + + ctx := &clientctx.ClientContext{} + ackValues := []uint32{0x12345678, 0, 0xFFFFFFFF, 0xDEADBEEF} + + for _, tc := range packets { + for _, ackVal := range ackValues { + t.Run(tc.name+"/ack_"+ackHex(ackVal), func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(ackVal) + bf.Seek(0, io.SeekStart) + + err := tc.pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + }) + } + } +} + +// TestParseMediumAckHandleOnlyVerifyValues tests each 3-stmt AckHandle-only +// packet individually, verifying that the AckHandle field is correctly populated. +func TestParseMediumAckHandleOnlyVerifyValues(t *testing.T) { + ctx := &clientctx.ClientContext{} + ack := uint32(0xCAFEBABE) + + makeFrame := func() *byteframe.ByteFrame { + bf := byteframe.NewByteFrame() + bf.WriteUint32(ack) + bf.Seek(0, io.SeekStart) + return bf + } + + t.Run("MsgMhfGetUdBonusQuestInfo", func(t *testing.T) { + pkt := &MsgMhfGetUdBonusQuestInfo{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdDailyPresentList", func(t *testing.T) { + pkt := &MsgMhfGetUdDailyPresentList{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdGuildMapInfo", func(t *testing.T) { + pkt := &MsgMhfGetUdGuildMapInfo{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdMonsterPoint", func(t *testing.T) { + pkt := &MsgMhfGetUdMonsterPoint{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdMyRanking", func(t *testing.T) { + pkt := &MsgMhfGetUdMyRanking{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdNormaPresentList", func(t *testing.T) { + pkt := &MsgMhfGetUdNormaPresentList{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdRankingRewardList", func(t *testing.T) { + pkt := &MsgMhfGetUdRankingRewardList{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdSelectedColorInfo", func(t *testing.T) { + pkt := &MsgMhfGetUdSelectedColorInfo{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdShopCoin", func(t *testing.T) { + pkt := &MsgMhfGetUdShopCoin{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsBonusQuest", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsBonusQuest{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsFirstQuestBonus", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsFirstQuestBonus{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsFollower", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsFollower{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsLog", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsLog{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsPoint", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsPoint{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfGetUdTacticsRewardList", func(t *testing.T) { + pkt := &MsgMhfGetUdTacticsRewardList{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgMhfReceiveCafeDurationBonus", func(t *testing.T) { + pkt := &MsgMhfReceiveCafeDurationBonus{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgSysDeleteSemaphore", func(t *testing.T) { + pkt := &MsgSysDeleteSemaphore{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) + + t.Run("MsgSysReleaseSemaphore", func(t *testing.T) { + pkt := &MsgSysReleaseSemaphore{} + if err := pkt.Parse(makeFrame(), ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != ack { + t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack) + } + }) +} + +// TestParseMediumDeleteUser verifies that MsgSysDeleteUser.Parse returns +// NOT IMPLEMENTED error (Parse is not implemented, only Build is). +func TestParseMediumDeleteUser(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(12345) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysDeleteUser{} + err := pkt.Parse(bf, nil) + if err == nil { + t.Fatal("Parse() should return error for NOT IMPLEMENTED") + } + if err.Error() != "NOT IMPLEMENTED" { + t.Errorf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED") + } +} + +// TestParseMediumInsertUser verifies that MsgSysInsertUser.Parse returns +// NOT IMPLEMENTED error (Parse is not implemented, only Build is). +func TestParseMediumInsertUser(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(12345) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysInsertUser{} + err := pkt.Parse(bf, nil) + if err == nil { + t.Fatal("Parse() should return error for NOT IMPLEMENTED") + } + if err.Error() != "NOT IMPLEMENTED" { + t.Errorf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED") + } +} + +// ackHex returns a hex string for a uint32 ack value, used for test naming. +func ackHex(v uint32) string { + const hex = "0123456789ABCDEF" + buf := make([]byte, 8) + for i := 7; i >= 0; i-- { + buf[i] = hex[v&0xF] + v >>= 4 + } + return string(buf) +} diff --git a/network/mhfpacket/msg_parse_small_test.go b/network/mhfpacket/msg_parse_small_test.go new file mode 100644 index 000000000..ba51be134 --- /dev/null +++ b/network/mhfpacket/msg_parse_small_test.go @@ -0,0 +1,238 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// TestParseSmallNotImplemented tests Parse for packets whose Parse method returns +// "NOT IMPLEMENTED". We verify that Parse returns a non-nil error and does not panic. +func TestParseSmallNotImplemented(t *testing.T) { + packets := []struct { + name string + pkt MHFPacket + }{ + // MHF packets - NOT IMPLEMENTED + {"MsgMhfAcceptReadReward", &MsgMhfAcceptReadReward{}}, + {"MsgMhfAddRewardSongCount", &MsgMhfAddRewardSongCount{}}, + {"MsgMhfCaravanMyRank", &MsgMhfCaravanMyRank{}}, + {"MsgMhfCaravanMyScore", &MsgMhfCaravanMyScore{}}, + {"MsgMhfCaravanRanking", &MsgMhfCaravanRanking{}}, + {"MsgMhfDebugPostValue", &MsgMhfDebugPostValue{}}, + {"MsgMhfEnterTournamentQuest", &MsgMhfEnterTournamentQuest{}}, + {"MsgMhfGetBreakSeibatuLevelReward", &MsgMhfGetBreakSeibatuLevelReward{}}, + {"MsgMhfGetCaAchievementHist", &MsgMhfGetCaAchievementHist{}}, + {"MsgMhfGetCaUniqueID", &MsgMhfGetCaUniqueID{}}, + {"MsgMhfGetDailyMissionMaster", &MsgMhfGetDailyMissionMaster{}}, + {"MsgMhfGetDailyMissionPersonal", &MsgMhfGetDailyMissionPersonal{}}, + {"MsgMhfGetExtraInfo", &MsgMhfGetExtraInfo{}}, + {"MsgMhfGetFixedSeibatuRankingTable", &MsgMhfGetFixedSeibatuRankingTable{}}, + {"MsgMhfGetRandFromTable", &MsgMhfGetRandFromTable{}}, + {"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}}, + {"MsgMhfGetSenyuDailyCount", &MsgMhfGetSenyuDailyCount{}}, + {"MsgMhfKickExportForce", &MsgMhfKickExportForce{}}, + {"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}}, + {"MsgMhfPlayFreeGacha", &MsgMhfPlayFreeGacha{}}, + {"MsgMhfPostBoostTimeLimit", &MsgMhfPostBoostTimeLimit{}}, + {"MsgMhfPostGemInfo", &MsgMhfPostGemInfo{}}, + {"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}}, + {"MsgMhfPostSeibattle", &MsgMhfPostSeibattle{}}, + {"MsgMhfReadBeatLevelAllRanking", &MsgMhfReadBeatLevelAllRanking{}}, + {"MsgMhfReadBeatLevelMyRanking", &MsgMhfReadBeatLevelMyRanking{}}, + {"MsgMhfReadLastWeekBeatRanking", &MsgMhfReadLastWeekBeatRanking{}}, + {"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}}, + {"MsgMhfResetAchievement", &MsgMhfResetAchievement{}}, + {"MsgMhfResetTitle", &MsgMhfResetTitle{}}, + {"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}}, + {"MsgMhfSetDailyMissionPersonal", &MsgMhfSetDailyMissionPersonal{}}, + {"MsgMhfSetUdTacticsFollower", &MsgMhfSetUdTacticsFollower{}}, + {"MsgMhfStampcardPrize", &MsgMhfStampcardPrize{}}, + {"MsgMhfUnreserveSrg", &MsgMhfUnreserveSrg{}}, + {"MsgMhfUpdateForceGuildRank", &MsgMhfUpdateForceGuildRank{}}, + {"MsgMhfUseUdShopCoin", &MsgMhfUseUdShopCoin{}}, + + // SYS packets - NOT IMPLEMENTED + {"MsgSysAuthData", &MsgSysAuthData{}}, + {"MsgSysAuthQuery", &MsgSysAuthQuery{}}, + {"MsgSysAuthTerminal", &MsgSysAuthTerminal{}}, + {"MsgSysCloseMutex", &MsgSysCloseMutex{}}, + {"MsgSysCollectBinary", &MsgSysCollectBinary{}}, + {"MsgSysCreateMutex", &MsgSysCreateMutex{}}, + {"MsgSysCreateOpenMutex", &MsgSysCreateOpenMutex{}}, + {"MsgSysDeleteMutex", &MsgSysDeleteMutex{}}, + {"MsgSysEnumlobby", &MsgSysEnumlobby{}}, + {"MsgSysEnumuser", &MsgSysEnumuser{}}, + {"MsgSysGetObjectBinary", &MsgSysGetObjectBinary{}}, + {"MsgSysGetObjectOwner", &MsgSysGetObjectOwner{}}, + {"MsgSysGetState", &MsgSysGetState{}}, + {"MsgSysInfokyserver", &MsgSysInfokyserver{}}, + {"MsgSysOpenMutex", &MsgSysOpenMutex{}}, + {"MsgSysRotateObject", &MsgSysRotateObject{}}, + {"MsgSysSerialize", &MsgSysSerialize{}}, + {"MsgSysTransBinary", &MsgSysTransBinary{}}, + } + + ctx := &clientctx.ClientContext{} + for _, tc := range packets { + t.Run(tc.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + // Write some padding bytes so Parse has data available if it tries to read. + bf.WriteUint32(0) + bf.Seek(0, io.SeekStart) + + err := tc.pkt.Parse(bf, ctx) + if err == nil { + t.Fatalf("Parse() expected error for NOT IMPLEMENTED packet, got nil") + } + if err.Error() != "NOT IMPLEMENTED" { + t.Fatalf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED") + } + }) + } +} + +// TestParseSmallNoData tests Parse for packets with no fields that return nil. +func TestParseSmallNoData(t *testing.T) { + packets := []struct { + name string + pkt MHFPacket + }{ + {"MsgSysCleanupObject", &MsgSysCleanupObject{}}, + {"MsgSysUnreserveStage", &MsgSysUnreserveStage{}}, + } + + ctx := &clientctx.ClientContext{} + for _, tc := range packets { + t.Run(tc.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + err := tc.pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v, want nil", err) + } + }) + } +} + +// TestParseSmallLogout tests Parse for MsgSysLogout which reads a single uint8 field. +func TestParseSmallLogout(t *testing.T) { + tests := []struct { + name string + unk0 uint8 + }{ + {"hardcoded 1", 1}, + {"zero", 0}, + {"max", 255}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(tt.unk0) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLogout{} + err := pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + }) + } +} + +// TestParseSmallEnumerateHouse tests Parse for MsgMhfEnumerateHouse which reads +// AckHandle, CharID, Method, Unk, lenName, and optional Name. +func TestParseSmallEnumerateHouse(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("no name", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11223344) // AckHandle + bf.WriteUint32(0xDEADBEEF) // CharID + bf.WriteUint8(2) // Method + bf.WriteUint16(100) // Unk + bf.WriteUint8(0) // lenName = 0 (no name) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfEnumerateHouse{} + err := pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + if pkt.AckHandle != 0x11223344 { + t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle) + } + if pkt.CharID != 0xDEADBEEF { + t.Errorf("CharID = 0x%X, want 0xDEADBEEF", pkt.CharID) + } + if pkt.Method != 2 { + t.Errorf("Method = %d, want 2", pkt.Method) + } + if pkt.Unk != 100 { + t.Errorf("Unk = %d, want 100", pkt.Unk) + } + if pkt.Name != "" { + t.Errorf("Name = %q, want empty", pkt.Name) + } + }) + + t.Run("with name", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(42) // CharID + bf.WriteUint8(1) // Method + bf.WriteUint16(200) // Unk + // The name is SJIS null-terminated bytes. Use ASCII-compatible bytes. + nameBytes := []byte("Test\x00") + bf.WriteUint8(uint8(len(nameBytes))) // lenName > 0 + bf.WriteBytes(nameBytes) // null-terminated name + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfEnumerateHouse{} + err := pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + if pkt.AckHandle != 1 { + t.Errorf("AckHandle = %d, want 1", pkt.AckHandle) + } + if pkt.CharID != 42 { + t.Errorf("CharID = %d, want 42", pkt.CharID) + } + if pkt.Method != 1 { + t.Errorf("Method = %d, want 1", pkt.Method) + } + if pkt.Unk != 200 { + t.Errorf("Unk = %d, want 200", pkt.Unk) + } + if pkt.Name != "Test" { + t.Errorf("Name = %q, want %q", pkt.Name, "Test") + } + }) +} + +// TestParseSmallNotImplementedDoesNotPanic ensures that calling Parse on NOT IMPLEMENTED +// packets with a nil ClientContext does not cause a nil pointer dereference panic. +func TestParseSmallNotImplementedDoesNotPanic(t *testing.T) { + packets := []MHFPacket{ + &MsgMhfAcceptReadReward{}, + &MsgSysAuthData{}, + &MsgSysSerialize{}, + } + + for _, pkt := range packets { + t.Run("nil_ctx", func(t *testing.T) { + bf := byteframe.NewByteFrame() + err := pkt.Parse(bf, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + } +} diff --git a/server/channelserver/handlers_guild_icon_test.go b/server/channelserver/handlers_guild_icon_test.go new file mode 100644 index 000000000..2cba83bca --- /dev/null +++ b/server/channelserver/handlers_guild_icon_test.go @@ -0,0 +1,249 @@ +package channelserver + +import ( + "encoding/json" + "testing" +) + +func TestGuildIconScan_Bytes(t *testing.T) { + jsonData := []byte(`{"Parts":[{"Index":1,"ID":100,"Page":2,"Size":3,"Rotation":4,"Red":255,"Green":128,"Blue":0,"PosX":50,"PosY":60}]}`) + + gi := &GuildIcon{} + err := gi.Scan(jsonData) + if err != nil { + t.Fatalf("Scan([]byte) error = %v", err) + } + + if len(gi.Parts) != 1 { + t.Fatalf("Parts length = %d, want 1", len(gi.Parts)) + } + + part := gi.Parts[0] + if part.Index != 1 { + t.Errorf("Index = %d, want 1", part.Index) + } + if part.ID != 100 { + t.Errorf("ID = %d, want 100", part.ID) + } + if part.Page != 2 { + t.Errorf("Page = %d, want 2", part.Page) + } + if part.Size != 3 { + t.Errorf("Size = %d, want 3", part.Size) + } + if part.Rotation != 4 { + t.Errorf("Rotation = %d, want 4", part.Rotation) + } + if part.Red != 255 { + t.Errorf("Red = %d, want 255", part.Red) + } + if part.Green != 128 { + t.Errorf("Green = %d, want 128", part.Green) + } + if part.Blue != 0 { + t.Errorf("Blue = %d, want 0", part.Blue) + } + if part.PosX != 50 { + t.Errorf("PosX = %d, want 50", part.PosX) + } + if part.PosY != 60 { + t.Errorf("PosY = %d, want 60", part.PosY) + } +} + +func TestGuildIconScan_String(t *testing.T) { + jsonStr := `{"Parts":[{"Index":5,"ID":200,"Page":1,"Size":2,"Rotation":0,"Red":100,"Green":50,"Blue":25,"PosX":300,"PosY":400}]}` + + gi := &GuildIcon{} + err := gi.Scan(jsonStr) + if err != nil { + t.Fatalf("Scan(string) error = %v", err) + } + + if len(gi.Parts) != 1 { + t.Fatalf("Parts length = %d, want 1", len(gi.Parts)) + } + if gi.Parts[0].ID != 200 { + t.Errorf("ID = %d, want 200", gi.Parts[0].ID) + } + if gi.Parts[0].PosX != 300 { + t.Errorf("PosX = %d, want 300", gi.Parts[0].PosX) + } +} + +func TestGuildIconScan_MultipleParts(t *testing.T) { + jsonData := []byte(`{"Parts":[{"Index":0,"ID":1,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":1,"ID":2,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":2,"ID":3,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0}]}`) + + gi := &GuildIcon{} + err := gi.Scan(jsonData) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + + if len(gi.Parts) != 3 { + t.Fatalf("Parts length = %d, want 3", len(gi.Parts)) + } + for i, part := range gi.Parts { + if part.Index != uint16(i) { + t.Errorf("Parts[%d].Index = %d, want %d", i, part.Index, i) + } + } +} + +func TestGuildIconScan_EmptyParts(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan([]byte(`{"Parts":[]}`)) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + if len(gi.Parts) != 0 { + t.Errorf("Parts length = %d, want 0", len(gi.Parts)) + } +} + +func TestGuildIconScan_InvalidJSON(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan([]byte(`{invalid`)) + if err == nil { + t.Error("Scan() with invalid JSON should return error") + } +} + +func TestGuildIconScan_InvalidJSONString(t *testing.T) { + gi := &GuildIcon{} + err := gi.Scan("{invalid") + if err == nil { + t.Error("Scan() with invalid JSON string should return error") + } +} + +func TestGuildIconScan_UnsupportedType(t *testing.T) { + gi := &GuildIcon{} + // Passing an unsupported type should not error (just no-op) + err := gi.Scan(12345) + if err != nil { + t.Errorf("Scan(int) unexpected error = %v", err) + } +} + +func TestGuildIconValue(t *testing.T) { + gi := &GuildIcon{ + Parts: []GuildIconPart{ + {Index: 1, ID: 100, Page: 2, Size: 3, Rotation: 4, Red: 255, Green: 128, Blue: 0, PosX: 50, PosY: 60}, + }, + } + + val, err := gi.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + jsonBytes, ok := val.([]byte) + if !ok { + t.Fatalf("Value() returned %T, want []byte", val) + } + + // Verify round-trip + gi2 := &GuildIcon{} + err = json.Unmarshal(jsonBytes, gi2) + if err != nil { + t.Fatalf("json.Unmarshal error = %v", err) + } + + if len(gi2.Parts) != 1 { + t.Fatalf("round-trip Parts length = %d, want 1", len(gi2.Parts)) + } + if gi2.Parts[0].ID != 100 { + t.Errorf("round-trip ID = %d, want 100", gi2.Parts[0].ID) + } + if gi2.Parts[0].Red != 255 { + t.Errorf("round-trip Red = %d, want 255", gi2.Parts[0].Red) + } +} + +func TestGuildIconValue_Empty(t *testing.T) { + gi := &GuildIcon{} + val, err := gi.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + if val == nil { + t.Error("Value() should not return nil") + } +} + +func TestGuildIconScanValueRoundTrip(t *testing.T) { + original := &GuildIcon{ + Parts: []GuildIconPart{ + {Index: 0, ID: 10, Page: 1, Size: 2, Rotation: 45, Red: 200, Green: 150, Blue: 100, PosX: 500, PosY: 600}, + {Index: 1, ID: 20, Page: 3, Size: 4, Rotation: 90, Red: 50, Green: 75, Blue: 255, PosX: 100, PosY: 200}, + }, + } + + // Value -> Scan round trip + val, err := original.Value() + if err != nil { + t.Fatalf("Value() error = %v", err) + } + + restored := &GuildIcon{} + err = restored.Scan(val) + if err != nil { + t.Fatalf("Scan() error = %v", err) + } + + if len(restored.Parts) != len(original.Parts) { + t.Fatalf("Parts length = %d, want %d", len(restored.Parts), len(original.Parts)) + } + + for i := range original.Parts { + if restored.Parts[i] != original.Parts[i] { + t.Errorf("Parts[%d] mismatch: got %+v, want %+v", i, restored.Parts[i], original.Parts[i]) + } + } +} + +func TestFestivalColourCodes(t *testing.T) { + tests := []struct { + colour FestivalColour + code uint8 + }{ + {FestivalColourBlue, 0x00}, + {FestivalColourRed, 0x01}, + {FestivalColourNone, 0xFF}, + } + + for _, tt := range tests { + t.Run(string(tt.colour), func(t *testing.T) { + code, ok := FestivalColourCodes[tt.colour] + if !ok { + t.Fatalf("FestivalColourCodes missing key %s", tt.colour) + } + if code != tt.code { + t.Errorf("FestivalColourCodes[%s] = %d, want %d", tt.colour, code, tt.code) + } + }) + } +} + +func TestFestivalColourConstants(t *testing.T) { + if FestivalColourNone != "none" { + t.Errorf("FestivalColourNone = %s, want none", FestivalColourNone) + } + if FestivalColourRed != "red" { + t.Errorf("FestivalColourRed = %s, want red", FestivalColourRed) + } + if FestivalColourBlue != "blue" { + t.Errorf("FestivalColourBlue = %s, want blue", FestivalColourBlue) + } +} + +func TestGuildApplicationTypeConstants(t *testing.T) { + if GuildApplicationTypeApplied != "applied" { + t.Errorf("GuildApplicationTypeApplied = %s, want applied", GuildApplicationTypeApplied) + } + if GuildApplicationTypeInvited != "invited" { + t.Errorf("GuildApplicationTypeInvited = %s, want invited", GuildApplicationTypeInvited) + } +} diff --git a/server/channelserver/handlers_quest_backport_test.go b/server/channelserver/handlers_quest_backport_test.go new file mode 100644 index 000000000..b07bca4c3 --- /dev/null +++ b/server/channelserver/handlers_quest_backport_test.go @@ -0,0 +1,128 @@ +package channelserver + +import ( + "encoding/binary" + "testing" + + _config "erupe-ce/config" +) + +func TestBackportQuest_Basic(t *testing.T) { + // Set up config for the test + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.ZZ + + // Create a quest data buffer large enough for BackportQuest to work with. + // The function reads a uint32 from data[0:4] as offset, then works at offset+96. + // We need at least offset + 96 + 108 + 6*8 bytes. + // Set offset (wp base) = 0, so wp starts at 96, rp at 100. + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) // offset = 0 + + // Fill some data at the rp positions so we can verify copies + for i := 100; i < 400; i++ { + data[i] = byte(i & 0xFF) + } + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } + if len(result) != len(data) { + t.Errorf("BackportQuest changed data length: got %d, want %d", len(result), len(data)) + } +} + +func TestBackportQuest_S6Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.S6 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + for i := 0; i < len(data); i++ { + data[i+4] = byte(i % 256) + if i+4 >= len(data)-1 { + break + } + } + + // Set some values at data[8:12] so we can check they get copied to data[16:20] + binary.LittleEndian.PutUint32(data[8:12], 0xDEADBEEF) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } + + // In S6 mode, data[16:20] should be copied from data[8:12] + got := binary.LittleEndian.Uint32(result[16:20]) + if got != 0xDEADBEEF { + t.Errorf("S6 mode: data[16:20] = 0x%X, want 0xDEADBEEF", got) + } +} + +func TestBackportQuest_G91Mode_PatternReplacement(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.G91 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + // Insert an armor sphere pattern at a known location + // Pattern: 0x0A, 0x00, 0x01, 0x33 -> should replace bytes at +2 with 0xD7, 0x00 + offset := 300 + data[offset] = 0x0A + data[offset+1] = 0x00 + data[offset+2] = 0x01 + data[offset+3] = 0x33 + + result := BackportQuest(data) + + // After BackportQuest, the pattern's last 2 bytes should be replaced + if result[offset+2] != 0xD7 || result[offset+3] != 0x00 { + t.Errorf("G91 pattern replacement failed: got [0x%X, 0x%X], want [0xD7, 0x00]", + result[offset+2], result[offset+3]) + } +} + +func TestBackportQuest_F5Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.F5 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } +} + +func TestBackportQuest_G101Mode(t *testing.T) { + oldConfig := _config.ErupeConfig + defer func() { _config.ErupeConfig = oldConfig }() + + _config.ErupeConfig = &_config.Config{} + _config.ErupeConfig.RealClientMode = _config.G101 + + data := make([]byte, 512) + binary.LittleEndian.PutUint32(data[0:4], 0) + + result := BackportQuest(data) + if result == nil { + t.Fatal("BackportQuest returned nil") + } +}