From 0bd724f74e7eaaef55145a0ca0c80b647939709b Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Tue, 17 Feb 2026 00:09:41 +0100 Subject: [PATCH] test: backport remaining test files from v9.2.x-stable Import 18 network packet test files and 5 server infrastructure test files, adapted for main branch APIs: fix config import alias (_config), remove non-existent DevMode field, use global handlerTable instead of per-server handlers map, and correct validateToken mock expectations to include both token and tokenID arguments. Adds go-sqlmock dependency for database mocking in signserver tests. --- go.mod | 1 + go.sum | 3 + network/binpacket/binpacket_test.go | 430 ++++ network/mhfpacket/mhfpacket_test.go | 828 ++++++ network/mhfpacket/msg_batch_parse_test.go | 2229 +++++++++++++++++ network/mhfpacket/msg_build_test.go | 1413 +++++++++++ network/mhfpacket/msg_comprehensive_test.go | 1150 +++++++++ .../msg_mhf_acquire_cafe_item_test.go | 180 ++ network/mhfpacket/msg_mhf_acquire_test.go | 263 ++ network/mhfpacket/msg_mhf_guacot_test.go | 364 +++ network/mhfpacket/msg_mhf_packets_test.go | 537 ++++ network/mhfpacket/msg_opcode_coverage_test.go | 301 +++ network/mhfpacket/msg_parse_large_test.go | 880 +++++++ network/mhfpacket/msg_parse_medium_test.go | 776 ++++++ network/mhfpacket/msg_parse_small_test.go | 216 ++ network/mhfpacket/msg_parse_test.go | 218 ++ network/mhfpacket/msg_sys_core_test.go | 310 +++ network/mhfpacket/msg_sys_packets_test.go | 592 +++++ network/mhfpacket/msg_sys_stage_test.go | 332 +++ network/packetid_test.go | 211 ++ .../channelserver/handlers_register_test.go | 229 ++ server/entranceserver/entrance_server_test.go | 522 ++++ server/signserver/dbutils_test.go | 825 ++++++ server/signserver/session_test.go | 393 +++ server/signserver/sign_server_test.go | 582 +++++ 25 files changed, 13785 insertions(+) create mode 100644 network/binpacket/binpacket_test.go create mode 100644 network/mhfpacket/mhfpacket_test.go create mode 100644 network/mhfpacket/msg_batch_parse_test.go create mode 100644 network/mhfpacket/msg_build_test.go create mode 100644 network/mhfpacket/msg_comprehensive_test.go create mode 100644 network/mhfpacket/msg_mhf_acquire_cafe_item_test.go create mode 100644 network/mhfpacket/msg_mhf_acquire_test.go create mode 100644 network/mhfpacket/msg_mhf_guacot_test.go create mode 100644 network/mhfpacket/msg_mhf_packets_test.go create mode 100644 network/mhfpacket/msg_opcode_coverage_test.go create mode 100644 network/mhfpacket/msg_parse_large_test.go create mode 100644 network/mhfpacket/msg_parse_medium_test.go create mode 100644 network/mhfpacket/msg_parse_small_test.go create mode 100644 network/mhfpacket/msg_parse_test.go create mode 100644 network/mhfpacket/msg_sys_core_test.go create mode 100644 network/mhfpacket/msg_sys_packets_test.go create mode 100644 network/mhfpacket/msg_sys_stage_test.go create mode 100644 network/packetid_test.go create mode 100644 server/channelserver/handlers_register_test.go create mode 100644 server/entranceserver/entrance_server_test.go create mode 100644 server/signserver/dbutils_test.go create mode 100644 server/signserver/session_test.go create mode 100644 server/signserver/sign_server_test.go diff --git a/go.mod b/go.mod index a1136a498..3eeb46d7c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect diff --git a/go.sum b/go.sum index 116dab45e..576eb28df 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -143,6 +145,7 @@ github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Cc github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/network/binpacket/binpacket_test.go b/network/binpacket/binpacket_test.go new file mode 100644 index 000000000..7a935e41e --- /dev/null +++ b/network/binpacket/binpacket_test.go @@ -0,0 +1,430 @@ +package binpacket + +import ( + "bytes" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" +) + +func TestMsgBinTargetedOpcode(t *testing.T) { + m := &MsgBinTargeted{} + if m.Opcode() != network.MSG_SYS_CAST_BINARY { + t.Errorf("MsgBinTargeted.Opcode() = %v, want MSG_SYS_CAST_BINARY", m.Opcode()) + } +} + +func TestMsgBinTargetedParseEmpty(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint16(0) // TargetCount = 0 + + bf.Seek(0, 0) + + m := &MsgBinTargeted{} + err := m.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if m.TargetCount != 0 { + t.Errorf("TargetCount = %d, want 0", m.TargetCount) + } + if len(m.TargetCharIDs) != 0 { + t.Errorf("TargetCharIDs len = %d, want 0", len(m.TargetCharIDs)) + } +} + +func TestMsgBinTargetedParseSingleTarget(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint16(1) // TargetCount = 1 + bf.WriteUint32(0x12345678) // TargetCharID + bf.WriteBytes([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + + bf.Seek(0, 0) + + m := &MsgBinTargeted{} + err := m.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if m.TargetCount != 1 { + t.Errorf("TargetCount = %d, want 1", m.TargetCount) + } + if len(m.TargetCharIDs) != 1 { + t.Errorf("TargetCharIDs len = %d, want 1", len(m.TargetCharIDs)) + } + if m.TargetCharIDs[0] != 0x12345678 { + t.Errorf("TargetCharIDs[0] = %x, want 0x12345678", m.TargetCharIDs[0]) + } + if !bytes.Equal(m.RawDataPayload, []byte{0xDE, 0xAD, 0xBE, 0xEF}) { + t.Errorf("RawDataPayload = %v, want [0xDE, 0xAD, 0xBE, 0xEF]", m.RawDataPayload) + } +} + +func TestMsgBinTargetedParseMultipleTargets(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint16(3) // TargetCount = 3 + bf.WriteUint32(100) + bf.WriteUint32(200) + bf.WriteUint32(300) + bf.WriteBytes([]byte{0x01, 0x02, 0x03}) + + bf.Seek(0, 0) + + m := &MsgBinTargeted{} + err := m.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if m.TargetCount != 3 { + t.Errorf("TargetCount = %d, want 3", m.TargetCount) + } + if len(m.TargetCharIDs) != 3 { + t.Errorf("TargetCharIDs len = %d, want 3", len(m.TargetCharIDs)) + } + if m.TargetCharIDs[0] != 100 || m.TargetCharIDs[1] != 200 || m.TargetCharIDs[2] != 300 { + t.Errorf("TargetCharIDs = %v, want [100, 200, 300]", m.TargetCharIDs) + } +} + +func TestMsgBinTargetedBuild(t *testing.T) { + m := &MsgBinTargeted{ + TargetCount: 2, + TargetCharIDs: []uint32{0x11111111, 0x22222222}, + RawDataPayload: []byte{0xAA, 0xBB}, + } + + bf := byteframe.NewByteFrame() + err := m.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + expected := []byte{ + 0x00, 0x02, // TargetCount + 0x11, 0x11, 0x11, 0x11, // TargetCharIDs[0] + 0x22, 0x22, 0x22, 0x22, // TargetCharIDs[1] + 0xAA, 0xBB, // RawDataPayload + } + + if !bytes.Equal(bf.Data(), expected) { + t.Errorf("Build() = %v, want %v", bf.Data(), expected) + } +} + +func TestMsgBinTargetedRoundTrip(t *testing.T) { + original := &MsgBinTargeted{ + TargetCount: 3, + TargetCharIDs: []uint32{1000, 2000, 3000}, + RawDataPayload: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + } + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, 0) + parsed := &MsgBinTargeted{} + err = parsed.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsed.TargetCount != original.TargetCount { + t.Errorf("TargetCount = %d, want %d", parsed.TargetCount, original.TargetCount) + } + if len(parsed.TargetCharIDs) != len(original.TargetCharIDs) { + t.Errorf("TargetCharIDs len = %d, want %d", len(parsed.TargetCharIDs), len(original.TargetCharIDs)) + } + for i := range original.TargetCharIDs { + if parsed.TargetCharIDs[i] != original.TargetCharIDs[i] { + t.Errorf("TargetCharIDs[%d] = %d, want %d", i, parsed.TargetCharIDs[i], original.TargetCharIDs[i]) + } + } + if !bytes.Equal(parsed.RawDataPayload, original.RawDataPayload) { + t.Errorf("RawDataPayload = %v, want %v", parsed.RawDataPayload, original.RawDataPayload) + } +} + +func TestMsgBinMailNotifyOpcode(t *testing.T) { + m := MsgBinMailNotify{} + if m.Opcode() != network.MSG_SYS_CASTED_BINARY { + t.Errorf("MsgBinMailNotify.Opcode() = %v, want MSG_SYS_CASTED_BINARY", m.Opcode()) + } +} + +func TestMsgBinMailNotifyBuild(t *testing.T) { + m := MsgBinMailNotify{ + SenderName: "TestPlayer", + } + + bf := byteframe.NewByteFrame() + err := m.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + data := bf.Data() + + // First byte should be 0x01 (Unk) + if data[0] != 0x01 { + t.Errorf("First byte = %x, want 0x01", data[0]) + } + + // Total length should be 1 (Unk) + 21 (padded name) = 22 + if len(data) != 22 { + t.Errorf("Data len = %d, want 22", len(data)) + } +} + +func TestMsgBinMailNotifyBuildEmptyName(t *testing.T) { + m := MsgBinMailNotify{ + SenderName: "", + } + + bf := byteframe.NewByteFrame() + err := m.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + if len(bf.Data()) != 22 { + t.Errorf("Data len = %d, want 22", len(bf.Data())) + } +} + +func TestMsgBinChatOpcode(t *testing.T) { + m := &MsgBinChat{} + if m.Opcode() != network.MSG_SYS_CAST_BINARY { + t.Errorf("MsgBinChat.Opcode() = %v, want MSG_SYS_CAST_BINARY", m.Opcode()) + } +} + +func TestMsgBinChatTypes(t *testing.T) { + tests := []struct { + chatType ChatType + value uint8 + }{ + {ChatTypeStage, 1}, + {ChatTypeGuild, 2}, + {ChatTypeAlliance, 3}, + {ChatTypeParty, 4}, + {ChatTypeWhisper, 5}, + } + + for _, tt := range tests { + if uint8(tt.chatType) != tt.value { + t.Errorf("ChatType %v = %d, want %d", tt.chatType, uint8(tt.chatType), tt.value) + } + } +} + +func TestMsgBinChatBuildParse(t *testing.T) { + original := &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeStage, + Flags: 0x0000, + Message: "Hello", + SenderName: "Player", + } + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, 0) + parsed := &MsgBinChat{} + err = parsed.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsed.Unk0 != original.Unk0 { + t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0) + } + if parsed.Type != original.Type { + t.Errorf("Type = %d, want %d", parsed.Type, original.Type) + } + if parsed.Flags != original.Flags { + t.Errorf("Flags = %d, want %d", parsed.Flags, original.Flags) + } + if parsed.Message != original.Message { + t.Errorf("Message = %q, want %q", parsed.Message, original.Message) + } + if parsed.SenderName != original.SenderName { + t.Errorf("SenderName = %q, want %q", parsed.SenderName, original.SenderName) + } +} + +func TestMsgBinChatBuildParseJapanese(t *testing.T) { + original := &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeGuild, + Flags: 0x0001, + Message: "こんにちは", + SenderName: "テスト", + } + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, 0) + parsed := &MsgBinChat{} + err = parsed.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.Message != original.Message { + t.Errorf("Message = %q, want %q", parsed.Message, original.Message) + } + if parsed.SenderName != original.SenderName { + t.Errorf("SenderName = %q, want %q", parsed.SenderName, original.SenderName) + } +} + +func TestMsgBinChatBuildParseEmpty(t *testing.T) { + original := &MsgBinChat{ + Unk0: 0x00, + Type: ChatTypeParty, + Flags: 0x0000, + Message: "", + SenderName: "", + } + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, 0) + parsed := &MsgBinChat{} + err = parsed.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.Message != "" { + t.Errorf("Message = %q, want empty", parsed.Message) + } + if parsed.SenderName != "" { + t.Errorf("SenderName = %q, want empty", parsed.SenderName) + } +} + +func TestMsgBinChatBuildFormat(t *testing.T) { + m := &MsgBinChat{ + Unk0: 0x12, + Type: ChatTypeWhisper, + Flags: 0x3456, + Message: "Hi", + SenderName: "A", + } + + bf := byteframe.NewByteFrame() + err := m.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + data := bf.Data() + + // Verify header structure + if data[0] != 0x12 { + t.Errorf("Unk0 = %x, want 0x12", data[0]) + } + if data[1] != uint8(ChatTypeWhisper) { + t.Errorf("Type = %x, want %x", data[1], uint8(ChatTypeWhisper)) + } + // Flags at bytes 2-3 (big endian) + if data[2] != 0x34 || data[3] != 0x56 { + t.Errorf("Flags = %x%x, want 3456", data[2], data[3]) + } +} + +func TestMsgBinChatAllTypes(t *testing.T) { + types := []ChatType{ + ChatTypeStage, + ChatTypeGuild, + ChatTypeAlliance, + ChatTypeParty, + ChatTypeWhisper, + } + + for _, chatType := range types { + t.Run("", func(t *testing.T) { + original := &MsgBinChat{ + Type: chatType, + Message: "Test", + SenderName: "Player", + } + + bf := byteframe.NewByteFrame() + err := original.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, 0) + parsed := &MsgBinChat{} + err = parsed.Parse(bf) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.Type != chatType { + t.Errorf("Type = %d, want %d", parsed.Type, chatType) + } + }) + } +} + +func TestMsgBinMailNotifyParsePanics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Parse() should panic with 'implement me'") + } + }() + + m := MsgBinMailNotify{} + bf := byteframe.NewByteFrame() + _ = m.Parse(bf) +} + +func TestMsgBinMailNotifyBuildLongName(t *testing.T) { + m := MsgBinMailNotify{ + SenderName: "ThisIsAVeryLongPlayerNameThatExceeds21Characters", + } + + bf := byteframe.NewByteFrame() + err := m.Build(bf) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Data should still be 22 bytes (1 + 21) + if len(bf.Data()) != 22 { + t.Errorf("Data len = %d, want 22", len(bf.Data())) + } +} diff --git a/network/mhfpacket/mhfpacket_test.go b/network/mhfpacket/mhfpacket_test.go new file mode 100644 index 000000000..87628f488 --- /dev/null +++ b/network/mhfpacket/mhfpacket_test.go @@ -0,0 +1,828 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func TestMHFPacketInterface(t *testing.T) { + // Verify that packets implement the MHFPacket interface + var _ MHFPacket = &MsgSysPing{} + var _ MHFPacket = &MsgSysTime{} + var _ MHFPacket = &MsgSysNop{} + var _ MHFPacket = &MsgSysEnd{} + var _ MHFPacket = &MsgSysLogin{} + var _ MHFPacket = &MsgSysLogout{} +} + +func TestFromOpcodeReturnsCorrectType(t *testing.T) { + tests := []struct { + opcode network.PacketID + wantType string + }{ + {network.MSG_HEAD, "*mhfpacket.MsgHead"}, + {network.MSG_SYS_PING, "*mhfpacket.MsgSysPing"}, + {network.MSG_SYS_TIME, "*mhfpacket.MsgSysTime"}, + {network.MSG_SYS_NOP, "*mhfpacket.MsgSysNop"}, + {network.MSG_SYS_END, "*mhfpacket.MsgSysEnd"}, + {network.MSG_SYS_ACK, "*mhfpacket.MsgSysAck"}, + {network.MSG_SYS_LOGIN, "*mhfpacket.MsgSysLogin"}, + {network.MSG_SYS_LOGOUT, "*mhfpacket.MsgSysLogout"}, + {network.MSG_SYS_CREATE_STAGE, "*mhfpacket.MsgSysCreateStage"}, + {network.MSG_SYS_ENTER_STAGE, "*mhfpacket.MsgSysEnterStage"}, + } + + for _, tt := range tests { + t.Run(tt.opcode.String(), func(t *testing.T) { + pkt := FromOpcode(tt.opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", tt.opcode) + return + } + if pkt.Opcode() != tt.opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), tt.opcode) + } + }) + } +} + +func TestFromOpcodeUnknown(t *testing.T) { + // Test with an invalid opcode + pkt := FromOpcode(network.PacketID(0xFFFF)) + if pkt != nil { + t.Error("FromOpcode(0xFFFF) should return nil for unknown opcode") + } +} + +func TestMsgSysPingRoundTrip(t *testing.T) { + original := &MsgSysPing{ + AckHandle: 0x12345678, + } + + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysPing{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = %d, want %d", parsed.AckHandle, original.AckHandle) + } +} + +func TestMsgSysTimeRoundTrip(t *testing.T) { + tests := []struct { + name string + getRemoteTime bool + timestamp uint32 + }{ + {"no remote time", false, 1577105879}, + {"with remote time", true, 1609459200}, + {"zero timestamp", false, 0}, + {"max timestamp", true, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgSysTime{ + GetRemoteTime: tt.getRemoteTime, + Timestamp: tt.timestamp, + } + + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysTime{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + 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) + } + }) + } +} + +func TestMsgSysPingOpcode(t *testing.T) { + pkt := &MsgSysPing{} + if pkt.Opcode() != network.MSG_SYS_PING { + t.Errorf("Opcode() = %s, want MSG_SYS_PING", pkt.Opcode()) + } +} + +func TestMsgSysTimeOpcode(t *testing.T) { + pkt := &MsgSysTime{} + if pkt.Opcode() != network.MSG_SYS_TIME { + t.Errorf("Opcode() = %s, want MSG_SYS_TIME", pkt.Opcode()) + } +} + +func TestFromOpcodeSystemPackets(t *testing.T) { + // Test all system packet opcodes return non-nil + systemOpcodes := []network.PacketID{ + network.MSG_SYS_reserve01, + network.MSG_SYS_reserve02, + network.MSG_SYS_reserve03, + network.MSG_SYS_reserve04, + network.MSG_SYS_reserve05, + network.MSG_SYS_reserve06, + network.MSG_SYS_reserve07, + network.MSG_SYS_ADD_OBJECT, + network.MSG_SYS_DEL_OBJECT, + network.MSG_SYS_DISP_OBJECT, + network.MSG_SYS_HIDE_OBJECT, + network.MSG_SYS_END, + network.MSG_SYS_NOP, + network.MSG_SYS_ACK, + network.MSG_SYS_LOGIN, + network.MSG_SYS_LOGOUT, + network.MSG_SYS_SET_STATUS, + network.MSG_SYS_PING, + network.MSG_SYS_TIME, + } + + for _, opcode := range systemOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestFromOpcodeStagePackets(t *testing.T) { + stageOpcodes := []network.PacketID{ + network.MSG_SYS_CREATE_STAGE, + network.MSG_SYS_STAGE_DESTRUCT, + network.MSG_SYS_ENTER_STAGE, + network.MSG_SYS_BACK_STAGE, + network.MSG_SYS_MOVE_STAGE, + network.MSG_SYS_LEAVE_STAGE, + network.MSG_SYS_LOCK_STAGE, + network.MSG_SYS_UNLOCK_STAGE, + network.MSG_SYS_RESERVE_STAGE, + network.MSG_SYS_UNRESERVE_STAGE, + network.MSG_SYS_SET_STAGE_PASS, + } + + for _, opcode := range stageOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestOpcodeMatches(t *testing.T) { + // Verify that packets return the same opcode they were created from + tests := []network.PacketID{ + network.MSG_HEAD, + network.MSG_SYS_PING, + network.MSG_SYS_TIME, + network.MSG_SYS_END, + network.MSG_SYS_NOP, + network.MSG_SYS_ACK, + network.MSG_SYS_LOGIN, + network.MSG_SYS_CREATE_STAGE, + } + + for _, opcode := range tests { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Skip("opcode not implemented") + } + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} + +func TestParserInterface(t *testing.T) { + // Verify Parser interface works + var p Parser = &MsgSysPing{} + bf := byteframe.NewByteFrame() + bf.WriteUint32(123) + bf.Seek(0, io.SeekStart) + + err := p.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Errorf("Parse() error = %v", err) + } +} + +func TestBuilderInterface(t *testing.T) { + // Verify Builder interface works + var b Builder = &MsgSysPing{AckHandle: 456} + bf := byteframe.NewByteFrame() + + err := b.Build(bf, &clientctx.ClientContext{}) + if err != nil { + t.Errorf("Build() error = %v", err) + } + if len(bf.Data()) == 0 { + t.Error("Build() should write data") + } +} + +func TestOpcoderInterface(t *testing.T) { + // Verify Opcoder interface works + var o Opcoder = &MsgSysPing{} + opcode := o.Opcode() + + if opcode != network.MSG_SYS_PING { + t.Errorf("Opcode() = %s, want MSG_SYS_PING", opcode) + } +} + +func TestClientContextNilSafe(t *testing.T) { + // Some packets may need to handle nil ClientContext + pkt := &MsgSysPing{AckHandle: 123} + bf := byteframe.NewByteFrame() + + // This should not panic even with nil context (implementation dependent) + // Note: The actual behavior depends on implementation + err := pkt.Build(bf, nil) + if err != nil { + // Error is acceptable if nil context is not supported + t.Logf("Build() with nil context returned error: %v", err) + } +} + +func TestMsgSysPingBuildFormat(t *testing.T) { + pkt := &MsgSysPing{AckHandle: 0x12345678} + bf := byteframe.NewByteFrame() + pkt.Build(bf, &clientctx.ClientContext{}) + + data := bf.Data() + if len(data) != 4 { + t.Errorf("Build() data len = %d, want 4", len(data)) + } + + // Verify big-endian format (default) + if data[0] != 0x12 || data[1] != 0x34 || data[2] != 0x56 || data[3] != 0x78 { + t.Errorf("Build() data = %x, want 12345678", data) + } +} + +func TestMsgSysTimeBuildFormat(t *testing.T) { + pkt := &MsgSysTime{ + GetRemoteTime: true, + Timestamp: 0xDEADBEEF, + } + bf := byteframe.NewByteFrame() + pkt.Build(bf, &clientctx.ClientContext{}) + + data := bf.Data() + if len(data) != 5 { + t.Errorf("Build() data len = %d, want 5 (1 bool + 4 uint32)", len(data)) + } + + // First byte is bool (1 = true) + if data[0] != 1 { + t.Errorf("GetRemoteTime byte = %d, want 1", data[0]) + } +} + +func TestMsgSysNop(t *testing.T) { + pkt := FromOpcode(network.MSG_SYS_NOP) + if pkt == nil { + t.Fatal("FromOpcode(MSG_SYS_NOP) returned nil") + } + if pkt.Opcode() != network.MSG_SYS_NOP { + t.Errorf("Opcode() = %s, want MSG_SYS_NOP", pkt.Opcode()) + } +} + +func TestMsgSysEnd(t *testing.T) { + pkt := FromOpcode(network.MSG_SYS_END) + if pkt == nil { + t.Fatal("FromOpcode(MSG_SYS_END) returned nil") + } + if pkt.Opcode() != network.MSG_SYS_END { + t.Errorf("Opcode() = %s, want MSG_SYS_END", pkt.Opcode()) + } +} + +func TestMsgHead(t *testing.T) { + pkt := FromOpcode(network.MSG_HEAD) + if pkt == nil { + t.Fatal("FromOpcode(MSG_HEAD) returned nil") + } + if pkt.Opcode() != network.MSG_HEAD { + t.Errorf("Opcode() = %s, want MSG_HEAD", pkt.Opcode()) + } +} + +func TestMsgSysAck(t *testing.T) { + pkt := FromOpcode(network.MSG_SYS_ACK) + if pkt == nil { + t.Fatal("FromOpcode(MSG_SYS_ACK) returned nil") + } + if pkt.Opcode() != network.MSG_SYS_ACK { + t.Errorf("Opcode() = %s, want MSG_SYS_ACK", pkt.Opcode()) + } +} + +func TestBinaryPackets(t *testing.T) { + binaryOpcodes := []network.PacketID{ + network.MSG_SYS_CAST_BINARY, + network.MSG_SYS_CASTED_BINARY, + network.MSG_SYS_SET_STAGE_BINARY, + network.MSG_SYS_GET_STAGE_BINARY, + network.MSG_SYS_WAIT_STAGE_BINARY, + } + + for _, opcode := range binaryOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestEnumeratePackets(t *testing.T) { + enumOpcodes := []network.PacketID{ + network.MSG_SYS_ENUMERATE_CLIENT, + network.MSG_SYS_ENUMERATE_STAGE, + } + + for _, opcode := range enumOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestSemaphorePackets(t *testing.T) { + semaOpcodes := []network.PacketID{ + network.MSG_SYS_CREATE_ACQUIRE_SEMAPHORE, + network.MSG_SYS_ACQUIRE_SEMAPHORE, + network.MSG_SYS_RELEASE_SEMAPHORE, + network.MSG_SYS_CHECK_SEMAPHORE, + } + + for _, opcode := range semaOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestObjectPackets(t *testing.T) { + objOpcodes := []network.PacketID{ + network.MSG_SYS_ADD_OBJECT, + network.MSG_SYS_DEL_OBJECT, + network.MSG_SYS_DISP_OBJECT, + network.MSG_SYS_HIDE_OBJECT, + } + + for _, opcode := range objOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestLogPackets(t *testing.T) { + logOpcodes := []network.PacketID{ + network.MSG_SYS_TERMINAL_LOG, + network.MSG_SYS_ISSUE_LOGKEY, + network.MSG_SYS_RECORD_LOG, + } + + for _, opcode := range logOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestMHFSaveLoad(t *testing.T) { + saveLoadOpcodes := []network.PacketID{ + network.MSG_MHF_SAVEDATA, + network.MSG_MHF_LOADDATA, + } + + for _, opcode := range saveLoadOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + } + }) + } +} + +func TestMsgSysCreateStageParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantHandle uint32 + wantUnk0 uint8 + wantPlayers uint8 + wantStageID string + }{ + { + name: "simple stage", + data: append([]byte{0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x05}, append([]byte("test"), 0x00)...), + wantHandle: 1, + wantUnk0: 2, + wantPlayers: 4, + wantStageID: "test", + }, + { + name: "empty stage ID", + data: []byte{0x12, 0x34, 0x56, 0x78, 0x01, 0x02, 0x00}, + wantHandle: 0x12345678, + wantUnk0: 1, + wantPlayers: 2, + wantStageID: "", + }, + { + name: "with null terminator", + data: append([]byte{0x00, 0x00, 0x00, 0x0A, 0x01, 0x01, 0x08}, append([]byte("stage01"), 0x00)...), + wantHandle: 10, + wantUnk0: 1, + wantPlayers: 1, + wantStageID: "stage01", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCreateStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.wantHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.wantHandle) + } + if pkt.Unk0 != tt.wantUnk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.wantUnk0) + } + if pkt.PlayerCount != tt.wantPlayers { + t.Errorf("PlayerCount = %d, want %d", pkt.PlayerCount, tt.wantPlayers) + } + if pkt.StageID != tt.wantStageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.wantStageID) + } + }) + } +} + +func TestMsgSysEnterStageParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantHandle uint32 + wantUnk bool + wantStageID string + }{ + { + name: "enter mezeporta", + data: append([]byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x0F}, append([]byte("sl1Ns200p0a0u0"), 0x00)...), + wantHandle: 1, + wantUnk: false, + wantStageID: "sl1Ns200p0a0u0", + }, + { + name: "with unk bool set", + data: append([]byte{0xAB, 0xCD, 0xEF, 0x12, 0x01, 0x05}, append([]byte("room1"), 0x00)...), + wantHandle: 0xABCDEF12, + wantUnk: true, + wantStageID: "room1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysEnterStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.wantHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.wantHandle) + } + if pkt.Unk != tt.wantUnk { + t.Errorf("Unk = %v, want %v", pkt.Unk, tt.wantUnk) + } + if pkt.StageID != tt.wantStageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.wantStageID) + } + }) + } +} + +func TestMsgSysMoveStageParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantHandle uint32 + wantUnkBool uint8 + wantStageID string + }{ + { + name: "move to quest stage", + data: append([]byte{0x00, 0x00, 0x12, 0x34, 0x00, 0x06}, []byte("quest1")...), + wantHandle: 0x1234, + wantUnkBool: 0, + wantStageID: "quest1", + }, + { + name: "with null in string", + data: append([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x08}, append([]byte("stage"), []byte{0x00, 0x00, 0x00}...)...), + wantHandle: 0xFFFFFFFF, + wantUnkBool: 1, + wantStageID: "stage", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysMoveStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.wantHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.wantHandle) + } + if pkt.UnkBool != tt.wantUnkBool { + t.Errorf("UnkBool = %d, want %d", pkt.UnkBool, tt.wantUnkBool) + } + if pkt.StageID != tt.wantStageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.wantStageID) + } + }) + } +} + +func TestMsgSysLockStageParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantHandle uint32 + wantStageID string + }{ + { + name: "lock stage", + data: append([]byte{0x00, 0x00, 0x00, 0x05, 0x01, 0x01, 0x06}, append([]byte("room01"), 0x00)...), + wantHandle: 5, + wantStageID: "room01", + }, + { + name: "different unk values", + data: append([]byte{0x12, 0x34, 0x56, 0x78, 0x02, 0x03, 0x04}, append([]byte("test"), 0x00)...), + wantHandle: 0x12345678, + wantStageID: "test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLockStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.wantHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.wantHandle) + } + if pkt.StageID != tt.wantStageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.wantStageID) + } + }) + } +} + +func TestMsgSysUnlockStageRoundTrip(t *testing.T) { + tests := []struct { + name string + unk0 uint16 + }{ + {"zero value", 0}, + {"typical value", 1}, + {"max value", 0xFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := &clientctx.ClientContext{} + + // Build (returns NOT IMPLEMENTED) + original := &MsgSysUnlockStage{} + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err == nil { + t.Fatal("Build() expected NOT IMPLEMENTED error") + } + + // Parse should consume a uint16 without error + bf = byteframe.NewByteFrame() + bf.WriteUint16(tt.unk0) + bf.Seek(0, io.SeekStart) + parsed := &MsgSysUnlockStage{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + }) + } +} + +func TestMsgSysBackStageParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantHandle uint32 + }{ + {"simple handle", []byte{0x00, 0x00, 0x00, 0x01}, 1}, + {"large handle", []byte{0xDE, 0xAD, 0xBE, 0xEF}, 0xDEADBEEF}, + {"zero handle", []byte{0x00, 0x00, 0x00, 0x00}, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysBackStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.wantHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.wantHandle) + } + }) + } +} + +func TestMsgSysLogoutParse(t *testing.T) { + tests := []struct { + name string + data []byte + wantUnk0 uint8 + }{ + {"typical logout", []byte{0x01}, 1}, + {"zero value", []byte{0x00}, 0}, + {"max value", []byte{0xFF}, 255}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBytes(tt.data) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLogout{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.Unk0 != tt.wantUnk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.wantUnk0) + } + }) + } +} + +func TestMsgSysLoginParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + charID0 uint32 + loginTokenNumber uint32 + hardcodedZero0 uint16 + requestVersion uint16 + charID1 uint32 + hardcodedZero1 uint16 + tokenStrLen uint16 + tokenString string + }{ + { + name: "typical login", + ackHandle: 1, + charID0: 12345, + loginTokenNumber: 67890, + hardcodedZero0: 0, + requestVersion: 1, + charID1: 12345, + hardcodedZero1: 0, + tokenStrLen: 0x11, + tokenString: "abc123token", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.charID0) + bf.WriteUint32(tt.loginTokenNumber) + bf.WriteUint16(tt.hardcodedZero0) + bf.WriteUint16(tt.requestVersion) + bf.WriteUint32(tt.charID1) + bf.WriteUint16(tt.hardcodedZero1) + bf.WriteUint16(tt.tokenStrLen) + bf.WriteBytes(append([]byte(tt.tokenString), 0x00)) // null terminated + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLogin{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.CharID0 != tt.charID0 { + t.Errorf("CharID0 = %d, want %d", pkt.CharID0, tt.charID0) + } + if pkt.LoginTokenNumber != tt.loginTokenNumber { + t.Errorf("LoginTokenNumber = %d, want %d", pkt.LoginTokenNumber, tt.loginTokenNumber) + } + if pkt.RequestVersion != tt.requestVersion { + t.Errorf("RequestVersion = %d, want %d", pkt.RequestVersion, tt.requestVersion) + } + if pkt.LoginTokenString != tt.tokenString { + t.Errorf("LoginTokenString = %q, want %q", pkt.LoginTokenString, tt.tokenString) + } + }) + } +} diff --git a/network/mhfpacket/msg_batch_parse_test.go b/network/mhfpacket/msg_batch_parse_test.go new file mode 100644 index 000000000..0695af9c4 --- /dev/null +++ b/network/mhfpacket/msg_batch_parse_test.go @@ -0,0 +1,2229 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// TestBatchParseAckHandleOnly tests Parse for packets that only read AckHandle (uint32). +func TestBatchParseAckHandleOnly(t *testing.T) { + packets := []struct { + name string + pkt MHFPacket + }{ + {"MsgMhfLoaddata", &MsgMhfLoaddata{}}, + {"MsgMhfLoadFavoriteQuest", &MsgMhfLoadFavoriteQuest{}}, + {"MsgMhfReadGuildcard", &MsgMhfReadGuildcard{}}, + {"MsgMhfGetEtcPoints", &MsgMhfGetEtcPoints{}}, + {"MsgMhfGetGuildMissionList", &MsgMhfGetGuildMissionList{}}, + {"MsgMhfGetGuildMissionRecord", &MsgMhfGetGuildMissionRecord{}}, + {"MsgMhfGetGuildTresureSouvenir", &MsgMhfGetGuildTresureSouvenir{}}, + {"MsgMhfAcquireGuildTresureSouvenir", &MsgMhfAcquireGuildTresureSouvenir{}}, + {"MsgMhfEnumerateFestaIntermediatePrize", &MsgMhfEnumerateFestaIntermediatePrize{}}, + {"MsgMhfEnumerateFestaPersonalPrize", &MsgMhfEnumerateFestaPersonalPrize{}}, + {"MsgMhfGetGuildWeeklyBonusMaster", &MsgMhfGetGuildWeeklyBonusMaster{}}, + {"MsgMhfGetGuildWeeklyBonusActiveCount", &MsgMhfGetGuildWeeklyBonusActiveCount{}}, + {"MsgMhfGetEquipSkinHist", &MsgMhfGetEquipSkinHist{}}, + {"MsgMhfGetRejectGuildScout", &MsgMhfGetRejectGuildScout{}}, + {"MsgMhfGetKeepLoginBoostStatus", &MsgMhfGetKeepLoginBoostStatus{}}, + {"MsgMhfAcquireMonthlyReward", &MsgMhfAcquireMonthlyReward{}}, + {"MsgMhfGetGuildScoutList", &MsgMhfGetGuildScoutList{}}, + {"MsgMhfGetGuildManageRight", &MsgMhfGetGuildManageRight{}}, + {"MsgMhfGetRengokuRankingRank", &MsgMhfGetRengokuRankingRank{}}, + {"MsgMhfGetUdMyPoint", &MsgMhfGetUdMyPoint{}}, + {"MsgMhfGetUdTotalPointInfo", &MsgMhfGetUdTotalPointInfo{}}, + {"MsgMhfCreateMercenary", &MsgMhfCreateMercenary{}}, + {"MsgMhfEnumerateMercenaryLog", &MsgMhfEnumerateMercenaryLog{}}, + {"MsgMhfLoadLegendDispatch", &MsgMhfLoadLegendDispatch{}}, + {"MsgMhfGetBoostRight", &MsgMhfGetBoostRight{}}, + {"MsgMhfPostBoostTimeQuestReturn", &MsgMhfPostBoostTimeQuestReturn{}}, + {"MsgMhfGetFpointExchangeList", &MsgMhfGetFpointExchangeList{}}, + {"MsgMhfGetRewardSong", &MsgMhfGetRewardSong{}}, + {"MsgMhfUseRewardSong", &MsgMhfUseRewardSong{}}, + {"MsgMhfGetKouryouPoint", &MsgMhfGetKouryouPoint{}}, + {"MsgMhfGetTrendWeapon", &MsgMhfGetTrendWeapon{}}, + {"MsgMhfInfoScenarioCounter", &MsgMhfInfoScenarioCounter{}}, + {"MsgMhfLoadScenarioData", &MsgMhfLoadScenarioData{}}, + {"MsgMhfLoadRengokuData", &MsgMhfLoadRengokuData{}}, + {"MsgMhfLoadMezfesData", &MsgMhfLoadMezfesData{}}, + {"MsgMhfLoadPlateMyset", &MsgMhfLoadPlateMyset{}}, + } + + ctx := &clientctx.ClientContext{} + for _, tc := range packets { + t.Run(tc.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.Seek(0, io.SeekStart) + + err := tc.pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + }) + } +} + +// TestBatchParseTwoUint32 tests packets with AckHandle + one uint32 field. +func TestBatchParseTwoUint32(t *testing.T) { + packets := []struct { + name string + pkt MHFPacket + }{ + {"MsgMhfListMail", &MsgMhfListMail{}}, + {"MsgMhfEnumerateTitle", &MsgMhfEnumerateTitle{}}, + {"MsgMhfInfoGuild", &MsgMhfInfoGuild{}}, + {"MsgMhfCheckDailyCafepoint", &MsgMhfCheckDailyCafepoint{}}, + {"MsgMhfEntryRookieGuild", &MsgMhfEntryRookieGuild{}}, + {"MsgMhfReleaseEvent", &MsgMhfReleaseEvent{}}, + {"MsgMhfSetGuildMissionTarget", &MsgMhfSetGuildMissionTarget{}}, + {"MsgMhfCancelGuildMissionTarget", &MsgMhfCancelGuildMissionTarget{}}, + {"MsgMhfAcquireFestaIntermediatePrize", &MsgMhfAcquireFestaIntermediatePrize{}}, + {"MsgMhfAcquireFestaPersonalPrize", &MsgMhfAcquireFestaPersonalPrize{}}, + {"MsgMhfGetGachaPlayHistory", &MsgMhfGetGachaPlayHistory{}}, + {"MsgMhfPostGuildScout", &MsgMhfPostGuildScout{}}, + {"MsgMhfCancelGuildScout", &MsgMhfCancelGuildScout{}}, + {"MsgMhfGetEnhancedMinidata", &MsgMhfGetEnhancedMinidata{}}, + {"MsgMhfPostBoostTime", &MsgMhfPostBoostTime{}}, + {"MsgMhfStartBoostTime", &MsgMhfStartBoostTime{}}, + {"MsgMhfAcquireGuildAdventure", &MsgMhfAcquireGuildAdventure{}}, + {"MsgMhfGetBoxGachaInfo", &MsgMhfGetBoxGachaInfo{}}, + {"MsgMhfResetBoxGachaInfo", &MsgMhfResetBoxGachaInfo{}}, + {"MsgMhfAddKouryouPoint", &MsgMhfAddKouryouPoint{}}, + {"MsgMhfExchangeKouryouPoint", &MsgMhfExchangeKouryouPoint{}}, + {"MsgMhfInfoJoint", &MsgMhfInfoJoint{}}, + } + + ctx := &clientctx.ClientContext{} + for _, tc := range packets { + t.Run(tc.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(0xDEADBEEF) // Second uint32 + bf.WriteUint32(0xCAFEBABE) // Padding for 3-field packets + bf.Seek(0, io.SeekStart) + + err := tc.pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + }) + } +} + +// TestBatchParseMultiField tests packets with various field combinations. +func TestBatchParseMultiField(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("MsgMhfGetRengokuBinary", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetRengokuBinary{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateDistItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // DistType + bf.WriteUint8(3) // Unk1 + bf.WriteUint16(4) // Unk2 + bf.WriteUint8(0) // Unk3 length (Z1+ mode) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateDistItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.DistType != 2 || pkt.Unk1 != 3 || pkt.Unk2 != 4 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfApplyDistItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // DistributionType + bf.WriteUint32(3) // DistributionID + bf.WriteUint32(4) // Unk2 + bf.WriteUint32(5) // Unk3 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfApplyDistItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.DistributionType != 2 || pkt.DistributionID != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfAcquireDistItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // DistributionType + bf.WriteUint32(3) // DistributionID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireDistItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.DistributionType != 2 || pkt.DistributionID != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfGetDistDescription", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint32(3) // DistributionID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetDistDescription{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk0 != 2 || pkt.DistributionID != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfRegisterEvent", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint16(3) // WorldID + bf.WriteUint16(4) // LandID + bf.WriteBool(true) // Unk1 + bf.WriteUint8(0) // Zeroed (discarded) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfRegisterEvent{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk0 != 2 || pkt.WorldID != 3 || pkt.LandID != 4 || !pkt.Unk1 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfUpdateCafepoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Zeroed (discarded) + bf.WriteUint16(3) // Zeroed (discarded) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateCafepoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfUpdateEtcPoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // PointType + bf.WriteInt16(-5) // Delta + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateEtcPoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.PointType != 2 || pkt.Delta != -5 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfAcquireTitle", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Title count + bf.WriteUint16(0) // Zeroed + bf.WriteUint16(4) // TitleIDs[0] + bf.WriteUint16(5) // TitleIDs[1] + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireTitle{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.TitleIDs) != 2 || pkt.TitleIDs[0] != 4 || pkt.TitleIDs[1] != 5 { + t.Errorf("TitleIDs = %v, want [4, 5]", pkt.TitleIDs) + } + }) + + t.Run("MsgSysHideClient", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteBool(true) // Hide + bf.WriteUint8(0) // Zeroed (discarded) + bf.WriteUint8(0) // Zeroed (discarded) + bf.WriteUint8(0) // Zeroed (discarded) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysHideClient{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if !pkt.Hide { + t.Error("field mismatch") + } + }) + + t.Run("MsgSysIssueLogkey", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint16(0) // Zeroed (discarded) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysIssueLogkey{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk0 != 2 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfGetTinyBin", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint8(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetTinyBin{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk0 != 2 || pkt.Unk1 != 3 || pkt.Unk2 != 4 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfGetPaperData", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint32(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetPaperData{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Unk2 != 4 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfGetEarthValue", func(t *testing.T) { + bf := byteframe.NewByteFrame() + for i := 0; i < 8; i++ { + bf.WriteUint32(uint32(i + 1)) + } + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetEarthValue{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk6 != 8 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfPresentBox", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint32(2) // Unk2 (controls Unk7 slice length) + bf.WriteUint32(5) // Unk3 + bf.WriteUint32(6) // Unk4 + bf.WriteUint32(7) // Unk5 + bf.WriteUint32(8) // Unk6 + bf.WriteUint32(9) // Unk7[0] + bf.WriteUint32(10) // Unk7[1] + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPresentBox{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 || pkt.Unk2 != 2 || pkt.Unk6 != 8 || len(pkt.Unk7) != 2 || pkt.Unk7[1] != 10 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfReadMail", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // AccIndex + bf.WriteUint8(3) // Index + bf.WriteUint16(4) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfReadMail{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AccIndex != 2 || pkt.Index != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfOprMember", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBool(true) // Blacklist + bf.WriteBool(false) // Operation + bf.WriteUint8(0) // Padding + bf.WriteUint8(1) // CharID count + bf.WriteUint32(99) // CharIDs[0] + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfOprMember{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if !pkt.Blacklist || pkt.Operation || len(pkt.CharIDs) != 1 || pkt.CharIDs[0] != 99 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfListMember", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(0) // Zeroed + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfListMember{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Unk0 != 2 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfTransferItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint8(0) // Zeroed + bf.WriteUint16(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfTransferItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Unk0 != 2 || pkt.Unk1 != 3 || pkt.Unk2 != 4 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfMercenaryHuntdata", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfMercenaryHuntdata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Unk0 != 2 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfEnumeratePrice", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(0) // Unk0 + bf.WriteUint16(0) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumeratePrice{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateUnionItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateUnionItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.AckHandle != 1 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfEnumerateGuildItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GuildId + bf.WriteUint16(3) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateGuildItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.GuildID != 2 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfEnumerateGuildMember", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint32(99) // GuildID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateGuildMember{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.GuildID != 99 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfOperateGuildMember", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GuildID + bf.WriteUint32(99) // CharID + bf.WriteUint8(1) // Action + bf.WriteBytes([]byte{0, 0, 0}) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfOperateGuildMember{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.CharID != 99 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfUpdateEquipSkinHist", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // MogType + bf.WriteUint16(3) // ArmourID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateEquipSkinHist{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.MogType != 2 || pkt.ArmourID != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfSetRejectGuildScout", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBool(true) // Reject + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetRejectGuildScout{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if !pkt.Reject { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfUseKeepLoginBoost", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(3) // BoostWeekUsed + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUseKeepLoginBoost{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.BoostWeekUsed != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfSetCaAchievementHist", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetCaAchievementHist{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAddGuildWeeklyBonusExceptionalUser", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // NumUsers + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAddGuildWeeklyBonusExceptionalUser{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetLobbyCrowd", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Server + bf.WriteUint32(3) // Room + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetLobbyCrowd{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSexChanger", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // Gender + bf.WriteUint8(0) // Unk0 + bf.WriteUint8(0) // Unk1 + bf.WriteUint8(0) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSexChanger{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Gender != 1 { + t.Error("field mismatch") + } + }) + + t.Run("MsgMhfSetKiju", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(5) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetKiju{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAddUdPoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk1 + bf.WriteUint32(3) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAddUdPoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetWeeklySeibatuRankingReward", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) + bf.WriteUint32(3) + bf.WriteUint32(4) + bf.WriteUint32(5) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetWeeklySeibatuRankingReward{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetEarthStatus", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetEarthStatus{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAddGuildMissionCount", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // MissionID + bf.WriteUint32(3) // Count + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAddGuildMissionCount{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateAiroulist", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint16(3) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateAiroulist{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfOperateGuildTresureReport", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(10) // HuntID + bf.WriteUint16(2) // State + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfOperateGuildTresureReport{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAcquireGuildTresure", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(10) // HuntID + bf.WriteUint8(1) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireGuildTresure{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateGuildTresure", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(5) // MaxHunts + bf.WriteUint32(0) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateGuildTresure{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetTenrouirai", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint16(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetTenrouirai{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfPostTenrouirai", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint32(4) // Unk2 + bf.WriteUint32(5) // Unk3 + bf.WriteUint32(6) // Unk4 + bf.WriteUint8(7) // Unk5 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPostTenrouirai{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetSeibattle", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint32(4) // Unk2 + bf.WriteUint8(5) // Unk3 + bf.WriteUint16(6) // Unk4 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetSeibattle{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetRyoudama", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint32(99) // GuildID + bf.WriteUint8(4) // Unk3 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetRyoudama{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateRengokuRanking", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Leaderboard + bf.WriteUint16(3) // Unk1 + bf.WriteUint16(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateRengokuRanking{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetAdditionalBeatReward", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) + bf.WriteUint32(3) + bf.WriteUint32(4) + bf.WriteUint32(5) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetAdditionalBeatReward{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSetRestrictionEvent", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // Unk1 + bf.WriteUint32(4) // Unk2 + bf.WriteUint8(5) // Unk3 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetRestrictionEvent{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfUpdateUseTrendWeaponLog", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint16(3) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateUseTrendWeaponLog{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfDisplayedAchievement", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(42) // AchievementID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfDisplayedAchievement{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfRegistGuildCooking", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // OverwriteID + bf.WriteUint16(3) // MealID + bf.WriteUint8(4) // Success + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfRegistGuildCooking{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfChargeGuildAdventure", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // ID + bf.WriteUint32(3) // Amount + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfChargeGuildAdventure{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfRegistGuildAdventure", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Destination + bf.WriteUint32(0) // discard CharID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfRegistGuildAdventure{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfReadMercenaryW", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Op + bf.WriteUint8(3) // Unk1 + bf.WriteUint16(4) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfReadMercenaryW{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfReadMercenaryM", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // CharID + bf.WriteUint32(3) // MercID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfReadMercenaryM{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfContractMercenary", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // PactMercID + bf.WriteUint32(3) // CID + bf.WriteUint8(4) // Op + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfContractMercenary{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetGuildTargetMemberNum", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GuildID + bf.WriteUint8(3) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetGuildTargetMemberNum{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSetGuildManageRight", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // CharID + bf.WriteBool(true) // Allowed + bf.WriteBytes([]byte{0, 0, 0}) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetGuildManageRight{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAnswerGuildScout", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // LeaderID + bf.WriteBool(true) // Answer + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAnswerGuildScout{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfPlayStepupGacha", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GachaID + bf.WriteUint8(3) // RollType + bf.WriteUint8(4) // GachaType + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPlayStepupGacha{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfPlayBoxGacha", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GachaID + bf.WriteUint8(3) // RollType + bf.WriteUint8(4) // GachaType + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPlayBoxGacha{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfPlayNormalGacha", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GachaID + bf.WriteUint8(3) // RollType + bf.WriteUint8(4) // GachaType + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPlayNormalGacha{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfReceiveGachaItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(5) // Max + bf.WriteBool(false) // Freeze + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfReceiveGachaItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetStepupStatus", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GachaID + bf.WriteUint8(3) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetStepupStatus{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfUseGachaPoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint32(3) // TrialCoins + bf.WriteUint32(4) // PremiumCoins + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUseGachaPoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateGuildMessageBoard", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint32(3) // MaxPosts + bf.WriteUint32(4) // BoardType + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateGuildMessageBoard{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) +} + +// TestBatchParseVariableLength tests packets with variable-length data. +func TestBatchParseVariableLength(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("MsgMhfSaveFavoriteQuest", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(4) // DataSize + bf.WriteBytes([]byte{0x01, 0x02, 0x03, 0x04}) // Data + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveFavoriteQuest{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.Data) != 4 { + t.Errorf("Data len = %d, want 4", len(pkt.Data)) + } + }) + + t.Run("MsgMhfSavedata_withDataSize", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(0) // AllocMemSize + bf.WriteUint8(0) // SaveType + bf.WriteUint32(0) // Unk1 + bf.WriteUint32(3) // DataSize (non-zero) + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavedata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.RawDataPayload) != 3 { + t.Errorf("RawDataPayload len = %d, want 3", len(pkt.RawDataPayload)) + } + }) + + t.Run("MsgMhfSavedata_withAllocMem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // AllocMemSize + bf.WriteUint8(0) // SaveType + bf.WriteUint32(0) // Unk1 + bf.WriteUint32(0) // DataSize (zero -> use AllocMemSize) + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavedata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.RawDataPayload) != 2 { + t.Errorf("RawDataPayload len = %d, want 2", len(pkt.RawDataPayload)) + } + }) + + t.Run("MsgMhfTransitMessage", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint16(4) // SearchType + bf.WriteUint16(3) // inline data length + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfTransitMessage{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.MessageData) != 3 { + t.Errorf("MessageData len = %d, want 3", len(pkt.MessageData)) + } + }) + + t.Run("MsgMhfPostTinyBin", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // Unk0 + bf.WriteUint8(3) // Unk1 + bf.WriteUint8(4) // Unk2 + bf.WriteUint16(2) // inline data length + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfPostTinyBin{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.Data) != 2 { + t.Errorf("Data len = %d, want 2", len(pkt.Data)) + } + }) + + t.Run("MsgSysRecordLog", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // Unk0 + bf.WriteUint16(3) // Unk1 + bf.WriteUint16(4) // HardcodedDataSize + bf.WriteUint32(5) // Unk3 + bf.WriteBytes([]byte{0x01, 0x02, 0x03, 0x04}) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysRecordLog{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.Data) != 4 { + t.Errorf("Data len = %d, want 4", len(pkt.Data)) + } + }) + + t.Run("MsgMhfUpdateInterior", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBytes(make([]byte, 20)) // InteriorData + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfUpdateInterior{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if len(pkt.InteriorData) != 20 { + t.Error("InteriorData wrong size") + } + }) + + t.Run("MsgMhfSavePartner", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(3) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavePartner{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveOtomoAirou", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveOtomoAirou{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveHunterNavi", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBool(true) // IsDataDiff + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveHunterNavi{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSavePlateData", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(3) // DataSize + bf.WriteBool(false) // IsDataDiff + bf.WriteBytes([]byte{0x01, 0x02, 0x03}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavePlateData{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSavePlateBox", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBool(true) // IsDataDiff + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavePlateBox{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSavePlateMyset", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSavePlateMyset{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveDecoMyset", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveDecoMyset{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveRengokuData", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveRengokuData{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveMezfesData", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveMezfesData{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSaveScenarioData", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(3) // DataSize + bf.WriteBytes([]byte{0x01, 0x02, 0x03}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSaveScenarioData{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAcquireExchangeShop", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(3) // DataSize + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireExchangeShop{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfSetEnhancedMinidata", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(0) // Unk0 + bf.WriteBytes(make([]byte, 0x400)) // RawDataPayload + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfSetEnhancedMinidata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetBbsUserStatus", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBytes(make([]byte, 12)) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetBbsUserStatus{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetBbsSnsStatus", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBytes(make([]byte, 12)) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetBbsSnsStatus{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) +} + +// TestBatchParseArrangeGuildMember tests the array-parsing packet. +func TestBatchParseArrangeGuildMember(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GuildID + bf.WriteUint16(3) // charCount + bf.WriteUint32(10) // CharIDs[0] + bf.WriteUint32(20) // CharIDs[1] + bf.WriteUint32(30) // CharIDs[2] + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfArrangeGuildMember{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if len(pkt.CharIDs) != 3 || pkt.CharIDs[2] != 30 { + t.Errorf("CharIDs = %v, want [10 20 30]", pkt.CharIDs) + } +} + +// TestBatchParseUpdateGuildIcon tests the guild icon array packet. +func TestBatchParseUpdateGuildIcon(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // GuildID + bf.WriteUint16(1) // PartCount + bf.WriteUint16(0) // Unk1 + // One part: 14 bytes + bf.WriteUint16(0) // Index + bf.WriteUint16(1) // ID + bf.WriteUint8(2) // Page + bf.WriteUint8(3) // Size + bf.WriteUint8(4) // Rotation + bf.WriteUint8(0xFF) // Red + bf.WriteUint8(0x00) // Green + bf.WriteUint8(0x80) // Blue + bf.WriteUint16(100) // PosX + bf.WriteUint16(200) // PosY + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfUpdateGuildIcon{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if len(pkt.IconParts) != 1 || pkt.IconParts[0].Red != 0xFF { + t.Error("icon parts mismatch") + } +} + +// TestBatchParseSysLoadRegister tests the fixed-zero validation packet. +func TestBatchParseSysLoadRegister(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // RegisterID + bf.WriteUint8(3) // Unk1 + bf.WriteUint16(0) // fixedZero0 + bf.WriteUint8(0) // fixedZero1 + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLoadRegister{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if pkt.RegisterID != 2 || pkt.Values != 3 { + t.Error("field mismatch") + } +} + +// TestBatchParseSysLoadRegisterNonZeroPadding tests that SysLoadRegister Parse +// succeeds even with non-zero values in the padding fields (they are discarded). +func TestBatchParseSysLoadRegisterNonZeroPadding(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // RegisterID + bf.WriteUint8(3) // Values + bf.WriteUint8(1) // Zeroed (discarded, non-zero is OK) + bf.WriteUint16(1) // Zeroed (discarded, non-zero is OK) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLoadRegister{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if pkt.AckHandle != 1 { + t.Errorf("AckHandle = %d, want 1", pkt.AckHandle) + } + if pkt.RegisterID != 2 { + t.Errorf("RegisterID = %d, want 2", pkt.RegisterID) + } + if pkt.Values != 3 { + t.Errorf("Values = %d, want 3", pkt.Values) + } +} + +// TestBatchParseSysOperateRegister tests the operate register packet. +func TestBatchParseSysOperateRegister(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // SemaphoreID + bf.WriteUint16(0) // fixedZero + bf.WriteUint16(3) // dataSize + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysOperateRegister{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if len(pkt.RawDataPayload) != 3 { + t.Error("payload size mismatch") + } +} + +// TestBatchParseSysOperateRegisterNonZeroPadding tests that SysOperateRegister Parse +// succeeds even with non-zero values in the padding field (it is discarded). +func TestBatchParseSysOperateRegisterNonZeroPadding(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // SemaphoreID + bf.WriteUint16(1) // Zeroed (discarded, non-zero is OK) + bf.WriteUint16(0) // dataSize + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysOperateRegister{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if pkt.AckHandle != 1 { + t.Errorf("AckHandle = %d, want 1", pkt.AckHandle) + } + if pkt.SemaphoreID != 2 { + t.Errorf("SemaphoreID = %d, want 2", pkt.SemaphoreID) + } + if len(pkt.RawDataPayload) != 0 { + t.Errorf("RawDataPayload len = %d, want 0", len(pkt.RawDataPayload)) + } +} + +// TestBatchParseSysGetFile tests the conditional scenario file packet. +func TestBatchParseSysGetFile(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("non-scenario", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBool(false) // IsScenario + bf.WriteUint8(5) // filenameLength + bf.WriteBytes([]byte("test\x00")) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysGetFile{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Filename != "test" || pkt.IsScenario { + t.Error("field mismatch") + } + }) + + t.Run("scenario", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBool(true) // IsScenario + bf.WriteUint8(0) // filenameLength (empty) + bf.WriteUint8(10) // CategoryID + bf.WriteUint32(100) // MainID + bf.WriteUint8(5) // ChapterID + bf.WriteUint8(0) // Flags + bf.Seek(0, io.SeekStart) + pkt := &MsgSysGetFile{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if !pkt.IsScenario || pkt.ScenarioIdentifer.MainID != 100 { + t.Error("field mismatch") + } + }) +} + +// TestBatchParseSysTerminalLog tests the entry-array packet. +func TestBatchParseSysTerminalLog(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(2) // LogID + bf.WriteUint16(1) // EntryCount + bf.WriteUint16(0) // Unk0 + // One entry: 4 + 1 + 1 + (15*2) = 36 bytes + bf.WriteUint32(0) // Index + bf.WriteUint8(1) // Type1 + bf.WriteUint8(2) // Type2 + for i := 0; i < 15; i++ { + bf.WriteInt16(int16(i)) + } + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysTerminalLog{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if len(pkt.Entries) != 1 || pkt.Entries[0].Type1 != 1 { + t.Error("entries mismatch") + } +} + +// TestBatchParseNoOpPackets tests packets with empty Parse (return nil). +func TestBatchParseNoOpPackets(t *testing.T) { + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + + packets := []struct { + name string + pkt MHFPacket + }{ + {"MsgSysExtendThreshold", &MsgSysExtendThreshold{}}, + {"MsgSysEnd", &MsgSysEnd{}}, + {"MsgSysNop", &MsgSysNop{}}, + {"MsgSysStageDestruct", &MsgSysStageDestruct{}}, + } + + for _, tc := range packets { + t.Run(tc.name, func(t *testing.T) { + if err := tc.pkt.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + }) + } +} + +// TestBatchParseNotImplemented tests that Parse returns NOT IMPLEMENTED for stub packets. +func TestBatchParseNotImplemented(t *testing.T) { + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + + packets := []MHFPacket{ + &MsgSysReserve01{}, &MsgSysReserve02{}, &MsgSysReserve03{}, + &MsgSysReserve04{}, &MsgSysReserve05{}, &MsgSysReserve06{}, + &MsgSysReserve07{}, &MsgSysReserve0C{}, &MsgSysReserve0D{}, + &MsgSysReserve0E{}, &MsgSysReserve4A{}, &MsgSysReserve4B{}, + &MsgSysReserve4C{}, &MsgSysReserve4D{}, &MsgSysReserve4E{}, + &MsgSysReserve4F{}, &MsgSysReserve55{}, &MsgSysReserve56{}, + &MsgSysReserve57{}, &MsgSysReserve5C{}, &MsgSysReserve5E{}, + &MsgSysReserve5F{}, &MsgSysReserve71{}, &MsgSysReserve72{}, + &MsgSysReserve73{}, &MsgSysReserve74{}, &MsgSysReserve75{}, + &MsgSysReserve76{}, &MsgSysReserve77{}, &MsgSysReserve78{}, + &MsgSysReserve79{}, &MsgSysReserve7A{}, &MsgSysReserve7B{}, + &MsgSysReserve7C{}, &MsgSysReserve7E{}, &MsgSysReserve18E{}, + &MsgSysReserve18F{}, &MsgSysReserve19E{}, &MsgSysReserve19F{}, + &MsgSysReserve1A4{}, &MsgSysReserve1A6{}, &MsgSysReserve1A7{}, + &MsgSysReserve1A8{}, &MsgSysReserve1A9{}, &MsgSysReserve1AA{}, + &MsgSysReserve1AB{}, &MsgSysReserve1AC{}, &MsgSysReserve1AD{}, + &MsgSysReserve1AE{}, &MsgSysReserve1AF{}, &MsgSysReserve19B{}, + &MsgSysReserve192{}, &MsgSysReserve193{}, &MsgSysReserve194{}, + &MsgSysReserve180{}, + &MsgMhfReserve10F{}, + // Empty-struct packets with NOT IMPLEMENTED Parse + &MsgHead{}, &MsgSysSetStatus{}, &MsgSysEcho{}, + &MsgSysLeaveStage{}, &MsgSysAddObject{}, &MsgSysDelObject{}, + &MsgSysDispObject{}, &MsgSysHideObject{}, + &MsgMhfServerCommand{}, &MsgMhfSetLoginwindow{}, &MsgMhfShutClient{}, + &MsgMhfUpdateGuildcard{}, + &MsgMhfGetCogInfo{}, + &MsgCaExchangeItem{}, + } + + for _, pkt := range packets { + t.Run(pkt.Opcode().String(), func(t *testing.T) { + err := pkt.Parse(bf, ctx) + if err == nil { + t.Error("expected NOT IMPLEMENTED error") + } + }) + } +} + +// TestBatchBuildNotImplemented tests that Build returns NOT IMPLEMENTED for many packets. +func TestBatchBuildNotImplemented(t *testing.T) { + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + + packets := []MHFPacket{ + &MsgMhfLoaddata{}, &MsgMhfSavedata{}, + &MsgMhfListMember{}, &MsgMhfOprMember{}, + &MsgMhfEnumerateDistItem{}, &MsgMhfApplyDistItem{}, &MsgMhfAcquireDistItem{}, + &MsgMhfGetDistDescription{}, &MsgMhfSendMail{}, &MsgMhfReadMail{}, + &MsgMhfListMail{}, &MsgMhfOprtMail{}, + &MsgMhfLoadFavoriteQuest{}, &MsgMhfSaveFavoriteQuest{}, + &MsgMhfRegisterEvent{}, &MsgMhfReleaseEvent{}, + &MsgMhfTransitMessage{}, &MsgMhfPresentBox{}, + &MsgMhfAcquireTitle{}, &MsgMhfEnumerateTitle{}, + &MsgMhfInfoGuild{}, &MsgMhfEnumerateGuild{}, + &MsgMhfCreateGuild{}, &MsgMhfOperateGuild{}, + &MsgMhfOperateGuildMember{}, &MsgMhfArrangeGuildMember{}, + &MsgMhfEnumerateGuildMember{}, &MsgMhfUpdateGuildIcon{}, + &MsgMhfInfoFesta{}, &MsgMhfEntryFesta{}, + &MsgMhfChargeFesta{}, &MsgMhfAcquireFesta{}, + &MsgMhfVoteFesta{}, &MsgMhfInfoTournament{}, + &MsgMhfEntryTournament{}, &MsgMhfAcquireTournament{}, + &MsgMhfUpdateCafepoint{}, &MsgMhfCheckDailyCafepoint{}, + &MsgMhfGetEtcPoints{}, &MsgMhfUpdateEtcPoint{}, + &MsgMhfReadGuildcard{}, &MsgMhfUpdateGuildcard{}, + &MsgMhfGetTinyBin{}, &MsgMhfPostTinyBin{}, + &MsgMhfGetPaperData{}, &MsgMhfGetEarthValue{}, + &MsgSysRecordLog{}, &MsgSysIssueLogkey{}, &MsgSysTerminalLog{}, + &MsgSysHideClient{}, &MsgSysGetFile{}, + &MsgSysOperateRegister{}, &MsgSysLoadRegister{}, + &MsgMhfGetGuildMissionList{}, &MsgMhfGetGuildMissionRecord{}, + &MsgMhfAddGuildMissionCount{}, &MsgMhfSetGuildMissionTarget{}, + &MsgMhfCancelGuildMissionTarget{}, + &MsgMhfEnumerateGuildTresure{}, &MsgMhfRegistGuildTresure{}, + &MsgMhfAcquireGuildTresure{}, &MsgMhfOperateGuildTresureReport{}, + &MsgMhfGetGuildTresureSouvenir{}, &MsgMhfAcquireGuildTresureSouvenir{}, + &MsgMhfEnumerateFestaIntermediatePrize{}, &MsgMhfAcquireFestaIntermediatePrize{}, + &MsgMhfEnumerateFestaPersonalPrize{}, &MsgMhfAcquireFestaPersonalPrize{}, + &MsgMhfGetGuildWeeklyBonusMaster{}, &MsgMhfGetGuildWeeklyBonusActiveCount{}, + &MsgMhfAddGuildWeeklyBonusExceptionalUser{}, + &MsgMhfGetEquipSkinHist{}, &MsgMhfUpdateEquipSkinHist{}, + &MsgMhfGetEnhancedMinidata{}, &MsgMhfSetEnhancedMinidata{}, + &MsgMhfGetLobbyCrowd{}, + &MsgMhfGetRejectGuildScout{}, &MsgMhfSetRejectGuildScout{}, + &MsgMhfGetKeepLoginBoostStatus{}, &MsgMhfUseKeepLoginBoost{}, + &MsgMhfAcquireMonthlyReward{}, + &MsgMhfPostGuildScout{}, &MsgMhfCancelGuildScout{}, + &MsgMhfAnswerGuildScout{}, &MsgMhfGetGuildScoutList{}, + &MsgMhfGetGuildManageRight{}, &MsgMhfSetGuildManageRight{}, + &MsgMhfGetGuildTargetMemberNum{}, + &MsgMhfPlayStepupGacha{}, &MsgMhfReceiveGachaItem{}, + &MsgMhfGetStepupStatus{}, &MsgMhfPlayNormalGacha{}, + &MsgMhfPlayBoxGacha{}, &MsgMhfGetBoxGachaInfo{}, &MsgMhfResetBoxGachaInfo{}, + &MsgMhfUseGachaPoint{}, &MsgMhfGetGachaPlayHistory{}, + &MsgMhfSavePartner{}, &MsgMhfSaveOtomoAirou{}, + &MsgMhfSaveHunterNavi{}, &MsgMhfSavePlateData{}, + &MsgMhfSavePlateBox{}, &MsgMhfSavePlateMyset{}, + &MsgMhfSaveDecoMyset{}, &MsgMhfSaveRengokuData{}, &MsgMhfSaveMezfesData{}, + &MsgMhfCreateMercenary{}, &MsgMhfSaveMercenary{}, + &MsgMhfReadMercenaryW{}, &MsgMhfReadMercenaryM{}, + &MsgMhfContractMercenary{}, &MsgMhfEnumerateMercenaryLog{}, + &MsgMhfRegistGuildCooking{}, &MsgMhfRegistGuildAdventure{}, + &MsgMhfAcquireGuildAdventure{}, &MsgMhfChargeGuildAdventure{}, + &MsgMhfLoadLegendDispatch{}, + &MsgMhfPostBoostTime{}, &MsgMhfStartBoostTime{}, + &MsgMhfPostBoostTimeQuestReturn{}, &MsgMhfGetBoostRight{}, + &MsgMhfGetFpointExchangeList{}, + &MsgMhfGetRewardSong{}, &MsgMhfUseRewardSong{}, + &MsgMhfGetKouryouPoint{}, &MsgMhfAddKouryouPoint{}, &MsgMhfExchangeKouryouPoint{}, + &MsgMhfSexChanger{}, &MsgMhfSetKiju{}, &MsgMhfAddUdPoint{}, + &MsgMhfGetTrendWeapon{}, &MsgMhfUpdateUseTrendWeaponLog{}, + &MsgMhfSetRestrictionEvent{}, + &MsgMhfGetWeeklySeibatuRankingReward{}, &MsgMhfGetEarthStatus{}, + &MsgMhfAddGuildMissionCount{}, + &MsgMhfEnumerateAiroulist{}, + &MsgMhfEnumerateRengokuRanking{}, &MsgMhfGetRengokuRankingRank{}, + &MsgMhfGetAdditionalBeatReward{}, + &MsgMhfSetCaAchievementHist{}, + &MsgMhfGetUdMyPoint{}, &MsgMhfGetUdTotalPointInfo{}, + &MsgMhfDisplayedAchievement{}, + &MsgMhfUpdateInterior{}, + &MsgMhfEnumerateUnionItem{}, + &MsgMhfEnumerateGuildItem{}, + &MsgMhfEnumerateGuildMember{}, + &MsgMhfEnumerateGuildMessageBoard{}, + &MsgMhfMercenaryHuntdata{}, + &MsgMhfEntryRookieGuild{}, + &MsgMhfEnumeratePrice{}, + &MsgMhfTransferItem{}, + &MsgMhfGetSeibattle{}, &MsgMhfGetRyoudama{}, + &MsgMhfGetTenrouirai{}, &MsgMhfPostTenrouirai{}, + &MsgMhfGetBbsUserStatus{}, &MsgMhfGetBbsSnsStatus{}, + &MsgMhfInfoScenarioCounter{}, &MsgMhfLoadScenarioData{}, + &MsgMhfSaveScenarioData{}, + &MsgMhfAcquireExchangeShop{}, + &MsgMhfLoadRengokuData{}, &MsgMhfGetRengokuBinary{}, + &MsgMhfLoadMezfesData{}, &MsgMhfLoadPlateMyset{}, + } + + for _, pkt := range packets { + t.Run(pkt.Opcode().String(), func(t *testing.T) { + err := pkt.Build(bf, ctx) + if err == nil { + // Some packets may have Build implemented - that's fine + t.Logf("Build() succeeded (has implementation)") + } + }) + } +} + +// TestBatchParseReserve188and18B tests reserve packets with AckHandle. +func TestBatchParseReserve188and18B(t *testing.T) { + ctx := &clientctx.ClientContext{} + + for _, tc := range []struct { + name string + pkt MHFPacket + }{ + {"MsgSysReserve188", &MsgSysReserve188{}}, + {"MsgSysReserve18B", &MsgSysReserve18B{}}, + } { + t.Run(tc.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) + bf.Seek(0, io.SeekStart) + if err := tc.pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + } +} + +// TestBatchParseStageStringPackets tests packets that read a stage ID string. +func TestBatchParseStageStringPackets(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("MsgSysGetStageBinary", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // BinaryType0 + bf.WriteUint8(3) // BinaryType1 + bf.WriteUint32(0) // Unk0 + bf.WriteUint8(6) // stageIDLength + bf.WriteBytes(append([]byte("room1"), 0x00)) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysGetStageBinary{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.StageID != "room1" { + t.Errorf("StageID = %q, want room1", pkt.StageID) + } + }) + + t.Run("MsgSysWaitStageBinary", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // BinaryType0 + bf.WriteUint8(3) // BinaryType1 + bf.WriteUint32(0) // Unk0 + bf.WriteUint8(6) // stageIDLength + bf.WriteBytes(append([]byte("room2"), 0x00)) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysWaitStageBinary{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.StageID != "room2" { + t.Errorf("StageID = %q, want room2", pkt.StageID) + } + }) + + t.Run("MsgSysSetStageBinary", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(1) // BinaryType0 + bf.WriteUint8(2) // BinaryType1 + bf.WriteUint8(6) // stageIDLength + bf.WriteUint16(3) // dataSize + bf.WriteBytes(append([]byte("room3"), 0x00)) + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC}) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysSetStageBinary{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.StageID != "room3" || len(pkt.RawDataPayload) != 3 { + t.Error("field mismatch") + } + }) + + t.Run("MsgSysEnumerateClient", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // Unk0 + bf.WriteUint8(3) // Get + bf.WriteUint8(6) // stageIDLength + bf.WriteBytes(append([]byte("room4"), 0x00)) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysEnumerateClient{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.StageID != "room4" { + t.Errorf("StageID = %q, want room4", pkt.StageID) + } + }) + + t.Run("MsgSysSetStagePass", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(1) // Unk0 + bf.WriteUint8(5) // Password length + bf.WriteBytes(append([]byte("pass"), 0x00)) + bf.Seek(0, io.SeekStart) + pkt := &MsgSysSetStagePass{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Password != "pass" { + t.Errorf("Password = %q, want pass", pkt.Password) + } + }) +} + +// TestBatchParseStampcardStamp tests the stampcard packet with downcasts. +func TestBatchParseStampcardStamp(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(2) // HR + bf.WriteUint16(3) // GR + bf.WriteUint16(4) // Stamps + bf.WriteUint16(0) // discard + bf.WriteUint32(5) // Reward1 (downcast to uint16) + bf.WriteUint32(6) // Reward2 + bf.WriteUint32(7) // Item1 + bf.WriteUint32(8) // Item2 + bf.WriteUint32(9) // Quantity1 + bf.WriteUint32(10) // Quantity2 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfStampcardStamp{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if pkt.HR != 2 || pkt.GR != 3 || pkt.Stamps != 4 || pkt.Reward1 != 5 { + t.Error("field mismatch") + } +} + +// TestBatchParseAnnounce tests the announce packet with fixed-size byte array. +func TestBatchParseAnnounce(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(0x7F000001) // IPAddress (127.0.0.1) + bf.WriteUint16(54001) // Port + bf.WriteUint8(0) // discard + bf.WriteUint16(0) // discard + bf.WriteBytes(make([]byte, 32)) // StageID + bf.WriteUint32(0) // Data length (0 bytes) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAnnounce{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } + if pkt.IPAddress != 0x7F000001 || pkt.Port != 54001 { + t.Error("field mismatch") + } +} + +// TestBatchParseOprtMail tests conditional parsing. +func TestBatchParseOprtMail(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("delete", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // AccIndex + bf.WriteUint8(1) // Index + bf.WriteUint8(0x01) // Operation = DELETE + bf.WriteUint8(0) // Unk0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfOprtMail{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("acquire_item", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // AccIndex + bf.WriteUint8(1) // Index + bf.WriteUint8(0x05) // Operation = ACQUIRE_ITEM + bf.WriteUint8(0) // Unk0 + bf.WriteUint16(5) // Amount + bf.WriteUint16(100) // ItemID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfOprtMail{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + if pkt.Amount != 5 || pkt.ItemID != 100 { + t.Error("field mismatch") + } + }) +} + +// TestBatchParsePostTowerInfo tests the 11-field packet. +func TestBatchParsePostTowerInfo(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + for i := 0; i < 11; i++ { + bf.WriteUint32(uint32(i + 10)) + } + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfPostTowerInfo{} + if err := pkt.Parse(bf, &clientctx.ClientContext{}); err != nil { + t.Fatal(err) + } +} + +// TestBatchParseGuildHuntdata tests conditional guild huntdata. +// TestBatchParseAdditionalMultiField tests Parse for more packets with multiple fields. +func TestBatchParseAdditionalMultiField(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("MsgMhfAcquireFesta", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // FestaID + bf.WriteUint32(200) // GuildID + bf.WriteUint16(0) // Unk + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireFesta{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAddUdTacticsPoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(10) // Unk0 + bf.WriteUint32(500) // Unk1 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAddUdTacticsPoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfApplyCampaign", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(1) // Unk0 + bf.WriteUint16(2) // Unk1 + bf.WriteBytes(make([]byte, 16)) // Unk2 (16 bytes) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfApplyCampaign{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfCheckMonthlyItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // Type + bf.WriteBytes(make([]byte, 3)) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfCheckMonthlyItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfCheckWeeklyStamp_hl", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // StampType = 1 ("hl") + bf.WriteUint8(0) // Unk1 (bool) + bf.WriteUint16(10) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfCheckWeeklyStamp{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfCheckWeeklyStamp_ex", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // StampType = 2 ("ex") + bf.WriteUint8(1) // Unk1 (bool) + bf.WriteUint16(20) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfCheckWeeklyStamp{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEntryFesta", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // FestaID + bf.WriteUint32(200) // GuildID + bf.WriteUint16(0) // padding + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEntryFesta{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateFestaMember", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // FestaID + bf.WriteUint32(200) // GuildID + bf.WriteUint16(0) // padding + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateFestaMember{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateInvGuild", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteBytes(make([]byte, 9)) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateInvGuild{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateWarehouse_item", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // boxType = 0 ("item") + bf.WriteUint8(1) // BoxIndex + bf.WriteUint16(0) // padding + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateWarehouse{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateWarehouse_equip", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // boxType = 1 ("equip") + bf.WriteUint8(2) // BoxIndex + bf.WriteUint16(0) // padding + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateWarehouse{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfExchangeFpoint2Item", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // TradeID + bf.WriteUint16(1) // ItemType + bf.WriteUint16(50) // ItemId + bf.WriteUint8(5) // Quantity + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfExchangeFpoint2Item{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfExchangeItem2Fpoint", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // TradeID + bf.WriteUint16(1) // ItemType + bf.WriteUint16(50) // ItemId + bf.WriteUint8(5) // Quantity + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfExchangeItem2Fpoint{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfExchangeWeeklyStamp_hl", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // StampType = 1 ("hl") + bf.WriteUint8(0) // Unk1 + bf.WriteUint16(0) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfExchangeWeeklyStamp{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfExchangeWeeklyStamp_ex", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(2) // StampType = 2 ("ex") + bf.WriteUint8(1) // Unk1 + bf.WriteUint16(5) // Unk2 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfExchangeWeeklyStamp{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGenerateUdGuildMap", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGenerateUdGuildMap{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetBoostTimeLimit", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetBoostTimeLimit{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetCafeDurationBonusInfo", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetCafeDurationBonusInfo{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfGetMyhouseInfo", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // Unk0 + bf.WriteUint8(4) // DataSize + bf.WriteBytes([]byte{0x01, 0x02, 0x03, 0x04}) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGetMyhouseInfo{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfAcquireUdItem", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // Unk0 + bf.WriteUint8(2) // RewardType + bf.WriteUint8(2) // Unk2 (count) + bf.WriteUint32(10) + bf.WriteUint32(20) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfAcquireUdItem{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("MsgMhfEnumerateHouse_noname", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // CharID + bf.WriteUint8(1) // Method + bf.WriteUint16(0) // Unk + bf.WriteUint8(0) // lenName = 0 (no name) + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfEnumerateHouse{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) +} + +func TestBatchParseGuildHuntdata(t *testing.T) { + ctx := &clientctx.ClientContext{} + + t.Run("operation_0", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(0) // Operation = 0 + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGuildHuntdata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) + + t.Run("operation_1", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint8(1) // Operation = 1 (reads GuildID) + bf.WriteUint32(99) // GuildID + bf.Seek(0, io.SeekStart) + pkt := &MsgMhfGuildHuntdata{} + if err := pkt.Parse(bf, ctx); err != nil { + t.Fatal(err) + } + }) +} diff --git a/network/mhfpacket/msg_build_test.go b/network/mhfpacket/msg_build_test.go new file mode 100644 index 000000000..a939373fa --- /dev/null +++ b/network/mhfpacket/msg_build_test.go @@ -0,0 +1,1413 @@ +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 manual-build/Parse round-trip for MsgSysLoadRegister. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +func TestBuildParseLoadRegister(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + registerID uint32 + values uint8 + }{ + {"typical", 0x11223344, 100, 1}, + {"zero values", 0, 0, 0}, + {"max values", 0xFFFFFFFF, 0xFFFFFFFF, 255}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.registerID) + bf.WriteUint8(tt.values) + bf.WriteUint8(0) // Zeroed + bf.WriteUint16(0) // Zeroed + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysLoadRegister{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.RegisterID != tt.registerID { + t.Errorf("RegisterID = %d, want %d", parsed.RegisterID, tt.registerID) + } + if parsed.Values != tt.values { + t.Errorf("Values = %d, want %d", parsed.Values, tt.values) + } + }) + } +} + +// TestBuildParseOperateRegister verifies manual-build/Parse round-trip for MsgSysOperateRegister. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +func TestBuildParseOperateRegister(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + semaphoreID uint32 + payload []byte + }{ + {"typical", 1, 42, []byte{0x01, 0x02, 0x03}}, + {"empty payload", 0, 0, []byte{}}, + {"large payload", 0xFFFFFFFF, 0xDEADBEEF, make([]byte, 256)}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.semaphoreID) + bf.WriteUint16(0) // Zeroed + bf.WriteUint16(uint16(len(tt.payload))) + bf.WriteBytes(tt.payload) + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysOperateRegister{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.SemaphoreID != tt.semaphoreID { + t.Errorf("SemaphoreID = %d, want %d", parsed.SemaphoreID, tt.semaphoreID) + } + if !bytes.Equal(parsed.RawDataPayload, tt.payload) { + t.Errorf("RawDataPayload length = %d, want %d", len(parsed.RawDataPayload), len(tt.payload)) + } + }) + } +} + +// TestBuildParseNotifyUserBinary verifies Build/Parse round-trip for MsgSysNotifyUserBinary. +func TestBuildParseNotifyUserBinary(t *testing.T) { + tests := []struct { + name string + charID uint32 + binaryType uint8 + }{ + {"typical", 12345, 1}, + {"zero", 0, 0}, + {"max", 0xFFFFFFFF, 255}, + } + + ctx := &clientctx.ClientContext{} + 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 manual-build/Parse round-trip for MsgMhfArrangeGuildMember. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +// Parse reads: uint32 AckHandle, uint32 GuildID, uint8 zeroed, uint8 charCount, then charCount * uint32. +func TestBuildParseArrangeGuildMember(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + guildID uint32 + charIDs []uint32 + }{ + {"single member", 1, 100, []uint32{12345}}, + {"multiple members", 0x12345678, 200, []uint32{111, 222, 333, 444}}, + {"no members", 42, 300, []uint32{}}, + {"many members", 999, 400, []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.guildID) + bf.WriteUint8(0) // Zeroed + bf.WriteUint8(uint8(len(tt.charIDs))) + for _, id := range tt.charIDs { + bf.WriteUint32(id) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfArrangeGuildMember{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.GuildID != tt.guildID { + t.Errorf("GuildID = %d, want %d", parsed.GuildID, tt.guildID) + } + if len(parsed.CharIDs) != len(tt.charIDs) { + t.Fatalf("CharIDs length = %d, want %d", len(parsed.CharIDs), len(tt.charIDs)) + } + for i, id := range parsed.CharIDs { + if id != tt.charIDs[i] { + t.Errorf("CharIDs[%d] = %d, want %d", i, id, tt.charIDs[i]) + } + } + }) + } +} + +// TestBuildParseEnumerateGuildMember verifies manual-build/Parse round-trip for MsgMhfEnumerateGuildMember. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +// Parse reads: uint32 AckHandle, uint8 zeroed, uint8 always1, uint32 AllianceID, uint32 GuildID. +func TestBuildParseEnumerateGuildMember(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + allianceID uint32 + guildID uint32 + }{ + {"typical", 1, 0, 100}, + {"zero", 0, 0, 0}, + {"large values", 0xFFFFFFFF, 0xDEADBEEF, 0xCAFEBABE}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint8(0) // Zeroed + bf.WriteUint8(1) // Always 1 + bf.WriteUint32(tt.allianceID) + bf.WriteUint32(tt.guildID) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfEnumerateGuildMember{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.AllianceID != tt.allianceID { + t.Errorf("AllianceID = %d, want %d", parsed.AllianceID, tt.allianceID) + } + if parsed.GuildID != tt.guildID { + t.Errorf("GuildID = %d, want %d", parsed.GuildID, tt.guildID) + } + }) + } +} + +// TestBuildParseStateCampaign verifies manual-build/Parse round-trip for MsgMhfStateCampaign. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +func TestBuildParseStateCampaign(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + campaignID uint32 + unk1 uint16 + }{ + {"typical", 1, 10, 300}, + {"zero", 0, 0, 0}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.campaignID) + bf.WriteUint16(tt.unk1) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfStateCampaign{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.CampaignID != tt.campaignID { + t.Errorf("CampaignID = %d, want %d", parsed.CampaignID, tt.campaignID) + } + if parsed.Unk1 != tt.unk1 { + t.Errorf("Unk1 = %d, want %d", parsed.Unk1, tt.unk1) + } + }) + } +} + +// TestBuildParseApplyCampaign verifies manual-build/Parse round-trip for MsgMhfApplyCampaign. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +func TestBuildParseApplyCampaign(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + unk0 uint32 + unk1 uint16 + unk2 []byte + }{ + {"typical", 0x55667788, 5, 10, make([]byte, 16)}, + {"zero", 0, 0, 0, make([]byte, 16)}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFF, make([]byte, 16)}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.unk0) + bf.WriteUint16(tt.unk1) + bf.WriteBytes(tt.unk2) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfApplyCampaign{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", parsed.Unk0, tt.unk0) + } + if parsed.Unk1 != tt.unk1 { + t.Errorf("Unk1 = %d, want %d", parsed.Unk1, tt.unk1) + } + if len(parsed.Unk2) != len(tt.unk2) { + t.Errorf("Unk2 len = %d, want %d", len(parsed.Unk2), len(tt.unk2)) + } + }) + } +} + +// TestBuildParseEnumerateCampaign verifies Build/Parse round-trip for MsgMhfEnumerateCampaign. +func TestBuildParseEnumerateCampaign(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + unk0 uint16 + unk1 uint16 + }{ + {"typical", 42, 1, 2}, + {"zero", 0, 0, 0}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgMhfEnumerateCampaign{ + AckHandle: tt.ackHandle, + Unk0: tt.unk0, + Unk1: tt.unk1, + } + + bf := byteframe.NewByteFrame() + if err := original.Build(bf, ctx); err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfEnumerateCampaign{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle) + } + if parsed.Unk0 != original.Unk0 { + t.Errorf("Unk0 = %d, want %d", parsed.Unk0, original.Unk0) + } + if parsed.Unk1 != original.Unk1 { + t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1) + } + }) + } +} + +// TestBuildParseEnumerateEvent verifies Build/Parse round-trip for MsgMhfEnumerateEvent. +func TestBuildParseEnumerateEvent(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + }{ + {"typical", 0x11223344}, + {"nonzero", 42}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgMhfEnumerateEvent{ + AckHandle: tt.ackHandle, + } + + bf := byteframe.NewByteFrame() + // Build is NOT IMPLEMENTED; manually write the binary representation + bf.WriteUint32(original.AckHandle) + bf.WriteUint16(0) // Zeroed (discarded by Parse) + bf.WriteUint16(0) // Zeroed (discarded by Parse) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfEnumerateEvent{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle) + } + }) + } +} + +// TestBuildParseAddUdTacticsPoint verifies Build/Parse round-trip for MsgMhfAddUdTacticsPoint. +func TestBuildParseAddUdTacticsPoint(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + 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 manual-build/Parse round-trip for MsgMhfApplyDistItem. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +// Note: Unk2 and Unk3 are conditionally parsed based on RealClientMode (G8+ and G10+). +// Default test config is ZZ, so both Unk2 and Unk3 are read. +func TestBuildParseApplyDistItem(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + distributionType uint8 + distributionID uint32 + unk2 uint32 + unk3 uint32 + }{ + {"typical", 0x12345678, 1, 42, 100, 200}, + {"zero", 0, 0, 0, 0, 0}, + {"max", 0xFFFFFFFF, 255, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint8(tt.distributionType) + bf.WriteUint32(tt.distributionID) + bf.WriteUint32(tt.unk2) // Read when RealClientMode >= G8 + bf.WriteUint32(tt.unk3) // Read when RealClientMode >= G10 + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfApplyDistItem{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.DistributionType != tt.distributionType { + t.Errorf("DistributionType = %d, want %d", parsed.DistributionType, tt.distributionType) + } + if parsed.DistributionID != tt.distributionID { + t.Errorf("DistributionID = %d, want %d", parsed.DistributionID, tt.distributionID) + } + if parsed.Unk2 != tt.unk2 { + t.Errorf("Unk2 = %d, want %d", parsed.Unk2, tt.unk2) + } + if parsed.Unk3 != tt.unk3 { + t.Errorf("Unk3 = %d, want %d", parsed.Unk3, tt.unk3) + } + }) + } +} + +// TestBuildParseEnumerateDistItem verifies Build/Parse round-trip for MsgMhfEnumerateDistItem. +func TestBuildParseEnumerateDistItem(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + distType uint8 + unk1 uint8 + unk2 uint16 + }{ + {"typical", 0xAABBCCDD, 5, 100, 200}, + {"zero", 0, 0, 0, 0}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgMhfEnumerateDistItem{ + AckHandle: tt.ackHandle, + DistType: tt.distType, + Unk1: tt.unk1, + Unk2: tt.unk2, + } + + bf := byteframe.NewByteFrame() + // Build is NOT IMPLEMENTED; manually write the binary representation + bf.WriteUint32(original.AckHandle) + bf.WriteUint8(original.DistType) + bf.WriteUint8(original.Unk1) + bf.WriteUint16(original.Unk2) + bf.WriteUint8(0) // Unk3 length (for Z1+ client mode) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfEnumerateDistItem{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle) + } + if parsed.DistType != original.DistType { + t.Errorf("DistType = %d, want %d", parsed.DistType, original.DistType) + } + if parsed.Unk1 != original.Unk1 { + t.Errorf("Unk1 = %d, want %d", parsed.Unk1, original.Unk1) + } + if parsed.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 Parse for MsgMhfDisplayedAchievement. +// This struct has no exported fields; Parse only discards a single zeroed byte. +func TestBuildParseDisplayedAchievement(t *testing.T) { + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + bf.WriteUint8(0) // Zeroed (discarded by Parse) + bf.Seek(0, io.SeekStart) + + parsed := &MsgMhfDisplayedAchievement{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +// TestBuildParseAddKouryouPoint verifies Build/Parse round-trip for MsgMhfAddKouryouPoint. +func TestBuildParseAddKouryouPoint(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + kouryouPoints uint32 + }{ + {"typical", 1, 5000}, + {"zero", 0, 0}, + {"max", 0xFFFFFFFF, 0xFFFFFFFF}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgMhfAddKouryouPoint{ + AckHandle: tt.ackHandle, + KouryouPoints: tt.kouryouPoints, + } + + bf := byteframe.NewByteFrame() + if err := original.Build(bf, ctx); err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfAddKouryouPoint{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, original.AckHandle) + } + if parsed.KouryouPoints != original.KouryouPoints { + t.Errorf("KouryouPoints = %d, want %d", parsed.KouryouPoints, original.KouryouPoints) + } + }) + } +} + +// TestBuildParseCheckDailyCafepoint verifies manual-build/Parse round-trip for MsgMhfCheckDailyCafepoint. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +func TestBuildParseCheckDailyCafepoint(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + unk uint32 + }{ + {"typical", 0x11223344, 100}, + {"zero", 0, 0}, + } + + ctx := &clientctx.ClientContext{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.unk) + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfCheckDailyCafepoint{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if parsed.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = 0x%X, want 0x%X", parsed.AckHandle, tt.ackHandle) + } + if parsed.Unk != tt.unk { + t.Errorf("Unk = %d, want %d", parsed.Unk, tt.unk) + } + }) + } +} + +// TestBuildParsePing verifies Build/Parse round-trip for MsgSysPing. +func TestBuildParsePing(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + }{ + {"typical", 0x12345678}, + {"zero", 0}, + {"max", 0xFFFFFFFF}, + } + + ctx := &clientctx.ClientContext{} + 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 Parse for MsgSysUnlockStage. +// This struct has no exported fields; Parse only discards a single zeroed uint16. +func TestBuildParseUnlockStage(t *testing.T) { + ctx := &clientctx.ClientContext{} + bf := byteframe.NewByteFrame() + bf.WriteUint16(0) // Zeroed (discarded by Parse) + bf.Seek(0, io.SeekStart) + + parsed := &MsgSysUnlockStage{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +// TestBuildParseUnlockGlobalSema verifies Build/Parse round-trip for MsgSysUnlockGlobalSema. +func TestBuildParseUnlockGlobalSema(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + }{ + {"typical", 0xAABBCCDD}, + {"zero", 0}, + } + + ctx := &clientctx.ClientContext{} + 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 +// manual-build/Parse for MsgSysOperateRegister. +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +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 + } + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint32(42) // SemaphoreID + bf.WriteUint16(0) // Zeroed + bf.WriteUint16(uint16(len(payload))) // dataSize + bf.WriteBytes(payload) + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysOperateRegister{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if !bytes.Equal(parsed.RawDataPayload, payload) { + t.Errorf("Payload mismatch: got %d bytes, want %d bytes", len(parsed.RawDataPayload), len(payload)) + } +} + +// TestBuildParseArrangeGuildMemberEmptySlice ensures that an empty CharIDs slice +// round-trips correctly (the uint8 count field should be 0). +// Build is NOT IMPLEMENTED, so we manually write the binary representation. +// Parse reads: uint32 AckHandle, uint32 GuildID, uint8 zeroed, uint8 charCount. +func TestBuildParseArrangeGuildMemberEmptySlice(t *testing.T) { + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint32(100) // GuildID + bf.WriteUint8(0) // Zeroed + bf.WriteUint8(0) // charCount = 0 + + // Verify the wire size: uint32 + uint32 + uint8 + uint8 = 10 bytes + if len(bf.Data()) != 10 { + t.Errorf("wrote %d bytes, want 10 for empty CharIDs", len(bf.Data())) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgMhfArrangeGuildMember{} + if err := parsed.Parse(bf, ctx); err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if len(parsed.CharIDs) != 0 { + t.Errorf("CharIDs length = %d, want 0", len(parsed.CharIDs)) + } +} + +// TestBuildBinaryFormat verifies the exact binary output format of a Build call +// for MsgSysDuplicateObject to ensure correct endianness and field ordering. +func TestBuildBinaryFormat(t *testing.T) { + ctx := &clientctx.ClientContext{} + 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_comprehensive_test.go b/network/mhfpacket/msg_comprehensive_test.go new file mode 100644 index 000000000..b16b8dc20 --- /dev/null +++ b/network/mhfpacket/msg_comprehensive_test.go @@ -0,0 +1,1150 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +// TestAllOpcodesFromOpcode verifies that FromOpcode returns non-nil packets for all known opcodes +func TestAllOpcodesFromOpcode(t *testing.T) { + // All opcodes from opcode_to_packet.go + opcodes := []network.PacketID{ + network.MSG_HEAD, + network.MSG_SYS_reserve01, + network.MSG_SYS_reserve02, + network.MSG_SYS_reserve03, + network.MSG_SYS_reserve04, + network.MSG_SYS_reserve05, + network.MSG_SYS_reserve06, + network.MSG_SYS_reserve07, + network.MSG_SYS_ADD_OBJECT, + network.MSG_SYS_DEL_OBJECT, + network.MSG_SYS_DISP_OBJECT, + network.MSG_SYS_HIDE_OBJECT, + network.MSG_SYS_reserve0C, + network.MSG_SYS_reserve0D, + network.MSG_SYS_reserve0E, + network.MSG_SYS_EXTEND_THRESHOLD, + network.MSG_SYS_END, + network.MSG_SYS_NOP, + network.MSG_SYS_ACK, + network.MSG_SYS_TERMINAL_LOG, + network.MSG_SYS_LOGIN, + network.MSG_SYS_LOGOUT, + network.MSG_SYS_SET_STATUS, + network.MSG_SYS_PING, + network.MSG_SYS_CAST_BINARY, + network.MSG_SYS_HIDE_CLIENT, + network.MSG_SYS_TIME, + network.MSG_SYS_CASTED_BINARY, + network.MSG_SYS_GET_FILE, + network.MSG_SYS_ISSUE_LOGKEY, + network.MSG_SYS_RECORD_LOG, + network.MSG_SYS_ECHO, + network.MSG_SYS_CREATE_STAGE, + network.MSG_SYS_STAGE_DESTRUCT, + network.MSG_SYS_ENTER_STAGE, + network.MSG_SYS_BACK_STAGE, + network.MSG_SYS_MOVE_STAGE, + network.MSG_SYS_LEAVE_STAGE, + network.MSG_SYS_LOCK_STAGE, + network.MSG_SYS_UNLOCK_STAGE, + network.MSG_SYS_RESERVE_STAGE, + network.MSG_SYS_UNRESERVE_STAGE, + network.MSG_SYS_SET_STAGE_PASS, + network.MSG_SYS_WAIT_STAGE_BINARY, + network.MSG_SYS_SET_STAGE_BINARY, + network.MSG_SYS_GET_STAGE_BINARY, + network.MSG_SYS_ENUMERATE_CLIENT, + network.MSG_SYS_ENUMERATE_STAGE, + network.MSG_SYS_CREATE_MUTEX, + network.MSG_SYS_CREATE_OPEN_MUTEX, + network.MSG_SYS_DELETE_MUTEX, + network.MSG_SYS_OPEN_MUTEX, + network.MSG_SYS_CLOSE_MUTEX, + network.MSG_SYS_CREATE_SEMAPHORE, + network.MSG_SYS_CREATE_ACQUIRE_SEMAPHORE, + network.MSG_SYS_DELETE_SEMAPHORE, + network.MSG_SYS_ACQUIRE_SEMAPHORE, + network.MSG_SYS_RELEASE_SEMAPHORE, + network.MSG_SYS_LOCK_GLOBAL_SEMA, + network.MSG_SYS_UNLOCK_GLOBAL_SEMA, + network.MSG_SYS_CHECK_SEMAPHORE, + network.MSG_SYS_OPERATE_REGISTER, + network.MSG_SYS_LOAD_REGISTER, + network.MSG_SYS_NOTIFY_REGISTER, + network.MSG_SYS_CREATE_OBJECT, + network.MSG_SYS_DELETE_OBJECT, + network.MSG_SYS_POSITION_OBJECT, + network.MSG_SYS_ROTATE_OBJECT, + network.MSG_SYS_DUPLICATE_OBJECT, + network.MSG_SYS_SET_OBJECT_BINARY, + network.MSG_SYS_GET_OBJECT_BINARY, + network.MSG_SYS_GET_OBJECT_OWNER, + network.MSG_SYS_UPDATE_OBJECT_BINARY, + network.MSG_SYS_CLEANUP_OBJECT, + network.MSG_SYS_reserve4A, + network.MSG_SYS_reserve4B, + network.MSG_SYS_reserve4C, + network.MSG_SYS_reserve4D, + network.MSG_SYS_reserve4E, + network.MSG_SYS_reserve4F, + network.MSG_SYS_INSERT_USER, + network.MSG_SYS_DELETE_USER, + network.MSG_SYS_SET_USER_BINARY, + network.MSG_SYS_GET_USER_BINARY, + network.MSG_SYS_NOTIFY_USER_BINARY, + network.MSG_SYS_reserve55, + network.MSG_SYS_reserve56, + network.MSG_SYS_reserve57, + network.MSG_SYS_UPDATE_RIGHT, + network.MSG_SYS_AUTH_QUERY, + network.MSG_SYS_AUTH_DATA, + network.MSG_SYS_AUTH_TERMINAL, + network.MSG_SYS_reserve5C, + network.MSG_SYS_RIGHTS_RELOAD, + network.MSG_SYS_reserve5E, + network.MSG_SYS_reserve5F, + network.MSG_MHF_SAVEDATA, + network.MSG_MHF_LOADDATA, + network.MSG_MHF_LIST_MEMBER, + network.MSG_MHF_OPR_MEMBER, + network.MSG_MHF_ENUMERATE_DIST_ITEM, + network.MSG_MHF_APPLY_DIST_ITEM, + network.MSG_MHF_ACQUIRE_DIST_ITEM, + network.MSG_MHF_GET_DIST_DESCRIPTION, + network.MSG_MHF_SEND_MAIL, + network.MSG_MHF_READ_MAIL, + network.MSG_MHF_LIST_MAIL, + network.MSG_MHF_OPRT_MAIL, + network.MSG_MHF_LOAD_FAVORITE_QUEST, + network.MSG_MHF_SAVE_FAVORITE_QUEST, + network.MSG_MHF_REGISTER_EVENT, + network.MSG_MHF_RELEASE_EVENT, + network.MSG_MHF_TRANSIT_MESSAGE, + network.MSG_SYS_reserve71, + network.MSG_SYS_reserve72, + network.MSG_SYS_reserve73, + network.MSG_SYS_reserve74, + network.MSG_SYS_reserve75, + network.MSG_SYS_reserve76, + network.MSG_SYS_reserve77, + network.MSG_SYS_reserve78, + network.MSG_SYS_reserve79, + network.MSG_SYS_reserve7A, + network.MSG_SYS_reserve7B, + network.MSG_SYS_reserve7C, + network.MSG_CA_EXCHANGE_ITEM, + network.MSG_SYS_reserve7E, + network.MSG_MHF_PRESENT_BOX, + network.MSG_MHF_SERVER_COMMAND, + network.MSG_MHF_SHUT_CLIENT, + network.MSG_MHF_ANNOUNCE, + network.MSG_MHF_SET_LOGINWINDOW, + network.MSG_SYS_TRANS_BINARY, + network.MSG_SYS_COLLECT_BINARY, + network.MSG_SYS_GET_STATE, + network.MSG_SYS_SERIALIZE, + network.MSG_SYS_ENUMLOBBY, + network.MSG_SYS_ENUMUSER, + network.MSG_SYS_INFOKYSERVER, + network.MSG_MHF_GET_CA_UNIQUE_ID, + network.MSG_MHF_SET_CA_ACHIEVEMENT, + network.MSG_MHF_CARAVAN_MY_SCORE, + network.MSG_MHF_CARAVAN_RANKING, + network.MSG_MHF_CARAVAN_MY_RANK, + network.MSG_MHF_CREATE_GUILD, + network.MSG_MHF_OPERATE_GUILD, + network.MSG_MHF_OPERATE_GUILD_MEMBER, + network.MSG_MHF_INFO_GUILD, + network.MSG_MHF_ENUMERATE_GUILD, + network.MSG_MHF_UPDATE_GUILD, + network.MSG_MHF_ARRANGE_GUILD_MEMBER, + network.MSG_MHF_ENUMERATE_GUILD_MEMBER, + network.MSG_MHF_ENUMERATE_CAMPAIGN, + network.MSG_MHF_STATE_CAMPAIGN, + network.MSG_MHF_APPLY_CAMPAIGN, + network.MSG_MHF_ENUMERATE_ITEM, + network.MSG_MHF_ACQUIRE_ITEM, + network.MSG_MHF_TRANSFER_ITEM, + network.MSG_MHF_MERCENARY_HUNTDATA, + network.MSG_MHF_ENTRY_ROOKIE_GUILD, + network.MSG_MHF_ENUMERATE_QUEST, + network.MSG_MHF_ENUMERATE_EVENT, + network.MSG_MHF_ENUMERATE_PRICE, + network.MSG_MHF_ENUMERATE_RANKING, + network.MSG_MHF_ENUMERATE_ORDER, + network.MSG_MHF_ENUMERATE_SHOP, + network.MSG_MHF_GET_EXTRA_INFO, + network.MSG_MHF_UPDATE_INTERIOR, + network.MSG_MHF_ENUMERATE_HOUSE, + network.MSG_MHF_UPDATE_HOUSE, + network.MSG_MHF_LOAD_HOUSE, + network.MSG_MHF_OPERATE_WAREHOUSE, + network.MSG_MHF_ENUMERATE_WAREHOUSE, + network.MSG_MHF_UPDATE_WAREHOUSE, + network.MSG_MHF_ACQUIRE_TITLE, + network.MSG_MHF_ENUMERATE_TITLE, + network.MSG_MHF_ENUMERATE_GUILD_ITEM, + network.MSG_MHF_UPDATE_GUILD_ITEM, + network.MSG_MHF_ENUMERATE_UNION_ITEM, + network.MSG_MHF_UPDATE_UNION_ITEM, + network.MSG_MHF_CREATE_JOINT, + network.MSG_MHF_OPERATE_JOINT, + network.MSG_MHF_INFO_JOINT, + network.MSG_MHF_UPDATE_GUILD_ICON, + network.MSG_MHF_INFO_FESTA, + network.MSG_MHF_ENTRY_FESTA, + network.MSG_MHF_CHARGE_FESTA, + network.MSG_MHF_ACQUIRE_FESTA, + network.MSG_MHF_STATE_FESTA_U, + network.MSG_MHF_STATE_FESTA_G, + network.MSG_MHF_ENUMERATE_FESTA_MEMBER, + network.MSG_MHF_VOTE_FESTA, + network.MSG_MHF_ACQUIRE_CAFE_ITEM, + network.MSG_MHF_UPDATE_CAFEPOINT, + network.MSG_MHF_CHECK_DAILY_CAFEPOINT, + network.MSG_MHF_GET_COG_INFO, + network.MSG_MHF_CHECK_MONTHLY_ITEM, + network.MSG_MHF_ACQUIRE_MONTHLY_ITEM, + network.MSG_MHF_CHECK_WEEKLY_STAMP, + network.MSG_MHF_EXCHANGE_WEEKLY_STAMP, + network.MSG_MHF_CREATE_MERCENARY, + network.MSG_MHF_SAVE_MERCENARY, + network.MSG_MHF_READ_MERCENARY_W, + network.MSG_MHF_READ_MERCENARY_M, + network.MSG_MHF_CONTRACT_MERCENARY, + network.MSG_MHF_ENUMERATE_MERCENARY_LOG, + network.MSG_MHF_ENUMERATE_GUACOT, + network.MSG_MHF_UPDATE_GUACOT, + network.MSG_MHF_INFO_TOURNAMENT, + network.MSG_MHF_ENTRY_TOURNAMENT, + network.MSG_MHF_ENTER_TOURNAMENT_QUEST, + network.MSG_MHF_ACQUIRE_TOURNAMENT, + network.MSG_MHF_GET_ACHIEVEMENT, + network.MSG_MHF_RESET_ACHIEVEMENT, + network.MSG_MHF_ADD_ACHIEVEMENT, + network.MSG_MHF_PAYMENT_ACHIEVEMENT, + network.MSG_MHF_DISPLAYED_ACHIEVEMENT, + network.MSG_MHF_INFO_SCENARIO_COUNTER, + network.MSG_MHF_SAVE_SCENARIO_DATA, + network.MSG_MHF_LOAD_SCENARIO_DATA, + network.MSG_MHF_GET_BBS_SNS_STATUS, + network.MSG_MHF_APPLY_BBS_ARTICLE, + network.MSG_MHF_GET_ETC_POINTS, + network.MSG_MHF_UPDATE_ETC_POINT, + network.MSG_MHF_GET_MYHOUSE_INFO, + network.MSG_MHF_UPDATE_MYHOUSE_INFO, + network.MSG_MHF_GET_WEEKLY_SCHEDULE, + network.MSG_MHF_ENUMERATE_INV_GUILD, + network.MSG_MHF_OPERATION_INV_GUILD, + network.MSG_MHF_STAMPCARD_STAMP, + network.MSG_MHF_STAMPCARD_PRIZE, + network.MSG_MHF_UNRESERVE_SRG, + network.MSG_MHF_LOAD_PLATE_DATA, + network.MSG_MHF_SAVE_PLATE_DATA, + network.MSG_MHF_LOAD_PLATE_BOX, + network.MSG_MHF_SAVE_PLATE_BOX, + network.MSG_MHF_READ_GUILDCARD, + network.MSG_MHF_UPDATE_GUILDCARD, + network.MSG_MHF_READ_BEAT_LEVEL, + network.MSG_MHF_UPDATE_BEAT_LEVEL, + network.MSG_MHF_READ_BEAT_LEVEL_ALL_RANKING, + network.MSG_MHF_READ_BEAT_LEVEL_MY_RANKING, + network.MSG_MHF_READ_LAST_WEEK_BEAT_RANKING, + network.MSG_MHF_ACCEPT_READ_REWARD, + network.MSG_MHF_GET_ADDITIONAL_BEAT_REWARD, + network.MSG_MHF_GET_FIXED_SEIBATU_RANKING_TABLE, + network.MSG_MHF_GET_BBS_USER_STATUS, + network.MSG_MHF_KICK_EXPORT_FORCE, + network.MSG_MHF_GET_BREAK_SEIBATU_LEVEL_REWARD, + network.MSG_MHF_GET_WEEKLY_SEIBATU_RANKING_REWARD, + network.MSG_MHF_GET_EARTH_STATUS, + network.MSG_MHF_LOAD_PARTNER, + network.MSG_MHF_SAVE_PARTNER, + network.MSG_MHF_GET_GUILD_MISSION_LIST, + network.MSG_MHF_GET_GUILD_MISSION_RECORD, + network.MSG_MHF_ADD_GUILD_MISSION_COUNT, + network.MSG_MHF_SET_GUILD_MISSION_TARGET, + network.MSG_MHF_CANCEL_GUILD_MISSION_TARGET, + network.MSG_MHF_LOAD_OTOMO_AIROU, + network.MSG_MHF_SAVE_OTOMO_AIROU, + network.MSG_MHF_ENUMERATE_GUILD_TRESURE, + network.MSG_MHF_ENUMERATE_AIROULIST, + network.MSG_MHF_REGIST_GUILD_TRESURE, + network.MSG_MHF_ACQUIRE_GUILD_TRESURE, + network.MSG_MHF_OPERATE_GUILD_TRESURE_REPORT, + network.MSG_MHF_GET_GUILD_TRESURE_SOUVENIR, + network.MSG_MHF_ACQUIRE_GUILD_TRESURE_SOUVENIR, + network.MSG_MHF_ENUMERATE_FESTA_INTERMEDIATE_PRIZE, + network.MSG_MHF_ACQUIRE_FESTA_INTERMEDIATE_PRIZE, + network.MSG_MHF_LOAD_DECO_MYSET, + network.MSG_MHF_SAVE_DECO_MYSET, + network.MSG_MHF_reserve10F, + network.MSG_MHF_LOAD_GUILD_COOKING, + network.MSG_MHF_REGIST_GUILD_COOKING, + network.MSG_MHF_LOAD_GUILD_ADVENTURE, + network.MSG_MHF_REGIST_GUILD_ADVENTURE, + network.MSG_MHF_ACQUIRE_GUILD_ADVENTURE, + network.MSG_MHF_CHARGE_GUILD_ADVENTURE, + network.MSG_MHF_LOAD_LEGEND_DISPATCH, + network.MSG_MHF_LOAD_HUNTER_NAVI, + network.MSG_MHF_SAVE_HUNTER_NAVI, + network.MSG_MHF_REGIST_SPABI_TIME, + network.MSG_MHF_GET_GUILD_WEEKLY_BONUS_MASTER, + network.MSG_MHF_GET_GUILD_WEEKLY_BONUS_ACTIVE_COUNT, + network.MSG_MHF_ADD_GUILD_WEEKLY_BONUS_EXCEPTIONAL_USER, + network.MSG_MHF_GET_TOWER_INFO, + network.MSG_MHF_POST_TOWER_INFO, + network.MSG_MHF_GET_GEM_INFO, + network.MSG_MHF_POST_GEM_INFO, + network.MSG_MHF_GET_EARTH_VALUE, + network.MSG_MHF_DEBUG_POST_VALUE, + network.MSG_MHF_GET_PAPER_DATA, + network.MSG_MHF_GET_NOTICE, + network.MSG_MHF_POST_NOTICE, + network.MSG_MHF_GET_BOOST_TIME, + network.MSG_MHF_POST_BOOST_TIME, + network.MSG_MHF_GET_BOOST_TIME_LIMIT, + network.MSG_MHF_POST_BOOST_TIME_LIMIT, + network.MSG_MHF_ENUMERATE_FESTA_PERSONAL_PRIZE, + network.MSG_MHF_ACQUIRE_FESTA_PERSONAL_PRIZE, + network.MSG_MHF_GET_RAND_FROM_TABLE, + network.MSG_MHF_GET_CAFE_DURATION, + network.MSG_MHF_GET_CAFE_DURATION_BONUS_INFO, + network.MSG_MHF_RECEIVE_CAFE_DURATION_BONUS, + network.MSG_MHF_POST_CAFE_DURATION_BONUS_RECEIVED, + network.MSG_MHF_GET_GACHA_POINT, + network.MSG_MHF_USE_GACHA_POINT, + network.MSG_MHF_EXCHANGE_FPOINT_2_ITEM, + network.MSG_MHF_EXCHANGE_ITEM_2_FPOINT, + network.MSG_MHF_GET_FPOINT_EXCHANGE_LIST, + network.MSG_MHF_PLAY_STEPUP_GACHA, + network.MSG_MHF_RECEIVE_GACHA_ITEM, + network.MSG_MHF_GET_STEPUP_STATUS, + network.MSG_MHF_PLAY_FREE_GACHA, + network.MSG_MHF_GET_TINY_BIN, + network.MSG_MHF_POST_TINY_BIN, + network.MSG_MHF_GET_SENYU_DAILY_COUNT, + network.MSG_MHF_GET_GUILD_TARGET_MEMBER_NUM, + network.MSG_MHF_GET_BOOST_RIGHT, + network.MSG_MHF_START_BOOST_TIME, + network.MSG_MHF_POST_BOOST_TIME_QUEST_RETURN, + network.MSG_MHF_GET_BOX_GACHA_INFO, + network.MSG_MHF_PLAY_BOX_GACHA, + network.MSG_MHF_RESET_BOX_GACHA_INFO, + network.MSG_MHF_GET_SEIBATTLE, + network.MSG_MHF_POST_SEIBATTLE, + network.MSG_MHF_GET_RYOUDAMA, + network.MSG_MHF_POST_RYOUDAMA, + network.MSG_MHF_GET_TENROUIRAI, + network.MSG_MHF_POST_TENROUIRAI, + network.MSG_MHF_POST_GUILD_SCOUT, + network.MSG_MHF_CANCEL_GUILD_SCOUT, + network.MSG_MHF_ANSWER_GUILD_SCOUT, + network.MSG_MHF_GET_GUILD_SCOUT_LIST, + network.MSG_MHF_GET_GUILD_MANAGE_RIGHT, + network.MSG_MHF_SET_GUILD_MANAGE_RIGHT, + network.MSG_MHF_PLAY_NORMAL_GACHA, + network.MSG_MHF_GET_DAILY_MISSION_MASTER, + network.MSG_MHF_GET_DAILY_MISSION_PERSONAL, + network.MSG_MHF_SET_DAILY_MISSION_PERSONAL, + network.MSG_MHF_GET_GACHA_PLAY_HISTORY, + network.MSG_MHF_GET_REJECT_GUILD_SCOUT, + network.MSG_MHF_SET_REJECT_GUILD_SCOUT, + network.MSG_MHF_GET_CA_ACHIEVEMENT_HIST, + network.MSG_MHF_SET_CA_ACHIEVEMENT_HIST, + network.MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS, + network.MSG_MHF_USE_KEEP_LOGIN_BOOST, + network.MSG_MHF_GET_UD_SCHEDULE, + network.MSG_MHF_GET_UD_INFO, + network.MSG_MHF_GET_KIJU_INFO, + network.MSG_MHF_SET_KIJU, + network.MSG_MHF_ADD_UD_POINT, + network.MSG_MHF_GET_UD_MY_POINT, + network.MSG_MHF_GET_UD_TOTAL_POINT_INFO, + network.MSG_MHF_GET_UD_BONUS_QUEST_INFO, + network.MSG_MHF_GET_UD_SELECTED_COLOR_INFO, + network.MSG_MHF_GET_UD_MONSTER_POINT, + network.MSG_MHF_GET_UD_DAILY_PRESENT_LIST, + network.MSG_MHF_GET_UD_NORMA_PRESENT_LIST, + network.MSG_MHF_GET_UD_RANKING_REWARD_LIST, + network.MSG_MHF_ACQUIRE_UD_ITEM, + network.MSG_MHF_GET_REWARD_SONG, + network.MSG_MHF_USE_REWARD_SONG, + network.MSG_MHF_ADD_REWARD_SONG_COUNT, + network.MSG_MHF_GET_UD_RANKING, + network.MSG_MHF_GET_UD_MY_RANKING, + network.MSG_MHF_ACQUIRE_MONTHLY_REWARD, + network.MSG_MHF_GET_UD_GUILD_MAP_INFO, + network.MSG_MHF_GENERATE_UD_GUILD_MAP, + network.MSG_MHF_GET_UD_TACTICS_POINT, + network.MSG_MHF_ADD_UD_TACTICS_POINT, + network.MSG_MHF_GET_UD_TACTICS_RANKING, + network.MSG_MHF_GET_UD_TACTICS_REWARD_LIST, + network.MSG_MHF_GET_UD_TACTICS_LOG, + network.MSG_MHF_GET_EQUIP_SKIN_HIST, + network.MSG_MHF_UPDATE_EQUIP_SKIN_HIST, + network.MSG_MHF_GET_UD_TACTICS_FOLLOWER, + network.MSG_MHF_SET_UD_TACTICS_FOLLOWER, + network.MSG_MHF_GET_UD_SHOP_COIN, + network.MSG_MHF_USE_UD_SHOP_COIN, + network.MSG_MHF_GET_ENHANCED_MINIDATA, + network.MSG_MHF_SET_ENHANCED_MINIDATA, + network.MSG_MHF_SEX_CHANGER, + network.MSG_MHF_GET_LOBBY_CROWD, + network.MSG_SYS_reserve180, + network.MSG_MHF_GUILD_HUNTDATA, + network.MSG_MHF_ADD_KOURYOU_POINT, + network.MSG_MHF_GET_KOURYOU_POINT, + network.MSG_MHF_EXCHANGE_KOURYOU_POINT, + network.MSG_MHF_GET_UD_TACTICS_BONUS_QUEST, + network.MSG_MHF_GET_UD_TACTICS_FIRST_QUEST_BONUS, + network.MSG_MHF_GET_UD_TACTICS_REMAINING_POINT, + network.MSG_SYS_reserve188, + network.MSG_MHF_LOAD_PLATE_MYSET, + network.MSG_MHF_SAVE_PLATE_MYSET, + network.MSG_SYS_reserve18B, + network.MSG_MHF_GET_RESTRICTION_EVENT, + network.MSG_MHF_SET_RESTRICTION_EVENT, + network.MSG_SYS_reserve18E, + network.MSG_SYS_reserve18F, + network.MSG_MHF_GET_TREND_WEAPON, + network.MSG_MHF_UPDATE_USE_TREND_WEAPON_LOG, + network.MSG_SYS_reserve192, + network.MSG_SYS_reserve193, + network.MSG_SYS_reserve194, + network.MSG_MHF_SAVE_RENGOKU_DATA, + network.MSG_MHF_LOAD_RENGOKU_DATA, + network.MSG_MHF_GET_RENGOKU_BINARY, + network.MSG_MHF_ENUMERATE_RENGOKU_RANKING, + network.MSG_MHF_GET_RENGOKU_RANKING_RANK, + network.MSG_MHF_ACQUIRE_EXCHANGE_SHOP, + network.MSG_SYS_reserve19B, + network.MSG_MHF_SAVE_MEZFES_DATA, + network.MSG_MHF_LOAD_MEZFES_DATA, + network.MSG_SYS_reserve19E, + network.MSG_SYS_reserve19F, + network.MSG_MHF_UPDATE_FORCE_GUILD_RANK, + network.MSG_MHF_RESET_TITLE, + network.MSG_MHF_ENUMERATE_GUILD_MESSAGE_BOARD, + network.MSG_MHF_UPDATE_GUILD_MESSAGE_BOARD, + network.MSG_SYS_reserve1A4, + network.MSG_MHF_REGIST_GUILD_ADVENTURE_DIVA, + network.MSG_SYS_reserve1A6, + network.MSG_SYS_reserve1A7, + network.MSG_SYS_reserve1A8, + network.MSG_SYS_reserve1A9, + network.MSG_SYS_reserve1AA, + network.MSG_SYS_reserve1AB, + network.MSG_SYS_reserve1AC, + network.MSG_SYS_reserve1AD, + network.MSG_SYS_reserve1AE, + network.MSG_SYS_reserve1AF, + } + + for _, opcode := range opcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", opcode) + return + } + // Verify Opcode() returns the correct value + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} + +// TestAckHandlePacketsParse tests parsing of packets with simple AckHandle uint32 field +func TestAckHandlePacketsParse(t *testing.T) { + testCases := []struct { + name string + opcode network.PacketID + }{ + {"MsgMhfGetAchievement", network.MSG_MHF_GET_ACHIEVEMENT}, + {"MsgMhfGetTowerInfo", network.MSG_MHF_GET_TOWER_INFO}, + {"MsgMhfGetGemInfo", network.MSG_MHF_GET_GEM_INFO}, + {"MsgMhfGetBoostTime", network.MSG_MHF_GET_BOOST_TIME}, + {"MsgMhfGetCafeDuration", network.MSG_MHF_GET_CAFE_DURATION}, + {"MsgMhfGetGachaPoint", network.MSG_MHF_GET_GACHA_POINT}, + {"MsgMhfLoadPartner", network.MSG_MHF_LOAD_PARTNER}, + {"MsgMhfLoadOtomoAirou", network.MSG_MHF_LOAD_OTOMO_AIROU}, + {"MsgMhfLoadPlateData", network.MSG_MHF_LOAD_PLATE_DATA}, + {"MsgMhfLoadPlateBox", network.MSG_MHF_LOAD_PLATE_BOX}, + {"MsgMhfLoadDecoMyset", network.MSG_MHF_LOAD_DECO_MYSET}, + {"MsgMhfLoadGuildCooking", network.MSG_MHF_LOAD_GUILD_COOKING}, + {"MsgMhfLoadGuildAdventure", network.MSG_MHF_LOAD_GUILD_ADVENTURE}, + {"MsgMhfLoadHunterNavi", network.MSG_MHF_LOAD_HUNTER_NAVI}, + {"MsgMhfInfoFesta", network.MSG_MHF_INFO_FESTA}, + {"MsgMhfInfoTournament", network.MSG_MHF_INFO_TOURNAMENT}, + {"MsgMhfEnumerateQuest", network.MSG_MHF_ENUMERATE_QUEST}, + {"MsgMhfEnumerateEvent", network.MSG_MHF_ENUMERATE_EVENT}, + {"MsgMhfEnumerateShop", network.MSG_MHF_ENUMERATE_SHOP}, + {"MsgMhfEnumerateRanking", network.MSG_MHF_ENUMERATE_RANKING}, + {"MsgMhfEnumerateOrder", network.MSG_MHF_ENUMERATE_ORDER}, + {"MsgMhfEnumerateCampaign", network.MSG_MHF_ENUMERATE_CAMPAIGN}, + {"MsgMhfGetWeeklySchedule", network.MSG_MHF_GET_WEEKLY_SCHEDULE}, + {"MsgMhfGetUdSchedule", network.MSG_MHF_GET_UD_SCHEDULE}, + {"MsgMhfGetUdInfo", network.MSG_MHF_GET_UD_INFO}, + {"MsgMhfGetKijuInfo", network.MSG_MHF_GET_KIJU_INFO}, + } + + ctx := &clientctx.ClientContext{} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pkt := FromOpcode(tc.opcode) + if pkt == nil { + t.Skipf("FromOpcode(%s) returned nil", tc.opcode) + return + } + + // Create test data - most of these packets read AckHandle + additional data + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + // Write extra padding bytes for packets that expect more data + for i := 0; i < 32; i++ { + bf.WriteUint32(uint32(i)) + } + bf.Seek(0, io.SeekStart) + + // Parse should not panic + err := pkt.Parse(bf, ctx) + if err != nil { + t.Logf("Parse() returned error (may be expected): %v", err) + } + }) + } +} + +// TestAddAchievementParse tests MsgMhfAddAchievement Parse +func TestAddAchievementParse(t *testing.T) { + tests := []struct { + name string + achievementID uint8 + unk1 uint16 + unk2 uint16 + }{ + {"typical values", 1, 100, 200}, + {"zero values", 0, 0, 0}, + {"max values", 255, 65535, 65535}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(tt.achievementID) + bf.WriteUint16(tt.unk1) + bf.WriteUint16(tt.unk2) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAddAchievement{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AchievementID != tt.achievementID { + t.Errorf("AchievementID = %d, want %d", pkt.AchievementID, tt.achievementID) + } + if pkt.Unk1 != tt.unk1 { + t.Errorf("Unk1 = %d, want %d", pkt.Unk1, tt.unk1) + } + if pkt.Unk2 != tt.unk2 { + t.Errorf("Unk2 = %d, want %d", pkt.Unk2, tt.unk2) + } + }) + } +} + +// TestGetAchievementParse tests MsgMhfGetAchievement Parse +func TestGetAchievementParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + charID uint32 + unk1 uint32 + }{ + {"typical values", 1, 12345, 0}, + {"large values", 0xFFFFFFFF, 0xDEADBEEF, 0xCAFEBABE}, + {"zero values", 0, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint32(tt.charID) + bf.WriteUint32(tt.unk1) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfGetAchievement{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.CharID != tt.charID { + t.Errorf("CharID = %d, want %d", pkt.CharID, tt.charID) + } + // Unk1 (third uint32) is read and discarded in Parse on main + }) + } +} + +// TestBuildNotImplemented tests that Build returns error for packets without implementation +func TestBuildNotImplemented(t *testing.T) { + packetsToTest := []MHFPacket{ + &MsgMhfAddAchievement{}, + &MsgMhfGetAchievement{}, + &MsgMhfAcquireItem{}, + &MsgMhfEnumerateGuild{}, + &MsgMhfInfoGuild{}, + &MsgMhfCreateGuild{}, + &MsgMhfOperateGuild{}, + &MsgMhfOperateGuildMember{}, + &MsgMhfUpdateGuild{}, + &MsgMhfArrangeGuildMember{}, + &MsgMhfEnumerateGuildMember{}, + &MsgMhfInfoFesta{}, + &MsgMhfEntryFesta{}, + &MsgMhfChargeFesta{}, + &MsgMhfAcquireFesta{}, + &MsgMhfVoteFesta{}, + &MsgMhfInfoTournament{}, + &MsgMhfEntryTournament{}, + &MsgMhfAcquireTournament{}, + } + + for _, pkt := range packetsToTest { + t.Run(pkt.Opcode().String(), func(t *testing.T) { + bf := byteframe.NewByteFrame() + err := pkt.Build(bf, &clientctx.ClientContext{}) + if err == nil { + t.Logf("Build() did not return error (implementation may exist)") + } else { + // Expected - Build is not implemented + if err.Error() != "NOT IMPLEMENTED" { + t.Logf("Build() returned unexpected error: %v", err) + } + } + }) + } +} + +// TestReservePacketsOpcode tests that reserve packets have correct opcodes +func TestReservePacketsOpcode(t *testing.T) { + reservePackets := []struct { + opcode network.PacketID + }{ + {network.MSG_SYS_reserve01}, + {network.MSG_SYS_reserve02}, + {network.MSG_SYS_reserve03}, + {network.MSG_SYS_reserve04}, + {network.MSG_SYS_reserve05}, + {network.MSG_SYS_reserve06}, + {network.MSG_SYS_reserve07}, + {network.MSG_SYS_reserve0C}, + {network.MSG_SYS_reserve0D}, + {network.MSG_SYS_reserve0E}, + {network.MSG_SYS_reserve4A}, + {network.MSG_SYS_reserve4B}, + {network.MSG_SYS_reserve4C}, + {network.MSG_SYS_reserve4D}, + {network.MSG_SYS_reserve4E}, + {network.MSG_SYS_reserve4F}, + {network.MSG_SYS_reserve55}, + {network.MSG_SYS_reserve56}, + {network.MSG_SYS_reserve57}, + {network.MSG_SYS_reserve5C}, + {network.MSG_SYS_reserve5E}, + {network.MSG_SYS_reserve5F}, + {network.MSG_SYS_reserve71}, + {network.MSG_SYS_reserve72}, + {network.MSG_SYS_reserve73}, + {network.MSG_SYS_reserve74}, + {network.MSG_SYS_reserve75}, + {network.MSG_SYS_reserve76}, + {network.MSG_SYS_reserve77}, + {network.MSG_SYS_reserve78}, + {network.MSG_SYS_reserve79}, + {network.MSG_SYS_reserve7A}, + {network.MSG_SYS_reserve7B}, + {network.MSG_SYS_reserve7C}, + {network.MSG_SYS_reserve7E}, + {network.MSG_SYS_reserve180}, + {network.MSG_SYS_reserve188}, + {network.MSG_SYS_reserve18B}, + {network.MSG_SYS_reserve18E}, + {network.MSG_SYS_reserve18F}, + {network.MSG_SYS_reserve192}, + {network.MSG_SYS_reserve193}, + {network.MSG_SYS_reserve194}, + {network.MSG_SYS_reserve19B}, + {network.MSG_SYS_reserve19E}, + {network.MSG_SYS_reserve19F}, + {network.MSG_SYS_reserve1A4}, + {network.MSG_SYS_reserve1A6}, + {network.MSG_SYS_reserve1A7}, + {network.MSG_SYS_reserve1A8}, + {network.MSG_SYS_reserve1A9}, + {network.MSG_SYS_reserve1AA}, + {network.MSG_SYS_reserve1AB}, + {network.MSG_SYS_reserve1AC}, + {network.MSG_SYS_reserve1AD}, + {network.MSG_SYS_reserve1AE}, + {network.MSG_SYS_reserve1AF}, + } + + for _, tc := range reservePackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + pkt := FromOpcode(tc.opcode) + if pkt == nil { + t.Errorf("FromOpcode(%s) returned nil", tc.opcode) + return + } + if pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestMHFPacketsOpcode tests Opcode() method for various MHF packets +func TestMHFPacketsOpcode(t *testing.T) { + mhfPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfSavedata{}, network.MSG_MHF_SAVEDATA}, + {&MsgMhfLoaddata{}, network.MSG_MHF_LOADDATA}, + {&MsgMhfListMember{}, network.MSG_MHF_LIST_MEMBER}, + {&MsgMhfOprMember{}, network.MSG_MHF_OPR_MEMBER}, + {&MsgMhfEnumerateDistItem{}, network.MSG_MHF_ENUMERATE_DIST_ITEM}, + {&MsgMhfApplyDistItem{}, network.MSG_MHF_APPLY_DIST_ITEM}, + {&MsgMhfAcquireDistItem{}, network.MSG_MHF_ACQUIRE_DIST_ITEM}, + {&MsgMhfGetDistDescription{}, network.MSG_MHF_GET_DIST_DESCRIPTION}, + {&MsgMhfSendMail{}, network.MSG_MHF_SEND_MAIL}, + {&MsgMhfReadMail{}, network.MSG_MHF_READ_MAIL}, + {&MsgMhfListMail{}, network.MSG_MHF_LIST_MAIL}, + {&MsgMhfOprtMail{}, network.MSG_MHF_OPRT_MAIL}, + {&MsgMhfLoadFavoriteQuest{}, network.MSG_MHF_LOAD_FAVORITE_QUEST}, + {&MsgMhfSaveFavoriteQuest{}, network.MSG_MHF_SAVE_FAVORITE_QUEST}, + {&MsgMhfRegisterEvent{}, network.MSG_MHF_REGISTER_EVENT}, + {&MsgMhfReleaseEvent{}, network.MSG_MHF_RELEASE_EVENT}, + {&MsgMhfTransitMessage{}, network.MSG_MHF_TRANSIT_MESSAGE}, + {&MsgMhfPresentBox{}, network.MSG_MHF_PRESENT_BOX}, + {&MsgMhfServerCommand{}, network.MSG_MHF_SERVER_COMMAND}, + {&MsgMhfShutClient{}, network.MSG_MHF_SHUT_CLIENT}, + {&MsgMhfAnnounce{}, network.MSG_MHF_ANNOUNCE}, + {&MsgMhfSetLoginwindow{}, network.MSG_MHF_SET_LOGINWINDOW}, + {&MsgMhfGetCaUniqueID{}, network.MSG_MHF_GET_CA_UNIQUE_ID}, + {&MsgMhfSetCaAchievement{}, network.MSG_MHF_SET_CA_ACHIEVEMENT}, + {&MsgMhfCaravanMyScore{}, network.MSG_MHF_CARAVAN_MY_SCORE}, + {&MsgMhfCaravanRanking{}, network.MSG_MHF_CARAVAN_RANKING}, + {&MsgMhfCaravanMyRank{}, network.MSG_MHF_CARAVAN_MY_RANK}, + } + + for _, tc := range mhfPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestGuildPacketsOpcode tests guild-related packets +func TestGuildPacketsOpcode(t *testing.T) { + guildPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfCreateGuild{}, network.MSG_MHF_CREATE_GUILD}, + {&MsgMhfOperateGuild{}, network.MSG_MHF_OPERATE_GUILD}, + {&MsgMhfOperateGuildMember{}, network.MSG_MHF_OPERATE_GUILD_MEMBER}, + {&MsgMhfInfoGuild{}, network.MSG_MHF_INFO_GUILD}, + {&MsgMhfEnumerateGuild{}, network.MSG_MHF_ENUMERATE_GUILD}, + {&MsgMhfUpdateGuild{}, network.MSG_MHF_UPDATE_GUILD}, + {&MsgMhfArrangeGuildMember{}, network.MSG_MHF_ARRANGE_GUILD_MEMBER}, + {&MsgMhfEnumerateGuildMember{}, network.MSG_MHF_ENUMERATE_GUILD_MEMBER}, + {&MsgMhfEnumerateGuildItem{}, network.MSG_MHF_ENUMERATE_GUILD_ITEM}, + {&MsgMhfUpdateGuildItem{}, network.MSG_MHF_UPDATE_GUILD_ITEM}, + {&MsgMhfUpdateGuildIcon{}, network.MSG_MHF_UPDATE_GUILD_ICON}, + {&MsgMhfEnumerateGuildTresure{}, network.MSG_MHF_ENUMERATE_GUILD_TRESURE}, + {&MsgMhfRegistGuildTresure{}, network.MSG_MHF_REGIST_GUILD_TRESURE}, + {&MsgMhfAcquireGuildTresure{}, network.MSG_MHF_ACQUIRE_GUILD_TRESURE}, + {&MsgMhfOperateGuildTresureReport{}, network.MSG_MHF_OPERATE_GUILD_TRESURE_REPORT}, + {&MsgMhfGetGuildTresureSouvenir{}, network.MSG_MHF_GET_GUILD_TRESURE_SOUVENIR}, + {&MsgMhfAcquireGuildTresureSouvenir{}, network.MSG_MHF_ACQUIRE_GUILD_TRESURE_SOUVENIR}, + {&MsgMhfLoadGuildCooking{}, network.MSG_MHF_LOAD_GUILD_COOKING}, + {&MsgMhfRegistGuildCooking{}, network.MSG_MHF_REGIST_GUILD_COOKING}, + {&MsgMhfLoadGuildAdventure{}, network.MSG_MHF_LOAD_GUILD_ADVENTURE}, + {&MsgMhfRegistGuildAdventure{}, network.MSG_MHF_REGIST_GUILD_ADVENTURE}, + {&MsgMhfAcquireGuildAdventure{}, network.MSG_MHF_ACQUIRE_GUILD_ADVENTURE}, + {&MsgMhfChargeGuildAdventure{}, network.MSG_MHF_CHARGE_GUILD_ADVENTURE}, + {&MsgMhfGetGuildMissionList{}, network.MSG_MHF_GET_GUILD_MISSION_LIST}, + {&MsgMhfGetGuildMissionRecord{}, network.MSG_MHF_GET_GUILD_MISSION_RECORD}, + {&MsgMhfAddGuildMissionCount{}, network.MSG_MHF_ADD_GUILD_MISSION_COUNT}, + {&MsgMhfSetGuildMissionTarget{}, network.MSG_MHF_SET_GUILD_MISSION_TARGET}, + {&MsgMhfCancelGuildMissionTarget{}, network.MSG_MHF_CANCEL_GUILD_MISSION_TARGET}, + {&MsgMhfGetGuildWeeklyBonusMaster{}, network.MSG_MHF_GET_GUILD_WEEKLY_BONUS_MASTER}, + {&MsgMhfGetGuildWeeklyBonusActiveCount{}, network.MSG_MHF_GET_GUILD_WEEKLY_BONUS_ACTIVE_COUNT}, + {&MsgMhfAddGuildWeeklyBonusExceptionalUser{}, network.MSG_MHF_ADD_GUILD_WEEKLY_BONUS_EXCEPTIONAL_USER}, + {&MsgMhfGetGuildTargetMemberNum{}, network.MSG_MHF_GET_GUILD_TARGET_MEMBER_NUM}, + {&MsgMhfPostGuildScout{}, network.MSG_MHF_POST_GUILD_SCOUT}, + {&MsgMhfCancelGuildScout{}, network.MSG_MHF_CANCEL_GUILD_SCOUT}, + {&MsgMhfAnswerGuildScout{}, network.MSG_MHF_ANSWER_GUILD_SCOUT}, + {&MsgMhfGetGuildScoutList{}, network.MSG_MHF_GET_GUILD_SCOUT_LIST}, + {&MsgMhfGetGuildManageRight{}, network.MSG_MHF_GET_GUILD_MANAGE_RIGHT}, + {&MsgMhfSetGuildManageRight{}, network.MSG_MHF_SET_GUILD_MANAGE_RIGHT}, + {&MsgMhfGetRejectGuildScout{}, network.MSG_MHF_GET_REJECT_GUILD_SCOUT}, + {&MsgMhfSetRejectGuildScout{}, network.MSG_MHF_SET_REJECT_GUILD_SCOUT}, + {&MsgMhfGuildHuntdata{}, network.MSG_MHF_GUILD_HUNTDATA}, + {&MsgMhfUpdateForceGuildRank{}, network.MSG_MHF_UPDATE_FORCE_GUILD_RANK}, + {&MsgMhfEnumerateGuildMessageBoard{}, network.MSG_MHF_ENUMERATE_GUILD_MESSAGE_BOARD}, + {&MsgMhfUpdateGuildMessageBoard{}, network.MSG_MHF_UPDATE_GUILD_MESSAGE_BOARD}, + } + + for _, tc := range guildPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestFestaPacketsOpcode tests festa-related packets +func TestFestaPacketsOpcode(t *testing.T) { + festaPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfInfoFesta{}, network.MSG_MHF_INFO_FESTA}, + {&MsgMhfEntryFesta{}, network.MSG_MHF_ENTRY_FESTA}, + {&MsgMhfChargeFesta{}, network.MSG_MHF_CHARGE_FESTA}, + {&MsgMhfAcquireFesta{}, network.MSG_MHF_ACQUIRE_FESTA}, + {&MsgMhfStateFestaU{}, network.MSG_MHF_STATE_FESTA_U}, + {&MsgMhfStateFestaG{}, network.MSG_MHF_STATE_FESTA_G}, + {&MsgMhfEnumerateFestaMember{}, network.MSG_MHF_ENUMERATE_FESTA_MEMBER}, + {&MsgMhfVoteFesta{}, network.MSG_MHF_VOTE_FESTA}, + {&MsgMhfEnumerateFestaIntermediatePrize{}, network.MSG_MHF_ENUMERATE_FESTA_INTERMEDIATE_PRIZE}, + {&MsgMhfAcquireFestaIntermediatePrize{}, network.MSG_MHF_ACQUIRE_FESTA_INTERMEDIATE_PRIZE}, + {&MsgMhfEnumerateFestaPersonalPrize{}, network.MSG_MHF_ENUMERATE_FESTA_PERSONAL_PRIZE}, + {&MsgMhfAcquireFestaPersonalPrize{}, network.MSG_MHF_ACQUIRE_FESTA_PERSONAL_PRIZE}, + } + + for _, tc := range festaPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestCafePacketsOpcode tests cafe-related packets +func TestCafePacketsOpcode(t *testing.T) { + cafePackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfAcquireCafeItem{}, network.MSG_MHF_ACQUIRE_CAFE_ITEM}, + {&MsgMhfUpdateCafepoint{}, network.MSG_MHF_UPDATE_CAFEPOINT}, + {&MsgMhfCheckDailyCafepoint{}, network.MSG_MHF_CHECK_DAILY_CAFEPOINT}, + {&MsgMhfGetCafeDuration{}, network.MSG_MHF_GET_CAFE_DURATION}, + {&MsgMhfGetCafeDurationBonusInfo{}, network.MSG_MHF_GET_CAFE_DURATION_BONUS_INFO}, + {&MsgMhfReceiveCafeDurationBonus{}, network.MSG_MHF_RECEIVE_CAFE_DURATION_BONUS}, + {&MsgMhfPostCafeDurationBonusReceived{}, network.MSG_MHF_POST_CAFE_DURATION_BONUS_RECEIVED}, + } + + for _, tc := range cafePackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestGachaPacketsOpcode tests gacha-related packets +func TestGachaPacketsOpcode(t *testing.T) { + gachaPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfGetGachaPoint{}, network.MSG_MHF_GET_GACHA_POINT}, + {&MsgMhfUseGachaPoint{}, network.MSG_MHF_USE_GACHA_POINT}, + {&MsgMhfPlayStepupGacha{}, network.MSG_MHF_PLAY_STEPUP_GACHA}, + {&MsgMhfReceiveGachaItem{}, network.MSG_MHF_RECEIVE_GACHA_ITEM}, + {&MsgMhfGetStepupStatus{}, network.MSG_MHF_GET_STEPUP_STATUS}, + {&MsgMhfPlayFreeGacha{}, network.MSG_MHF_PLAY_FREE_GACHA}, + {&MsgMhfGetBoxGachaInfo{}, network.MSG_MHF_GET_BOX_GACHA_INFO}, + {&MsgMhfPlayBoxGacha{}, network.MSG_MHF_PLAY_BOX_GACHA}, + {&MsgMhfResetBoxGachaInfo{}, network.MSG_MHF_RESET_BOX_GACHA_INFO}, + {&MsgMhfPlayNormalGacha{}, network.MSG_MHF_PLAY_NORMAL_GACHA}, + {&MsgMhfGetGachaPlayHistory{}, network.MSG_MHF_GET_GACHA_PLAY_HISTORY}, + } + + for _, tc := range gachaPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestUDPacketsOpcode tests UD (Ultimate Devastation) related packets +func TestUDPacketsOpcode(t *testing.T) { + udPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfGetUdSchedule{}, network.MSG_MHF_GET_UD_SCHEDULE}, + {&MsgMhfGetUdInfo{}, network.MSG_MHF_GET_UD_INFO}, + {&MsgMhfAddUdPoint{}, network.MSG_MHF_ADD_UD_POINT}, + {&MsgMhfGetUdMyPoint{}, network.MSG_MHF_GET_UD_MY_POINT}, + {&MsgMhfGetUdTotalPointInfo{}, network.MSG_MHF_GET_UD_TOTAL_POINT_INFO}, + {&MsgMhfGetUdBonusQuestInfo{}, network.MSG_MHF_GET_UD_BONUS_QUEST_INFO}, + {&MsgMhfGetUdSelectedColorInfo{}, network.MSG_MHF_GET_UD_SELECTED_COLOR_INFO}, + {&MsgMhfGetUdMonsterPoint{}, network.MSG_MHF_GET_UD_MONSTER_POINT}, + {&MsgMhfGetUdDailyPresentList{}, network.MSG_MHF_GET_UD_DAILY_PRESENT_LIST}, + {&MsgMhfGetUdNormaPresentList{}, network.MSG_MHF_GET_UD_NORMA_PRESENT_LIST}, + {&MsgMhfGetUdRankingRewardList{}, network.MSG_MHF_GET_UD_RANKING_REWARD_LIST}, + {&MsgMhfAcquireUdItem{}, network.MSG_MHF_ACQUIRE_UD_ITEM}, + {&MsgMhfGetUdRanking{}, network.MSG_MHF_GET_UD_RANKING}, + {&MsgMhfGetUdMyRanking{}, network.MSG_MHF_GET_UD_MY_RANKING}, + {&MsgMhfGetUdGuildMapInfo{}, network.MSG_MHF_GET_UD_GUILD_MAP_INFO}, + {&MsgMhfGenerateUdGuildMap{}, network.MSG_MHF_GENERATE_UD_GUILD_MAP}, + {&MsgMhfGetUdTacticsPoint{}, network.MSG_MHF_GET_UD_TACTICS_POINT}, + {&MsgMhfAddUdTacticsPoint{}, network.MSG_MHF_ADD_UD_TACTICS_POINT}, + {&MsgMhfGetUdTacticsRanking{}, network.MSG_MHF_GET_UD_TACTICS_RANKING}, + {&MsgMhfGetUdTacticsRewardList{}, network.MSG_MHF_GET_UD_TACTICS_REWARD_LIST}, + {&MsgMhfGetUdTacticsLog{}, network.MSG_MHF_GET_UD_TACTICS_LOG}, + {&MsgMhfGetUdTacticsFollower{}, network.MSG_MHF_GET_UD_TACTICS_FOLLOWER}, + {&MsgMhfSetUdTacticsFollower{}, network.MSG_MHF_SET_UD_TACTICS_FOLLOWER}, + {&MsgMhfGetUdShopCoin{}, network.MSG_MHF_GET_UD_SHOP_COIN}, + {&MsgMhfUseUdShopCoin{}, network.MSG_MHF_USE_UD_SHOP_COIN}, + {&MsgMhfGetUdTacticsBonusQuest{}, network.MSG_MHF_GET_UD_TACTICS_BONUS_QUEST}, + {&MsgMhfGetUdTacticsFirstQuestBonus{}, network.MSG_MHF_GET_UD_TACTICS_FIRST_QUEST_BONUS}, + {&MsgMhfGetUdTacticsRemainingPoint{}, network.MSG_MHF_GET_UD_TACTICS_REMAINING_POINT}, + } + + for _, tc := range udPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestRengokuPacketsOpcode tests rengoku (purgatory tower) related packets +func TestRengokuPacketsOpcode(t *testing.T) { + rengokuPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfSaveRengokuData{}, network.MSG_MHF_SAVE_RENGOKU_DATA}, + {&MsgMhfLoadRengokuData{}, network.MSG_MHF_LOAD_RENGOKU_DATA}, + {&MsgMhfGetRengokuBinary{}, network.MSG_MHF_GET_RENGOKU_BINARY}, + {&MsgMhfEnumerateRengokuRanking{}, network.MSG_MHF_ENUMERATE_RENGOKU_RANKING}, + {&MsgMhfGetRengokuRankingRank{}, network.MSG_MHF_GET_RENGOKU_RANKING_RANK}, + } + + for _, tc := range rengokuPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestMezFesPacketsOpcode tests Mezeporta Festival related packets +func TestMezFesPacketsOpcode(t *testing.T) { + mezfesPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfSaveMezfesData{}, network.MSG_MHF_SAVE_MEZFES_DATA}, + {&MsgMhfLoadMezfesData{}, network.MSG_MHF_LOAD_MEZFES_DATA}, + } + + for _, tc := range mezfesPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestWarehousePacketsOpcode tests warehouse related packets +func TestWarehousePacketsOpcode(t *testing.T) { + warehousePackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfOperateWarehouse{}, network.MSG_MHF_OPERATE_WAREHOUSE}, + {&MsgMhfEnumerateWarehouse{}, network.MSG_MHF_ENUMERATE_WAREHOUSE}, + {&MsgMhfUpdateWarehouse{}, network.MSG_MHF_UPDATE_WAREHOUSE}, + } + + for _, tc := range warehousePackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestMercenaryPacketsOpcode tests mercenary related packets +func TestMercenaryPacketsOpcode(t *testing.T) { + mercenaryPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfMercenaryHuntdata{}, network.MSG_MHF_MERCENARY_HUNTDATA}, + {&MsgMhfCreateMercenary{}, network.MSG_MHF_CREATE_MERCENARY}, + {&MsgMhfSaveMercenary{}, network.MSG_MHF_SAVE_MERCENARY}, + {&MsgMhfReadMercenaryW{}, network.MSG_MHF_READ_MERCENARY_W}, + {&MsgMhfReadMercenaryM{}, network.MSG_MHF_READ_MERCENARY_M}, + {&MsgMhfContractMercenary{}, network.MSG_MHF_CONTRACT_MERCENARY}, + {&MsgMhfEnumerateMercenaryLog{}, network.MSG_MHF_ENUMERATE_MERCENARY_LOG}, + } + + for _, tc := range mercenaryPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestHousePacketsOpcode tests house related packets +func TestHousePacketsOpcode(t *testing.T) { + housePackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfUpdateInterior{}, network.MSG_MHF_UPDATE_INTERIOR}, + {&MsgMhfEnumerateHouse{}, network.MSG_MHF_ENUMERATE_HOUSE}, + {&MsgMhfUpdateHouse{}, network.MSG_MHF_UPDATE_HOUSE}, + {&MsgMhfLoadHouse{}, network.MSG_MHF_LOAD_HOUSE}, + {&MsgMhfGetMyhouseInfo{}, network.MSG_MHF_GET_MYHOUSE_INFO}, + {&MsgMhfUpdateMyhouseInfo{}, network.MSG_MHF_UPDATE_MYHOUSE_INFO}, + } + + for _, tc := range housePackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestBoostPacketsOpcode tests boost related packets +func TestBoostPacketsOpcode(t *testing.T) { + boostPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfGetBoostTime{}, network.MSG_MHF_GET_BOOST_TIME}, + {&MsgMhfPostBoostTime{}, network.MSG_MHF_POST_BOOST_TIME}, + {&MsgMhfGetBoostTimeLimit{}, network.MSG_MHF_GET_BOOST_TIME_LIMIT}, + {&MsgMhfPostBoostTimeLimit{}, network.MSG_MHF_POST_BOOST_TIME_LIMIT}, + {&MsgMhfGetBoostRight{}, network.MSG_MHF_GET_BOOST_RIGHT}, + {&MsgMhfStartBoostTime{}, network.MSG_MHF_START_BOOST_TIME}, + {&MsgMhfPostBoostTimeQuestReturn{}, network.MSG_MHF_POST_BOOST_TIME_QUEST_RETURN}, + {&MsgMhfGetKeepLoginBoostStatus{}, network.MSG_MHF_GET_KEEP_LOGIN_BOOST_STATUS}, + {&MsgMhfUseKeepLoginBoost{}, network.MSG_MHF_USE_KEEP_LOGIN_BOOST}, + } + + for _, tc := range boostPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestTournamentPacketsOpcode tests tournament related packets +func TestTournamentPacketsOpcode(t *testing.T) { + tournamentPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfInfoTournament{}, network.MSG_MHF_INFO_TOURNAMENT}, + {&MsgMhfEntryTournament{}, network.MSG_MHF_ENTRY_TOURNAMENT}, + {&MsgMhfEnterTournamentQuest{}, network.MSG_MHF_ENTER_TOURNAMENT_QUEST}, + {&MsgMhfAcquireTournament{}, network.MSG_MHF_ACQUIRE_TOURNAMENT}, + } + + for _, tc := range tournamentPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestPlatePacketsOpcode tests plate related packets +func TestPlatePacketsOpcode(t *testing.T) { + platePackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfLoadPlateData{}, network.MSG_MHF_LOAD_PLATE_DATA}, + {&MsgMhfSavePlateData{}, network.MSG_MHF_SAVE_PLATE_DATA}, + {&MsgMhfLoadPlateBox{}, network.MSG_MHF_LOAD_PLATE_BOX}, + {&MsgMhfSavePlateBox{}, network.MSG_MHF_SAVE_PLATE_BOX}, + {&MsgMhfLoadPlateMyset{}, network.MSG_MHF_LOAD_PLATE_MYSET}, + {&MsgMhfSavePlateMyset{}, network.MSG_MHF_SAVE_PLATE_MYSET}, + } + + for _, tc := range platePackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} + +// TestScenarioPacketsOpcode tests scenario related packets +func TestScenarioPacketsOpcode(t *testing.T) { + scenarioPackets := []struct { + pkt MHFPacket + opcode network.PacketID + }{ + {&MsgMhfInfoScenarioCounter{}, network.MSG_MHF_INFO_SCENARIO_COUNTER}, + {&MsgMhfSaveScenarioData{}, network.MSG_MHF_SAVE_SCENARIO_DATA}, + {&MsgMhfLoadScenarioData{}, network.MSG_MHF_LOAD_SCENARIO_DATA}, + } + + for _, tc := range scenarioPackets { + t.Run(tc.opcode.String(), func(t *testing.T) { + if tc.pkt.Opcode() != tc.opcode { + t.Errorf("Opcode() = %s, want %s", tc.pkt.Opcode(), tc.opcode) + } + }) + } +} diff --git a/network/mhfpacket/msg_mhf_acquire_cafe_item_test.go b/network/mhfpacket/msg_mhf_acquire_cafe_item_test.go new file mode 100644 index 000000000..865b99aea --- /dev/null +++ b/network/mhfpacket/msg_mhf_acquire_cafe_item_test.go @@ -0,0 +1,180 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + _config "erupe-ce/config" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func init() { + // Initialize ErupeConfig for tests that access it + _config.ErupeConfig = &_config.Config{ + RealClientMode: _config.ZZ, // Default to ZZ for tests + } +} + +func TestMsgMhfAcquireCafeItemOpcode(t *testing.T) { + pkt := &MsgMhfAcquireCafeItem{} + if pkt.Opcode() != network.MSG_MHF_ACQUIRE_CAFE_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ACQUIRE_CAFE_ITEM", pkt.Opcode()) + } +} + +func TestMsgMhfAcquireCafeItemParse(t *testing.T) { + // Test basic parsing with current implementation (always reads uint32 for PointCost) + // Current code: m.PointCost = bf.ReadUint32() (no client mode check) + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint16(1) // ItemType + bf.WriteUint16(100) // ItemID + bf.WriteUint16(5) // Quant + bf.WriteUint32(1000) // PointCost (uint32) + bf.WriteUint16(0) // Unk0 + + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireCafeItem{} + ctx := &clientctx.ClientContext{} + + err := pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.ItemType != 1 { + t.Errorf("ItemType = %d, want 1", pkt.ItemType) + } + if pkt.ItemID != 100 { + t.Errorf("ItemID = %d, want 100", pkt.ItemID) + } + if pkt.Quant != 5 { + t.Errorf("Quant = %d, want 5", pkt.Quant) + } + if pkt.PointCost != 1000 { + t.Errorf("PointCost = %d, want 1000", pkt.PointCost) + } +} + +// TestMsgMhfAcquireCafeItemParseUint32PointCost documents the current behavior. +// +// CURRENT BEHAVIOR: Always reads PointCost as uint32. +// +// EXPECTED BEHAVIOR AFTER FIX (commit 3d0114c): +// - G6+: Read PointCost as uint32 +// - G1-G5.2: Read PointCost as uint16 +// +// This test verifies current uint32 parsing works correctly. +// After the fix is applied, this test should still pass for G6+ clients. +func TestMsgMhfAcquireCafeItemParseUint32PointCost(t *testing.T) { + tests := []struct { + name string + pointCost uint32 + wantCost uint32 + }{ + {"small cost", 100, 100}, + {"medium cost", 5000, 5000}, + {"large cost exceeding uint16", 70000, 70000}, + {"max uint32", 0xFFFFFFFF, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAAAABBBB) // AckHandle + bf.WriteUint16(1) // ItemType + bf.WriteUint16(200) // ItemID + bf.WriteUint16(10) // Quant + bf.WriteUint32(tt.pointCost) + bf.WriteUint16(0) // Unk0 + + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireCafeItem{} + ctx := &clientctx.ClientContext{} + + err := pkt.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.PointCost != tt.wantCost { + t.Errorf("PointCost = %d, want %d", pkt.PointCost, tt.wantCost) + } + }) + } +} + +// TestMsgMhfAcquireCafeItemParseFieldOrder verifies the exact field order in parsing. +// This is important because the fix changes when PointCost is read (uint16 vs uint32). +func TestMsgMhfAcquireCafeItemParseFieldOrder(t *testing.T) { + // Build a packet with known values + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x11223344) // AckHandle (offset 0-3) + bf.WriteUint16(0x5566) // ItemType (offset 4-5) + bf.WriteUint16(0x7788) // ItemID (offset 6-7) + bf.WriteUint16(0x99AA) // Quant (offset 8-9) + bf.WriteUint32(0xBBCCDDEE) // PointCost (offset 10-13) + bf.WriteUint16(0xFF00) // Unk0 (offset 14-15) + + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireCafeItem{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x11223344 { + t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle) + } + if pkt.ItemType != 0x5566 { + t.Errorf("ItemType = 0x%X, want 0x5566", pkt.ItemType) + } + if pkt.ItemID != 0x7788 { + t.Errorf("ItemID = 0x%X, want 0x7788", pkt.ItemID) + } + if pkt.Quant != 0x99AA { + t.Errorf("Quant = 0x%X, want 0x99AA", pkt.Quant) + } + if pkt.PointCost != 0xBBCCDDEE { + t.Errorf("PointCost = 0x%X, want 0xBBCCDDEE", pkt.PointCost) + } + if pkt.Unk0 != 0xFF00 { + t.Errorf("Unk0 = 0x%X, want 0xFF00", pkt.Unk0) + } +} + +func TestMsgMhfAcquireCafeItemBuildNotImplemented(t *testing.T) { + pkt := &MsgMhfAcquireCafeItem{ + AckHandle: 123, + ItemType: 1, + ItemID: 100, + Quant: 5, + PointCost: 1000, + } + + bf := byteframe.NewByteFrame() + ctx := &clientctx.ClientContext{} + + err := pkt.Build(bf, ctx) + if err == nil { + t.Error("Build() should return error (NOT IMPLEMENTED)") + } +} + +func TestMsgMhfAcquireCafeItemFromOpcode(t *testing.T) { + pkt := FromOpcode(network.MSG_MHF_ACQUIRE_CAFE_ITEM) + if pkt == nil { + t.Fatal("FromOpcode(MSG_MHF_ACQUIRE_CAFE_ITEM) returned nil") + } + if pkt.Opcode() != network.MSG_MHF_ACQUIRE_CAFE_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ACQUIRE_CAFE_ITEM", pkt.Opcode()) + } +} diff --git a/network/mhfpacket/msg_mhf_acquire_test.go b/network/mhfpacket/msg_mhf_acquire_test.go new file mode 100644 index 000000000..0087af799 --- /dev/null +++ b/network/mhfpacket/msg_mhf_acquire_test.go @@ -0,0 +1,263 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func TestAcquirePacketOpcodes(t *testing.T) { + tests := []struct { + name string + pkt MHFPacket + expect network.PacketID + }{ + {"MsgMhfAcquireGuildTresure", &MsgMhfAcquireGuildTresure{}, network.MSG_MHF_ACQUIRE_GUILD_TRESURE}, + {"MsgMhfAcquireTitle", &MsgMhfAcquireTitle{}, network.MSG_MHF_ACQUIRE_TITLE}, + {"MsgMhfAcquireDistItem", &MsgMhfAcquireDistItem{}, network.MSG_MHF_ACQUIRE_DIST_ITEM}, + {"MsgMhfAcquireMonthlyItem", &MsgMhfAcquireMonthlyItem{}, network.MSG_MHF_ACQUIRE_MONTHLY_ITEM}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.pkt.Opcode(); got != tt.expect { + t.Errorf("Opcode() = %v, want %v", got, tt.expect) + } + }) + } +} + +func TestMsgMhfAcquireGuildTresureParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + huntID uint32 + unk bool + }{ + {"basic acquisition", 1, 12345, false}, + {"large hunt ID", 0xABCDEF12, 0xFFFFFFFF, true}, + {"zero values", 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.huntID) + bf.WriteBool(tt.unk) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireGuildTresure{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.HuntID != tt.huntID { + t.Errorf("HuntID = %d, want %d", pkt.HuntID, tt.huntID) + } + if pkt.Unk != tt.unk { + t.Errorf("Unk = %v, want %v", pkt.Unk, tt.unk) + } + }) + } +} + +func TestMsgMhfAcquireTitleParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + titleIDs []uint16 + }{ + {"acquire title 1", 1, []uint16{1}}, + {"acquire titles 100 200", 0x12345678, []uint16{100, 200}}, + {"no titles", 0xFFFFFFFF, []uint16{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint16(uint16(len(tt.titleIDs))) // count + bf.WriteUint16(0) // zeroed + for _, id := range tt.titleIDs { + bf.WriteUint16(id) + } + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireTitle{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if len(pkt.TitleIDs) != len(tt.titleIDs) { + t.Errorf("TitleIDs len = %d, want %d", len(pkt.TitleIDs), len(tt.titleIDs)) + } + for i, id := range tt.titleIDs { + if i < len(pkt.TitleIDs) && pkt.TitleIDs[i] != id { + t.Errorf("TitleIDs[%d] = %d, want %d", i, pkt.TitleIDs[i], id) + } + } + }) + } +} + +func TestMsgMhfAcquireDistItemParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + distributionType uint8 + distributionID uint32 + }{ + {"type 0", 1, 0, 12345}, + {"type 1", 0xABCD, 1, 67890}, + {"max values", 0xFFFFFFFF, 0xFF, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint8(tt.distributionType) + bf.WriteUint32(tt.distributionID) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireDistItem{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.DistributionType != tt.distributionType { + t.Errorf("DistributionType = %d, want %d", pkt.DistributionType, tt.distributionType) + } + if pkt.DistributionID != tt.distributionID { + t.Errorf("DistributionID = %d, want %d", pkt.DistributionID, tt.distributionID) + } + }) + } +} + +func TestMsgMhfAcquireMonthlyItemParse(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + unk0 uint8 + unk1 uint8 + unk2 uint16 + unk3 uint32 + }{ + {"basic", 1, 0, 0, 0, 0}, + {"with values", 100, 10, 20, 30, 40}, + {"max values", 0xFFFFFFFF, 0xFF, 0xFF, 0xFFFF, 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint8(tt.unk0) + bf.WriteUint8(tt.unk1) + bf.WriteUint16(tt.unk2) + bf.WriteUint32(tt.unk3) + bf.WriteUint32(0) // Zeroed (consumed by Parse) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireMonthlyItem{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + if pkt.Unk1 != tt.unk1 { + t.Errorf("Unk1 = %d, want %d", pkt.Unk1, tt.unk1) + } + if pkt.Unk2 != tt.unk2 { + t.Errorf("Unk2 = %d, want %d", pkt.Unk2, tt.unk2) + } + if pkt.Unk3 != tt.unk3 { + t.Errorf("Unk3 = %d, want %d", pkt.Unk3, tt.unk3) + } + }) + } +} + +func TestAcquirePacketsFromOpcode(t *testing.T) { + acquireOpcodes := []network.PacketID{ + network.MSG_MHF_ACQUIRE_GUILD_TRESURE, + network.MSG_MHF_ACQUIRE_TITLE, + network.MSG_MHF_ACQUIRE_DIST_ITEM, + network.MSG_MHF_ACQUIRE_MONTHLY_ITEM, + } + + for _, opcode := range acquireOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Fatalf("FromOpcode(%s) returned nil", opcode) + } + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} + +func TestAcquirePacketEdgeCases(t *testing.T) { + t.Run("guild tresure with max hunt ID", func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) + bf.WriteUint32(0xFFFFFFFF) + bf.WriteBool(true) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireGuildTresure{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.HuntID != 0xFFFFFFFF { + t.Errorf("HuntID = %d, want %d", pkt.HuntID, 0xFFFFFFFF) + } + }) + + t.Run("dist item with all types", func(t *testing.T) { + for i := uint8(0); i < 5; i++ { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) + bf.WriteUint8(i) + bf.WriteUint32(12345) + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAcquireDistItem{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v for type %d", err, i) + } + + if pkt.DistributionType != i { + t.Errorf("DistributionType = %d, want %d", pkt.DistributionType, i) + } + } + }) +} diff --git a/network/mhfpacket/msg_mhf_guacot_test.go b/network/mhfpacket/msg_mhf_guacot_test.go new file mode 100644 index 000000000..8be310d42 --- /dev/null +++ b/network/mhfpacket/msg_mhf_guacot_test.go @@ -0,0 +1,364 @@ +package mhfpacket + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" +) + +func TestMsgMhfUpdateGuacotOpcode_Guacot(t *testing.T) { + pkt := &MsgMhfUpdateGuacot{} + if pkt.Opcode() != network.MSG_MHF_UPDATE_GUACOT { + t.Errorf("Opcode() = %s, want MSG_MHF_UPDATE_GUACOT", pkt.Opcode()) + } +} + +func TestMsgMhfEnumerateGuacotOpcode_Guacot(t *testing.T) { + pkt := &MsgMhfEnumerateGuacot{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_GUACOT { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_GUACOT", pkt.Opcode()) + } +} + +func TestMsgMhfUpdateGuacotParse_SingleEntry(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0xAABBCCDD) // AckHandle + bf.WriteUint16(1) // EntryCount + bf.WriteUint16(0) // Zeroed + + // Goocoo entry + bf.WriteUint32(2) // Index + for i := 0; i < 22; i++ { + bf.WriteInt16(int16(i + 1)) // Data1 + } + bf.WriteUint32(100) // Data2[0] + bf.WriteUint32(200) // Data2[1] + bf.WriteUint8(5) // Name length + bf.WriteBytes([]byte("Porky")) + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if pkt.AckHandle != 0xAABBCCDD { + t.Errorf("AckHandle = 0x%X, want 0xAABBCCDD", pkt.AckHandle) + } + if pkt.EntryCount != 1 { + t.Errorf("EntryCount = %d, want 1", pkt.EntryCount) + } + if len(pkt.Goocoos) != 1 { + t.Fatalf("len(Goocoos) = %d, want 1", len(pkt.Goocoos)) + } + + g := pkt.Goocoos[0] + if g.Index != 2 { + t.Errorf("Index = %d, want 2", g.Index) + } + if len(g.Data1) != 22 { + t.Fatalf("len(Data1) = %d, want 22", len(g.Data1)) + } + for i := 0; i < 22; i++ { + if g.Data1[i] != int16(i+1) { + t.Errorf("Data1[%d] = %d, want %d", i, g.Data1[i], i+1) + } + } + if len(g.Data2) != 2 { + t.Fatalf("len(Data2) = %d, want 2", len(g.Data2)) + } + if g.Data2[0] != 100 { + t.Errorf("Data2[0] = %d, want 100", g.Data2[0]) + } + if g.Data2[1] != 200 { + t.Errorf("Data2[1] = %d, want 200", g.Data2[1]) + } + if string(g.Name) != "Porky" { + t.Errorf("Name = %q, want %q", string(g.Name), "Porky") + } +} + +func TestMsgMhfUpdateGuacotParse_MultipleEntries(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(3) // EntryCount + bf.WriteUint16(0) // Zeroed + + for idx := uint32(0); idx < 3; idx++ { + bf.WriteUint32(idx) // Index + for i := 0; i < 22; i++ { + bf.WriteInt16(int16(idx*100 + uint32(i))) + } + bf.WriteUint32(idx * 10) // Data2[0] + bf.WriteUint32(idx * 20) // Data2[1] + name := []byte("Pog") + bf.WriteUint8(uint8(len(name))) + bf.WriteBytes(name) + } + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if len(pkt.Goocoos) != 3 { + t.Fatalf("len(Goocoos) = %d, want 3", len(pkt.Goocoos)) + } + for idx := uint32(0); idx < 3; idx++ { + g := pkt.Goocoos[idx] + if g.Index != idx { + t.Errorf("Goocoos[%d].Index = %d, want %d", idx, g.Index, idx) + } + if g.Data1[0] != int16(idx*100) { + t.Errorf("Goocoos[%d].Data1[0] = %d, want %d", idx, g.Data1[0], idx*100) + } + if g.Data2[0] != idx*10 { + t.Errorf("Goocoos[%d].Data2[0] = %d, want %d", idx, g.Data2[0], idx*10) + } + } +} + +func TestMsgMhfUpdateGuacotParse_ZeroEntries(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(42) // AckHandle + bf.WriteUint16(0) // EntryCount + bf.WriteUint16(0) // Zeroed + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if pkt.EntryCount != 0 { + t.Errorf("EntryCount = %d, want 0", pkt.EntryCount) + } + if len(pkt.Goocoos) != 0 { + t.Errorf("len(Goocoos) = %d, want 0", len(pkt.Goocoos)) + } +} + +func TestMsgMhfUpdateGuacotParse_DeletionEntry(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(1) // EntryCount + bf.WriteUint16(0) // Zeroed + + bf.WriteUint32(0) // Index + // Data1[0] = 0 signals deletion + bf.WriteInt16(0) + for i := 1; i < 22; i++ { + bf.WriteInt16(0) + } + bf.WriteUint32(0) // Data2[0] + bf.WriteUint32(0) // Data2[1] + bf.WriteUint8(0) // Empty name + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + g := pkt.Goocoos[0] + if g.Data1[0] != 0 { + t.Errorf("Data1[0] = %d, want 0 (deletion marker)", g.Data1[0]) + } +} + +func TestMsgMhfUpdateGuacotParse_EmptyName(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(1) // EntryCount + bf.WriteUint16(0) // Zeroed + + bf.WriteUint32(0) // Index + for i := 0; i < 22; i++ { + bf.WriteInt16(1) + } + bf.WriteUint32(0) // Data2[0] + bf.WriteUint32(0) // Data2[1] + bf.WriteUint8(0) // Empty name + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if len(pkt.Goocoos[0].Name) != 0 { + t.Errorf("Name length = %d, want 0", len(pkt.Goocoos[0].Name)) + } +} + +func TestMsgMhfEnumerateGuacotParse(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(0) // Unk0 + bf.WriteUint16(0) // Zeroed + + pkt := &MsgMhfEnumerateGuacot{} + bf.Seek(0, 0) + err := pkt.Parse(bf, nil) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.Unk0 != 0 { + t.Errorf("Unk0 = %d, want 0", pkt.Unk0) + } +} + +func TestMsgMhfUpdateGuacotBuild_NotImplemented(t *testing.T) { + pkt := &MsgMhfUpdateGuacot{} + err := pkt.Build(byteframe.NewByteFrame(), nil) + if err == nil { + t.Error("Build() should return error (not implemented)") + } +} + +func TestMsgMhfEnumerateGuacotBuild_NotImplemented(t *testing.T) { + pkt := &MsgMhfEnumerateGuacot{} + err := pkt.Build(byteframe.NewByteFrame(), nil) + if err == nil { + t.Error("Build() should return error (not implemented)") + } +} + +func TestGoocooStruct_Data1Size(t *testing.T) { + // Verify 22 int16 entries = 44 bytes of outfit/appearance data + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) // AckHandle + bf.WriteUint16(1) // EntryCount + bf.WriteUint16(0) // Zeroed + + bf.WriteUint32(0) // Index + for i := 0; i < 22; i++ { + bf.WriteInt16(int16(i * 3)) + } + bf.WriteUint32(0xDEAD) // Data2[0] + bf.WriteUint32(0xBEEF) // Data2[1] + bf.WriteUint8(0) // No name + + pkt := &MsgMhfUpdateGuacot{} + bf.Seek(0, 0) + _ = pkt.Parse(bf, nil) + + g := pkt.Goocoos[0] + + // Verify all 22 data slots are correctly read + for i := 0; i < 22; i++ { + expected := int16(i * 3) + if g.Data1[i] != expected { + t.Errorf("Data1[%d] = %d, want %d", i, g.Data1[i], expected) + } + } + + if g.Data2[0] != 0xDEAD { + t.Errorf("Data2[0] = 0x%X, want 0xDEAD", g.Data2[0]) + } + if g.Data2[1] != 0xBEEF { + t.Errorf("Data2[1] = 0x%X, want 0xBEEF", g.Data2[1]) + } +} + +func TestGoocooSerialization_Roundtrip(t *testing.T) { + // Simulate what handleMsgMhfUpdateGuacot does when saving to DB + goocoo := Goocoo{ + Index: 1, + Data1: make([]int16, 22), + Data2: []uint32{0x1234, 0x5678}, + Name: []byte("MyPoogie"), + } + goocoo.Data1[0] = 5 // outfit type (non-zero = exists) + goocoo.Data1[1] = 100 // some appearance data + goocoo.Data1[21] = -50 // test negative int16 + + // Serialize (matches handler logic) + bf := byteframe.NewByteFrame() + bf.WriteUint32(goocoo.Index) + for i := range goocoo.Data1 { + bf.WriteInt16(goocoo.Data1[i]) + } + for i := range goocoo.Data2 { + bf.WriteUint32(goocoo.Data2[i]) + } + bf.WriteUint8(uint8(len(goocoo.Name))) + bf.WriteBytes(goocoo.Name) + + // Deserialize and verify + data := bf.Data() + rbf := byteframe.NewByteFrameFromBytes(data) + + index := rbf.ReadUint32() + if index != 1 { + t.Errorf("index = %d, want 1", index) + } + + data1_0 := rbf.ReadInt16() + if data1_0 != 5 { + t.Errorf("data1[0] = %d, want 5", data1_0) + } + data1_1 := rbf.ReadInt16() + if data1_1 != 100 { + t.Errorf("data1[1] = %d, want 100", data1_1) + } + // Skip to data1[21] + for i := 2; i < 21; i++ { + rbf.ReadInt16() + } + data1_21 := rbf.ReadInt16() + if data1_21 != -50 { + t.Errorf("data1[21] = %d, want -50", data1_21) + } + + d2_0 := rbf.ReadUint32() + if d2_0 != 0x1234 { + t.Errorf("data2[0] = 0x%X, want 0x1234", d2_0) + } + d2_1 := rbf.ReadUint32() + if d2_1 != 0x5678 { + t.Errorf("data2[1] = 0x%X, want 0x5678", d2_1) + } + + nameLen := rbf.ReadUint8() + if nameLen != 8 { + t.Errorf("nameLen = %d, want 8", nameLen) + } + name := rbf.ReadBytes(uint(nameLen)) + if string(name) != "MyPoogie" { + t.Errorf("name = %q, want %q", string(name), "MyPoogie") + } +} + +func TestGoocooEntrySize(t *testing.T) { + // Each goocoo entry in the packet should be: + // 4 (index) + 22*2 (data1) + 2*4 (data2) + 1 (name len) + N (name) + // = 4 + 44 + 8 + 1 + N = 57 + N bytes + name := []byte("Test") + expectedSize := 4 + 44 + 8 + 1 + len(name) + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) // index + for i := 0; i < 22; i++ { + bf.WriteInt16(0) + } + bf.WriteUint32(0) // data2[0] + bf.WriteUint32(0) // data2[1] + bf.WriteUint8(uint8(len(name))) // name len + bf.WriteBytes(name) + + if len(bf.Data()) != expectedSize { + t.Errorf("entry size = %d bytes, want %d bytes (57 + %d name)", len(bf.Data()), expectedSize, len(name)) + } +} diff --git a/network/mhfpacket/msg_mhf_packets_test.go b/network/mhfpacket/msg_mhf_packets_test.go new file mode 100644 index 000000000..a666d1797 --- /dev/null +++ b/network/mhfpacket/msg_mhf_packets_test.go @@ -0,0 +1,537 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +// TestMsgMhfSavedataParse tests parsing MsgMhfSavedata +func TestMsgMhfSavedataParse(t *testing.T) { + pkt := FromOpcode(network.MSG_MHF_SAVEDATA) + if pkt == nil { + t.Fatal("FromOpcode(MSG_MHF_SAVEDATA) returned nil") + } + if pkt.Opcode() != network.MSG_MHF_SAVEDATA { + t.Errorf("Opcode() = %s, want MSG_MHF_SAVEDATA", pkt.Opcode()) + } +} + +// TestMsgMhfLoaddataParse tests parsing MsgMhfLoaddata +func TestMsgMhfLoaddataParse(t *testing.T) { + pkt := FromOpcode(network.MSG_MHF_LOADDATA) + if pkt == nil { + t.Fatal("FromOpcode(MSG_MHF_LOADDATA) returned nil") + } + if pkt.Opcode() != network.MSG_MHF_LOADDATA { + t.Errorf("Opcode() = %s, want MSG_MHF_LOADDATA", pkt.Opcode()) + } +} + +// TestMsgMhfListMemberOpcode tests MsgMhfListMember Opcode +func TestMsgMhfListMemberOpcode(t *testing.T) { + pkt := &MsgMhfListMember{} + if pkt.Opcode() != network.MSG_MHF_LIST_MEMBER { + t.Errorf("Opcode() = %s, want MSG_MHF_LIST_MEMBER", pkt.Opcode()) + } +} + +// TestMsgMhfOprMemberOpcode tests MsgMhfOprMember Opcode +func TestMsgMhfOprMemberOpcode(t *testing.T) { + pkt := &MsgMhfOprMember{} + if pkt.Opcode() != network.MSG_MHF_OPR_MEMBER { + t.Errorf("Opcode() = %s, want MSG_MHF_OPR_MEMBER", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateDistItemOpcode tests MsgMhfEnumerateDistItem Opcode +func TestMsgMhfEnumerateDistItemOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateDistItem{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_DIST_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_DIST_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfApplyDistItemOpcode tests MsgMhfApplyDistItem Opcode +func TestMsgMhfApplyDistItemOpcode(t *testing.T) { + pkt := &MsgMhfApplyDistItem{} + if pkt.Opcode() != network.MSG_MHF_APPLY_DIST_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_APPLY_DIST_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfAcquireDistItemOpcode tests MsgMhfAcquireDistItem Opcode +func TestMsgMhfAcquireDistItemOpcode(t *testing.T) { + pkt := &MsgMhfAcquireDistItem{} + if pkt.Opcode() != network.MSG_MHF_ACQUIRE_DIST_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ACQUIRE_DIST_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfGetDistDescriptionOpcode tests MsgMhfGetDistDescription Opcode +func TestMsgMhfGetDistDescriptionOpcode(t *testing.T) { + pkt := &MsgMhfGetDistDescription{} + if pkt.Opcode() != network.MSG_MHF_GET_DIST_DESCRIPTION { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_DIST_DESCRIPTION", pkt.Opcode()) + } +} + +// TestMsgMhfSendMailOpcode tests MsgMhfSendMail Opcode +func TestMsgMhfSendMailOpcode(t *testing.T) { + pkt := &MsgMhfSendMail{} + if pkt.Opcode() != network.MSG_MHF_SEND_MAIL { + t.Errorf("Opcode() = %s, want MSG_MHF_SEND_MAIL", pkt.Opcode()) + } +} + +// TestMsgMhfReadMailOpcode tests MsgMhfReadMail Opcode +func TestMsgMhfReadMailOpcode(t *testing.T) { + pkt := &MsgMhfReadMail{} + if pkt.Opcode() != network.MSG_MHF_READ_MAIL { + t.Errorf("Opcode() = %s, want MSG_MHF_READ_MAIL", pkt.Opcode()) + } +} + +// TestMsgMhfListMailOpcode tests MsgMhfListMail Opcode +func TestMsgMhfListMailOpcode(t *testing.T) { + pkt := &MsgMhfListMail{} + if pkt.Opcode() != network.MSG_MHF_LIST_MAIL { + t.Errorf("Opcode() = %s, want MSG_MHF_LIST_MAIL", pkt.Opcode()) + } +} + +// TestMsgMhfOprtMailOpcode tests MsgMhfOprtMail Opcode +func TestMsgMhfOprtMailOpcode(t *testing.T) { + pkt := &MsgMhfOprtMail{} + if pkt.Opcode() != network.MSG_MHF_OPRT_MAIL { + t.Errorf("Opcode() = %s, want MSG_MHF_OPRT_MAIL", pkt.Opcode()) + } +} + +// TestMsgMhfLoadFavoriteQuestOpcode tests MsgMhfLoadFavoriteQuest Opcode +func TestMsgMhfLoadFavoriteQuestOpcode(t *testing.T) { + pkt := &MsgMhfLoadFavoriteQuest{} + if pkt.Opcode() != network.MSG_MHF_LOAD_FAVORITE_QUEST { + t.Errorf("Opcode() = %s, want MSG_MHF_LOAD_FAVORITE_QUEST", pkt.Opcode()) + } +} + +// TestMsgMhfSaveFavoriteQuestOpcode tests MsgMhfSaveFavoriteQuest Opcode +func TestMsgMhfSaveFavoriteQuestOpcode(t *testing.T) { + pkt := &MsgMhfSaveFavoriteQuest{} + if pkt.Opcode() != network.MSG_MHF_SAVE_FAVORITE_QUEST { + t.Errorf("Opcode() = %s, want MSG_MHF_SAVE_FAVORITE_QUEST", pkt.Opcode()) + } +} + +// TestMsgMhfRegisterEventOpcode tests MsgMhfRegisterEvent Opcode +func TestMsgMhfRegisterEventOpcode(t *testing.T) { + pkt := &MsgMhfRegisterEvent{} + if pkt.Opcode() != network.MSG_MHF_REGISTER_EVENT { + t.Errorf("Opcode() = %s, want MSG_MHF_REGISTER_EVENT", pkt.Opcode()) + } +} + +// TestMsgMhfReleaseEventOpcode tests MsgMhfReleaseEvent Opcode +func TestMsgMhfReleaseEventOpcode(t *testing.T) { + pkt := &MsgMhfReleaseEvent{} + if pkt.Opcode() != network.MSG_MHF_RELEASE_EVENT { + t.Errorf("Opcode() = %s, want MSG_MHF_RELEASE_EVENT", pkt.Opcode()) + } +} + +// TestMsgMhfTransitMessageOpcode tests MsgMhfTransitMessage Opcode +func TestMsgMhfTransitMessageOpcode(t *testing.T) { + pkt := &MsgMhfTransitMessage{} + if pkt.Opcode() != network.MSG_MHF_TRANSIT_MESSAGE { + t.Errorf("Opcode() = %s, want MSG_MHF_TRANSIT_MESSAGE", pkt.Opcode()) + } +} + +// TestMsgMhfPresentBoxOpcode tests MsgMhfPresentBox Opcode +func TestMsgMhfPresentBoxOpcode(t *testing.T) { + pkt := &MsgMhfPresentBox{} + if pkt.Opcode() != network.MSG_MHF_PRESENT_BOX { + t.Errorf("Opcode() = %s, want MSG_MHF_PRESENT_BOX", pkt.Opcode()) + } +} + +// TestMsgMhfServerCommandOpcode tests MsgMhfServerCommand Opcode +func TestMsgMhfServerCommandOpcode(t *testing.T) { + pkt := &MsgMhfServerCommand{} + if pkt.Opcode() != network.MSG_MHF_SERVER_COMMAND { + t.Errorf("Opcode() = %s, want MSG_MHF_SERVER_COMMAND", pkt.Opcode()) + } +} + +// TestMsgMhfShutClientOpcode tests MsgMhfShutClient Opcode +func TestMsgMhfShutClientOpcode(t *testing.T) { + pkt := &MsgMhfShutClient{} + if pkt.Opcode() != network.MSG_MHF_SHUT_CLIENT { + t.Errorf("Opcode() = %s, want MSG_MHF_SHUT_CLIENT", pkt.Opcode()) + } +} + +// TestMsgMhfAnnounceOpcode tests MsgMhfAnnounce Opcode +func TestMsgMhfAnnounceOpcode(t *testing.T) { + pkt := &MsgMhfAnnounce{} + if pkt.Opcode() != network.MSG_MHF_ANNOUNCE { + t.Errorf("Opcode() = %s, want MSG_MHF_ANNOUNCE", pkt.Opcode()) + } +} + +// TestMsgMhfSetLoginwindowOpcode tests MsgMhfSetLoginwindow Opcode +func TestMsgMhfSetLoginwindowOpcode(t *testing.T) { + pkt := &MsgMhfSetLoginwindow{} + if pkt.Opcode() != network.MSG_MHF_SET_LOGINWINDOW { + t.Errorf("Opcode() = %s, want MSG_MHF_SET_LOGINWINDOW", pkt.Opcode()) + } +} + +// TestMsgMhfGetCaUniqueIDOpcode tests MsgMhfGetCaUniqueID Opcode +func TestMsgMhfGetCaUniqueIDOpcode(t *testing.T) { + pkt := &MsgMhfGetCaUniqueID{} + if pkt.Opcode() != network.MSG_MHF_GET_CA_UNIQUE_ID { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_CA_UNIQUE_ID", pkt.Opcode()) + } +} + +// TestMsgMhfSetCaAchievementOpcode tests MsgMhfSetCaAchievement Opcode +func TestMsgMhfSetCaAchievementOpcode(t *testing.T) { + pkt := &MsgMhfSetCaAchievement{} + if pkt.Opcode() != network.MSG_MHF_SET_CA_ACHIEVEMENT { + t.Errorf("Opcode() = %s, want MSG_MHF_SET_CA_ACHIEVEMENT", pkt.Opcode()) + } +} + +// TestMsgMhfCaravanMyScoreOpcode tests MsgMhfCaravanMyScore Opcode +func TestMsgMhfCaravanMyScoreOpcode(t *testing.T) { + pkt := &MsgMhfCaravanMyScore{} + if pkt.Opcode() != network.MSG_MHF_CARAVAN_MY_SCORE { + t.Errorf("Opcode() = %s, want MSG_MHF_CARAVAN_MY_SCORE", pkt.Opcode()) + } +} + +// TestMsgMhfCaravanRankingOpcode tests MsgMhfCaravanRanking Opcode +func TestMsgMhfCaravanRankingOpcode(t *testing.T) { + pkt := &MsgMhfCaravanRanking{} + if pkt.Opcode() != network.MSG_MHF_CARAVAN_RANKING { + t.Errorf("Opcode() = %s, want MSG_MHF_CARAVAN_RANKING", pkt.Opcode()) + } +} + +// TestMsgMhfCaravanMyRankOpcode tests MsgMhfCaravanMyRank Opcode +func TestMsgMhfCaravanMyRankOpcode(t *testing.T) { + pkt := &MsgMhfCaravanMyRank{} + if pkt.Opcode() != network.MSG_MHF_CARAVAN_MY_RANK { + t.Errorf("Opcode() = %s, want MSG_MHF_CARAVAN_MY_RANK", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateQuestOpcode tests MsgMhfEnumerateQuest Opcode +func TestMsgMhfEnumerateQuestOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateQuest{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_QUEST { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_QUEST", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateEventOpcode tests MsgMhfEnumerateEvent Opcode +func TestMsgMhfEnumerateEventOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateEvent{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_EVENT { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_EVENT", pkt.Opcode()) + } +} + +// TestMsgMhfEnumeratePriceOpcode tests MsgMhfEnumeratePrice Opcode +func TestMsgMhfEnumeratePriceOpcode(t *testing.T) { + pkt := &MsgMhfEnumeratePrice{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_PRICE { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_PRICE", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateRankingOpcode tests MsgMhfEnumerateRanking Opcode +func TestMsgMhfEnumerateRankingOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateRanking{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_RANKING { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_RANKING", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateOrderOpcode tests MsgMhfEnumerateOrder Opcode +func TestMsgMhfEnumerateOrderOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateOrder{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_ORDER { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_ORDER", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateShopOpcode tests MsgMhfEnumerateShop Opcode +func TestMsgMhfEnumerateShopOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateShop{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_SHOP { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_SHOP", pkt.Opcode()) + } +} + +// TestMsgMhfGetExtraInfoOpcode tests MsgMhfGetExtraInfo Opcode +func TestMsgMhfGetExtraInfoOpcode(t *testing.T) { + pkt := &MsgMhfGetExtraInfo{} + if pkt.Opcode() != network.MSG_MHF_GET_EXTRA_INFO { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_EXTRA_INFO", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateItemOpcode tests MsgMhfEnumerateItem Opcode +func TestMsgMhfEnumerateItemOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateItem{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfAcquireItemOpcode tests MsgMhfAcquireItem Opcode +func TestMsgMhfAcquireItemOpcode(t *testing.T) { + pkt := &MsgMhfAcquireItem{} + if pkt.Opcode() != network.MSG_MHF_ACQUIRE_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ACQUIRE_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfTransferItemOpcode tests MsgMhfTransferItem Opcode +func TestMsgMhfTransferItemOpcode(t *testing.T) { + pkt := &MsgMhfTransferItem{} + if pkt.Opcode() != network.MSG_MHF_TRANSFER_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_TRANSFER_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfEntryRookieGuildOpcode tests MsgMhfEntryRookieGuild Opcode +func TestMsgMhfEntryRookieGuildOpcode(t *testing.T) { + pkt := &MsgMhfEntryRookieGuild{} + if pkt.Opcode() != network.MSG_MHF_ENTRY_ROOKIE_GUILD { + t.Errorf("Opcode() = %s, want MSG_MHF_ENTRY_ROOKIE_GUILD", pkt.Opcode()) + } +} + +// TestMsgCaExchangeItemOpcode tests MsgCaExchangeItem Opcode +func TestMsgCaExchangeItemOpcode(t *testing.T) { + pkt := &MsgCaExchangeItem{} + if pkt.Opcode() != network.MSG_CA_EXCHANGE_ITEM { + t.Errorf("Opcode() = %s, want MSG_CA_EXCHANGE_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateCampaignOpcode tests MsgMhfEnumerateCampaign Opcode +func TestMsgMhfEnumerateCampaignOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateCampaign{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_CAMPAIGN { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_CAMPAIGN", pkt.Opcode()) + } +} + +// TestMsgMhfStateCampaignOpcode tests MsgMhfStateCampaign Opcode +func TestMsgMhfStateCampaignOpcode(t *testing.T) { + pkt := &MsgMhfStateCampaign{} + if pkt.Opcode() != network.MSG_MHF_STATE_CAMPAIGN { + t.Errorf("Opcode() = %s, want MSG_MHF_STATE_CAMPAIGN", pkt.Opcode()) + } +} + +// TestMsgMhfApplyCampaignOpcode tests MsgMhfApplyCampaign Opcode +func TestMsgMhfApplyCampaignOpcode(t *testing.T) { + pkt := &MsgMhfApplyCampaign{} + if pkt.Opcode() != network.MSG_MHF_APPLY_CAMPAIGN { + t.Errorf("Opcode() = %s, want MSG_MHF_APPLY_CAMPAIGN", pkt.Opcode()) + } +} + +// TestMsgMhfCreateJointOpcode tests MsgMhfCreateJoint Opcode +func TestMsgMhfCreateJointOpcode(t *testing.T) { + pkt := &MsgMhfCreateJoint{} + if pkt.Opcode() != network.MSG_MHF_CREATE_JOINT { + t.Errorf("Opcode() = %s, want MSG_MHF_CREATE_JOINT", pkt.Opcode()) + } +} + +// TestMsgMhfOperateJointOpcode tests MsgMhfOperateJoint Opcode +func TestMsgMhfOperateJointOpcode(t *testing.T) { + pkt := &MsgMhfOperateJoint{} + if pkt.Opcode() != network.MSG_MHF_OPERATE_JOINT { + t.Errorf("Opcode() = %s, want MSG_MHF_OPERATE_JOINT", pkt.Opcode()) + } +} + +// TestMsgMhfInfoJointOpcode tests MsgMhfInfoJoint Opcode +func TestMsgMhfInfoJointOpcode(t *testing.T) { + pkt := &MsgMhfInfoJoint{} + if pkt.Opcode() != network.MSG_MHF_INFO_JOINT { + t.Errorf("Opcode() = %s, want MSG_MHF_INFO_JOINT", pkt.Opcode()) + } +} + +// TestMsgMhfGetCogInfoOpcode tests MsgMhfGetCogInfo Opcode +func TestMsgMhfGetCogInfoOpcode(t *testing.T) { + pkt := &MsgMhfGetCogInfo{} + if pkt.Opcode() != network.MSG_MHF_GET_COG_INFO { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_COG_INFO", pkt.Opcode()) + } +} + +// TestMsgMhfCheckMonthlyItemOpcode tests MsgMhfCheckMonthlyItem Opcode +func TestMsgMhfCheckMonthlyItemOpcode(t *testing.T) { + pkt := &MsgMhfCheckMonthlyItem{} + if pkt.Opcode() != network.MSG_MHF_CHECK_MONTHLY_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_CHECK_MONTHLY_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfAcquireMonthlyItemOpcode tests MsgMhfAcquireMonthlyItem Opcode +func TestMsgMhfAcquireMonthlyItemOpcode(t *testing.T) { + pkt := &MsgMhfAcquireMonthlyItem{} + if pkt.Opcode() != network.MSG_MHF_ACQUIRE_MONTHLY_ITEM { + t.Errorf("Opcode() = %s, want MSG_MHF_ACQUIRE_MONTHLY_ITEM", pkt.Opcode()) + } +} + +// TestMsgMhfCheckWeeklyStampOpcode tests MsgMhfCheckWeeklyStamp Opcode +func TestMsgMhfCheckWeeklyStampOpcode(t *testing.T) { + pkt := &MsgMhfCheckWeeklyStamp{} + if pkt.Opcode() != network.MSG_MHF_CHECK_WEEKLY_STAMP { + t.Errorf("Opcode() = %s, want MSG_MHF_CHECK_WEEKLY_STAMP", pkt.Opcode()) + } +} + +// TestMsgMhfExchangeWeeklyStampOpcode tests MsgMhfExchangeWeeklyStamp Opcode +func TestMsgMhfExchangeWeeklyStampOpcode(t *testing.T) { + pkt := &MsgMhfExchangeWeeklyStamp{} + if pkt.Opcode() != network.MSG_MHF_EXCHANGE_WEEKLY_STAMP { + t.Errorf("Opcode() = %s, want MSG_MHF_EXCHANGE_WEEKLY_STAMP", pkt.Opcode()) + } +} + +// TestMsgMhfCreateMercenaryOpcode tests MsgMhfCreateMercenary Opcode +func TestMsgMhfCreateMercenaryOpcode(t *testing.T) { + pkt := &MsgMhfCreateMercenary{} + if pkt.Opcode() != network.MSG_MHF_CREATE_MERCENARY { + t.Errorf("Opcode() = %s, want MSG_MHF_CREATE_MERCENARY", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateMercenaryLogOpcode tests MsgMhfEnumerateMercenaryLog Opcode +func TestMsgMhfEnumerateMercenaryLogOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateMercenaryLog{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_MERCENARY_LOG { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_MERCENARY_LOG", pkt.Opcode()) + } +} + +// TestMsgMhfEnumerateGuacotOpcode tests MsgMhfEnumerateGuacot Opcode +func TestMsgMhfEnumerateGuacotOpcode(t *testing.T) { + pkt := &MsgMhfEnumerateGuacot{} + if pkt.Opcode() != network.MSG_MHF_ENUMERATE_GUACOT { + t.Errorf("Opcode() = %s, want MSG_MHF_ENUMERATE_GUACOT", pkt.Opcode()) + } +} + +// TestMsgMhfUpdateGuacotOpcode tests MsgMhfUpdateGuacot Opcode +func TestMsgMhfUpdateGuacotOpcode(t *testing.T) { + pkt := &MsgMhfUpdateGuacot{} + if pkt.Opcode() != network.MSG_MHF_UPDATE_GUACOT { + t.Errorf("Opcode() = %s, want MSG_MHF_UPDATE_GUACOT", pkt.Opcode()) + } +} + +// TestMsgMhfEnterTournamentQuestOpcode tests MsgMhfEnterTournamentQuest Opcode +func TestMsgMhfEnterTournamentQuestOpcode(t *testing.T) { + pkt := &MsgMhfEnterTournamentQuest{} + if pkt.Opcode() != network.MSG_MHF_ENTER_TOURNAMENT_QUEST { + t.Errorf("Opcode() = %s, want MSG_MHF_ENTER_TOURNAMENT_QUEST", pkt.Opcode()) + } +} + +// TestMsgMhfResetAchievementOpcode tests MsgMhfResetAchievement Opcode +func TestMsgMhfResetAchievementOpcode(t *testing.T) { + pkt := &MsgMhfResetAchievement{} + if pkt.Opcode() != network.MSG_MHF_RESET_ACHIEVEMENT { + t.Errorf("Opcode() = %s, want MSG_MHF_RESET_ACHIEVEMENT", pkt.Opcode()) + } +} + +// TestMsgMhfPaymentAchievementOpcode tests MsgMhfPaymentAchievement Opcode +func TestMsgMhfPaymentAchievementOpcode(t *testing.T) { + pkt := &MsgMhfPaymentAchievement{} + if pkt.Opcode() != network.MSG_MHF_PAYMENT_ACHIEVEMENT { + t.Errorf("Opcode() = %s, want MSG_MHF_PAYMENT_ACHIEVEMENT", pkt.Opcode()) + } +} + +// TestMsgMhfDisplayedAchievementOpcode tests MsgMhfDisplayedAchievement Opcode +func TestMsgMhfDisplayedAchievementOpcode(t *testing.T) { + pkt := &MsgMhfDisplayedAchievement{} + if pkt.Opcode() != network.MSG_MHF_DISPLAYED_ACHIEVEMENT { + t.Errorf("Opcode() = %s, want MSG_MHF_DISPLAYED_ACHIEVEMENT", pkt.Opcode()) + } +} + +// TestMsgMhfGetBbsSnsStatusOpcode tests MsgMhfGetBbsSnsStatus Opcode +func TestMsgMhfGetBbsSnsStatusOpcode(t *testing.T) { + pkt := &MsgMhfGetBbsSnsStatus{} + if pkt.Opcode() != network.MSG_MHF_GET_BBS_SNS_STATUS { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_BBS_SNS_STATUS", pkt.Opcode()) + } +} + +// TestMsgMhfApplyBbsArticleOpcode tests MsgMhfApplyBbsArticle Opcode +func TestMsgMhfApplyBbsArticleOpcode(t *testing.T) { + pkt := &MsgMhfApplyBbsArticle{} + if pkt.Opcode() != network.MSG_MHF_APPLY_BBS_ARTICLE { + t.Errorf("Opcode() = %s, want MSG_MHF_APPLY_BBS_ARTICLE", pkt.Opcode()) + } +} + +// TestMsgMhfGetEtcPointsOpcode tests MsgMhfGetEtcPoints Opcode +func TestMsgMhfGetEtcPointsOpcode(t *testing.T) { + pkt := &MsgMhfGetEtcPoints{} + if pkt.Opcode() != network.MSG_MHF_GET_ETC_POINTS { + t.Errorf("Opcode() = %s, want MSG_MHF_GET_ETC_POINTS", pkt.Opcode()) + } +} + +// TestMsgMhfUpdateEtcPointOpcode tests MsgMhfUpdateEtcPoint Opcode +func TestMsgMhfUpdateEtcPointOpcode(t *testing.T) { + pkt := &MsgMhfUpdateEtcPoint{} + if pkt.Opcode() != network.MSG_MHF_UPDATE_ETC_POINT { + t.Errorf("Opcode() = %s, want MSG_MHF_UPDATE_ETC_POINT", pkt.Opcode()) + } +} + +// TestAchievementPacketParse tests simple achievement packet parsing +func TestAchievementPacketParse(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(5) // AchievementID + bf.WriteUint16(100) // Unk1 + bf.WriteUint16(200) // Unk2 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAddAchievement{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AchievementID != 5 { + t.Errorf("AchievementID = %d, want 5", pkt.AchievementID) + } + if pkt.Unk1 != 100 { + t.Errorf("Unk1 = %d, want 100", pkt.Unk1) + } + if pkt.Unk2 != 200 { + t.Errorf("Unk2 = %d, want 200", pkt.Unk2) + } +} 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..4b432369e --- /dev/null +++ b/network/mhfpacket/msg_parse_large_test.go @@ -0,0 +1,880 @@ +package mhfpacket + +import ( + "bytes" + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// 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 != 0 { + t.Errorf("BoxType = %d, want 0", pkt.BoxType) + } + 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 != 1 { + t.Errorf("BoxType = %d, want 1", pkt.BoxType) + } + if pkt.Name != "Arms" { + t.Errorf("Name = %q, want %q", pkt.Name, "Arms") + } +} + +// 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 len(pkt.Souls) != 3 { + t.Fatalf("Souls len = %d, want 3", len(pkt.Souls)) + } + expectedSouls := []uint16{10, 20, 30} + for i, v := range expectedSouls { + if pkt.Souls[i] != v { + t.Errorf("Souls[%d] = %d, want %d", i, pkt.Souls[i], v) + } + } +} + +// 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 len(pkt.Souls) != 0 { + t.Errorf("Souls len = %d, want 0", len(pkt.Souls)) + } +} + +// TestParseLargeMsgMhfOperateJoint tests Parse for MsgMhfOperateJoint. +// Parse reads: uint32 AckHandle, uint32 AllianceID, uint32 GuildID, uint8 Action, +// uint8 dataLen, 4 bytes Data1, dataLen bytes Data2. +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 + bf.WriteUint8(3) // dataLen = 3 + bf.WriteBytes([]byte{0xAA, 0xBB, 0xCC, 0xDD}) // Data1 (always 4 bytes) + bf.WriteBytes([]byte{0x01, 0x02, 0x03}) // Data2 (dataLen bytes) + 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.Data1 == nil { + t.Fatal("Data1 is nil") + } + if pkt.Data2 == nil { + t.Fatal("Data2 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.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 = OperateGuildUpdateComment + 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 != OperateGuildUpdateComment { + t.Errorf("Action = %d, want %d", pkt.Action, OperateGuildUpdateComment) + } + 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) + } + } +} + +// 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") + } +} + +// 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.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..deaf2a604 --- /dev/null +++ b/network/mhfpacket/msg_parse_medium_test.go @@ -0,0 +1,776 @@ +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), FestaID(u32), GuildID(u32), TrialID(u32) +func TestParseMediumVoteFesta(t *testing.T) { + tests := []struct { + name string + ack uint32 + festaID 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.festaID) + 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.FestaID != tt.festaID { + t.Errorf("FestaID = %d, want %d", pkt.FestaID, tt.festaID) + } + 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.Data) != 0x16A { + t.Errorf("Unk0 length = %d, want %d", len(pkt.Data), 0x16A) + } + if !bytes.Equal(pkt.Data, 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.Data) != 0x16A { + t.Errorf("Unk0 length = %d, want %d", len(pkt.Data), 0x16A) + } + }) +} + + +// --- 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.SemaphoreID != ack { + t.Errorf("SemaphoreID = 0x%X, want 0x%X", pkt.SemaphoreID, 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..32e4db358 --- /dev/null +++ b/network/mhfpacket/msg_parse_small_test.go @@ -0,0 +1,216 @@ +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{}}, + {"MsgMhfDebugPostValue", &MsgMhfDebugPostValue{}}, + {"MsgMhfEnterTournamentQuest", &MsgMhfEnterTournamentQuest{}}, + {"MsgMhfGetCaAchievementHist", &MsgMhfGetCaAchievementHist{}}, + {"MsgMhfGetCaUniqueID", &MsgMhfGetCaUniqueID{}}, + {"MsgMhfGetDailyMissionMaster", &MsgMhfGetDailyMissionMaster{}}, + {"MsgMhfGetDailyMissionPersonal", &MsgMhfGetDailyMissionPersonal{}}, + {"MsgMhfGetExtraInfo", &MsgMhfGetExtraInfo{}}, + {"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}}, + {"MsgMhfKickExportForce", &MsgMhfKickExportForce{}}, + {"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}}, + {"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}}, + {"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}}, + {"MsgMhfResetAchievement", &MsgMhfResetAchievement{}}, + {"MsgMhfResetTitle", &MsgMhfResetTitle{}}, + {"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}}, + {"MsgMhfSetDailyMissionPersonal", &MsgMhfSetDailyMissionPersonal{}}, + {"MsgMhfSetUdTacticsFollower", &MsgMhfSetUdTacticsFollower{}}, + {"MsgMhfStampcardPrize", &MsgMhfStampcardPrize{}}, + {"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{}}, + {"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.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.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/network/mhfpacket/msg_parse_test.go b/network/mhfpacket/msg_parse_test.go new file mode 100644 index 000000000..6d153bdcb --- /dev/null +++ b/network/mhfpacket/msg_parse_test.go @@ -0,0 +1,218 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/clientctx" +) + +// TestMsgMhfGetAchievementParse tests MsgMhfGetAchievement parsing +func TestMsgMhfGetAchievementDetailedParse(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(0x12345678) // AckHandle + bf.WriteUint32(54321) // CharID + bf.WriteUint32(99999) // Unk1 + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfGetAchievement{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != 0x12345678 { + t.Errorf("AckHandle = 0x%X, want 0x12345678", pkt.AckHandle) + } + if pkt.CharID != 54321 { + t.Errorf("CharID = %d, want 54321", pkt.CharID) + } +} + +// TestMsgMhfAddAchievementDetailedParse tests MsgMhfAddAchievement parsing +func TestMsgMhfAddAchievementDetailedParse(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint8(42) // AchievementID + bf.WriteUint16(12345) // Unk1 + bf.WriteUint16(0xFFFF) // Unk2 - max value + bf.Seek(0, io.SeekStart) + + pkt := &MsgMhfAddAchievement{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AchievementID != 42 { + t.Errorf("AchievementID = %d, want 42", pkt.AchievementID) + } + if pkt.Unk1 != 12345 { + t.Errorf("Unk1 = %d, want 12345", pkt.Unk1) + } + if pkt.Unk2 != 0xFFFF { + t.Errorf("Unk2 = %d, want 65535", pkt.Unk2) + } +} + +// TestMsgSysCastBinaryDetailedParse tests MsgSysCastBinary parsing with various payloads +func TestMsgSysCastBinaryDetailedParse(t *testing.T) { + tests := []struct { + name string + unk uint32 + broadcastType uint8 + messageType uint8 + payload []byte + }{ + {"empty payload", 0, 1, 2, []byte{}}, + {"typical payload", 0x006400C8, 0x10, 0x20, []byte{0x01, 0x02, 0x03}}, + {"chat message", 0, 0x01, 0x01, []byte("Hello, World!")}, + {"binary data", 0xFFFFFFFF, 0xFF, 0xFF, []byte{0xDE, 0xAD, 0xBE, 0xEF}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.unk) + bf.WriteUint8(tt.broadcastType) + bf.WriteUint8(tt.messageType) + bf.WriteUint16(uint16(len(tt.payload))) + bf.WriteBytes(tt.payload) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCastBinary{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.Unk != tt.unk { + t.Errorf("Unk = %d, want %d", pkt.Unk, tt.unk) + } + if pkt.BroadcastType != tt.broadcastType { + t.Errorf("BroadcastType = %d, want %d", pkt.BroadcastType, tt.broadcastType) + } + if pkt.MessageType != tt.messageType { + t.Errorf("MessageType = %d, want %d", pkt.MessageType, tt.messageType) + } + if len(pkt.RawDataPayload) != len(tt.payload) { + t.Errorf("RawDataPayload len = %d, want %d", len(pkt.RawDataPayload), len(tt.payload)) + } + }) + } +} + +// TestMsgSysLogoutParse tests MsgSysLogout parsing +func TestMsgSysLogoutDetailedParse(t *testing.T) { + tests := []struct { + unk0 uint8 + }{ + {0}, + {1}, + {100}, + {255}, + } + + for _, tt := range tests { + bf := byteframe.NewByteFrame() + bf.WriteUint8(tt.unk0) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLogout{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + } +} + +// TestMsgSysBackStageParse tests MsgSysBackStage parsing +func TestMsgSysBackStageDetailedParse(t *testing.T) { + tests := []struct { + ackHandle uint32 + }{ + {0}, + {1}, + {0x12345678}, + {0xFFFFFFFF}, + } + + for _, tt := range tests { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysBackStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if 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) + } + } +} + +// TestMsgSysPingParse tests MsgSysPing parsing +func TestMsgSysPingDetailedParse(t *testing.T) { + tests := []struct { + ackHandle uint32 + }{ + {0}, + {0xABCDEF12}, + {0xFFFFFFFF}, + } + + for _, tt := range tests { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysPing{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if 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) + } + } +} + +// TestMsgSysTimeParse tests MsgSysTime parsing +func TestMsgSysTimeDetailedParse(t *testing.T) { + tests := []struct { + getRemoteTime bool + timestamp uint32 + }{ + {false, 0}, + {true, 1577836800}, // 2020-01-01 00:00:00 + {false, 0xFFFFFFFF}, + } + + for _, tt := range tests { + bf := byteframe.NewByteFrame() + bf.WriteBool(tt.getRemoteTime) + bf.WriteUint32(tt.timestamp) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysTime{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.GetRemoteTime != tt.getRemoteTime { + t.Errorf("GetRemoteTime = %v, want %v", pkt.GetRemoteTime, tt.getRemoteTime) + } + if pkt.Timestamp != tt.timestamp { + t.Errorf("Timestamp = %d, want %d", pkt.Timestamp, tt.timestamp) + } + } +} diff --git a/network/mhfpacket/msg_sys_core_test.go b/network/mhfpacket/msg_sys_core_test.go new file mode 100644 index 000000000..29e568a64 --- /dev/null +++ b/network/mhfpacket/msg_sys_core_test.go @@ -0,0 +1,310 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func TestMsgSysAckRoundTrip(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + isBufferResponse bool + errorCode uint8 + ackData []byte + }{ + { + name: "simple non-buffer response", + ackHandle: 1, + isBufferResponse: false, + errorCode: 0, + ackData: []byte{0x00, 0x00, 0x00, 0x00}, + }, + { + name: "buffer response with small data", + ackHandle: 0x12345678, + isBufferResponse: true, + errorCode: 0, + ackData: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + }, + { + name: "error response", + ackHandle: 100, + isBufferResponse: false, + errorCode: 1, + ackData: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + }, + { + name: "empty buffer response", + ackHandle: 999, + isBufferResponse: true, + errorCode: 0, + ackData: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := &MsgSysAck{ + AckHandle: tt.ackHandle, + IsBufferResponse: tt.isBufferResponse, + ErrorCode: tt.errorCode, + AckData: tt.ackData, + } + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Compare + if parsed.AckHandle != original.AckHandle { + t.Errorf("AckHandle = %d, want %d", 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) + } + }) + } +} + +func TestMsgSysAckLargePayload(t *testing.T) { + // Test with payload larger than 0xFFFF to trigger extended size field + largeData := make([]byte, 0x10000) // 65536 bytes + for i := range largeData { + largeData[i] = byte(i % 256) + } + + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: true, + ErrorCode: 0, + AckData: largeData, + } + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Parse + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if len(parsed.AckData) != len(largeData) { + t.Errorf("AckData len = %d, want %d", len(parsed.AckData), len(largeData)) + } +} + +func TestMsgSysAckOpcode(t *testing.T) { + pkt := &MsgSysAck{} + if pkt.Opcode() != network.MSG_SYS_ACK { + t.Errorf("Opcode() = %s, want MSG_SYS_ACK", pkt.Opcode()) + } +} + +func TestMsgSysNopRoundTrip(t *testing.T) { + original := &MsgSysNop{} + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Should write no data + if len(bf.Data()) != 0 { + t.Errorf("MsgSysNop.Build() wrote %d bytes, want 0", len(bf.Data())) + } + + // Parse (from empty buffer) + parsed := &MsgSysNop{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +func TestMsgSysNopOpcode(t *testing.T) { + pkt := &MsgSysNop{} + if pkt.Opcode() != network.MSG_SYS_NOP { + t.Errorf("Opcode() = %s, want MSG_SYS_NOP", pkt.Opcode()) + } +} + +func TestMsgSysEndRoundTrip(t *testing.T) { + original := &MsgSysEnd{} + ctx := &clientctx.ClientContext{} + + // Build + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + // Should write no data + if len(bf.Data()) != 0 { + t.Errorf("MsgSysEnd.Build() wrote %d bytes, want 0", len(bf.Data())) + } + + // Parse (from empty buffer) + parsed := &MsgSysEnd{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } +} + +func TestMsgSysEndOpcode(t *testing.T) { + pkt := &MsgSysEnd{} + if pkt.Opcode() != network.MSG_SYS_END { + t.Errorf("Opcode() = %s, want MSG_SYS_END", pkt.Opcode()) + } +} + +func TestMsgSysAckNonBufferResponse(t *testing.T) { + // Non-buffer response should always read/write 4 bytes of data + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0xAA, 0xBB, 0xCC, 0xDD}, + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Non-buffer response should have exactly 4 bytes of data + if len(parsed.AckData) != 4 { + t.Errorf("Non-buffer AckData len = %d, want 4", len(parsed.AckData)) + } +} + +func TestMsgSysAckNonBufferShortData(t *testing.T) { + // Non-buffer response with short data should pad to 4 bytes + original := &MsgSysAck{ + AckHandle: 1, + IsBufferResponse: false, + ErrorCode: 0, + AckData: []byte{0x01}, // Only 1 byte + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + err := original.Build(bf, ctx) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + + bf.Seek(0, io.SeekStart) + parsed := &MsgSysAck{} + err = parsed.Parse(bf, ctx) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Should still read 4 bytes + if len(parsed.AckData) != 4 { + t.Errorf("AckData len = %d, want 4", len(parsed.AckData)) + } +} + +func TestMsgSysAckBuildFormat(t *testing.T) { + pkt := &MsgSysAck{ + AckHandle: 0x12345678, + IsBufferResponse: true, + ErrorCode: 0x55, + AckData: []byte{0xAA, 0xBB}, + } + ctx := &clientctx.ClientContext{} + + bf := byteframe.NewByteFrame() + pkt.Build(bf, ctx) + + data := bf.Data() + + // Check AckHandle (big-endian) + if data[0] != 0x12 || data[1] != 0x34 || data[2] != 0x56 || data[3] != 0x78 { + t.Errorf("AckHandle bytes = %X, want 12345678", data[:4]) + } + + // Check IsBufferResponse (1 = true) + if data[4] != 1 { + t.Errorf("IsBufferResponse byte = %d, want 1", data[4]) + } + + // Check ErrorCode + if data[5] != 0x55 { + t.Errorf("ErrorCode byte = %X, want 55", data[5]) + } + + // Check payload size (2 bytes, big-endian) + if data[6] != 0x00 || data[7] != 0x02 { + t.Errorf("PayloadSize bytes = %X %X, want 00 02", data[6], data[7]) + } + + // Check actual data + if data[8] != 0xAA || data[9] != 0xBB { + t.Errorf("AckData bytes = %X %X, want AA BB", data[8], data[9]) + } +} + +func TestCorePacketsFromOpcode(t *testing.T) { + coreOpcodes := []network.PacketID{ + network.MSG_SYS_NOP, + network.MSG_SYS_END, + network.MSG_SYS_ACK, + network.MSG_SYS_PING, + } + + for _, opcode := range coreOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Fatalf("FromOpcode(%s) returned nil", opcode) + } + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} diff --git a/network/mhfpacket/msg_sys_packets_test.go b/network/mhfpacket/msg_sys_packets_test.go new file mode 100644 index 000000000..a5689cae8 --- /dev/null +++ b/network/mhfpacket/msg_sys_packets_test.go @@ -0,0 +1,592 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +// TestMsgSysCastBinaryParse tests parsing MsgSysCastBinary +func TestMsgSysCastBinaryParse(t *testing.T) { + tests := []struct { + name string + unk uint32 + broadcastType uint8 + messageType uint8 + payload []byte + }{ + {"empty payload", 0, 1, 2, []byte{}}, + {"small payload", 0x006400C8, 3, 4, []byte{0xAA, 0xBB, 0xCC}}, + {"large payload", 0xFFFFFFFF, 0xFF, 0xFF, make([]byte, 100)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.unk) + bf.WriteUint8(tt.broadcastType) + bf.WriteUint8(tt.messageType) + bf.WriteUint16(uint16(len(tt.payload))) + bf.WriteBytes(tt.payload) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCastBinary{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.Unk != tt.unk { + t.Errorf("Unk = %d, want %d", pkt.Unk, tt.unk) + } + if pkt.BroadcastType != tt.broadcastType { + t.Errorf("BroadcastType = %d, want %d", pkt.BroadcastType, tt.broadcastType) + } + if pkt.MessageType != tt.messageType { + t.Errorf("MessageType = %d, want %d", pkt.MessageType, tt.messageType) + } + if len(pkt.RawDataPayload) != len(tt.payload) { + t.Errorf("RawDataPayload len = %d, want %d", len(pkt.RawDataPayload), len(tt.payload)) + } + }) + } +} + +// TestMsgSysCastBinaryOpcode tests Opcode method +func TestMsgSysCastBinaryOpcode(t *testing.T) { + pkt := &MsgSysCastBinary{} + if pkt.Opcode() != network.MSG_SYS_CAST_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_CAST_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysCreateSemaphoreOpcode tests Opcode method +func TestMsgSysCreateSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysCreateSemaphore{} + if pkt.Opcode() != network.MSG_SYS_CREATE_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_CREATE_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysCastedBinaryOpcode tests Opcode method +func TestMsgSysCastedBinaryOpcode(t *testing.T) { + pkt := &MsgSysCastedBinary{} + if pkt.Opcode() != network.MSG_SYS_CASTED_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_CASTED_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysSetStageBinaryOpcode tests Opcode method +func TestMsgSysSetStageBinaryOpcode(t *testing.T) { + pkt := &MsgSysSetStageBinary{} + if pkt.Opcode() != network.MSG_SYS_SET_STAGE_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_SET_STAGE_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysGetStageBinaryOpcode tests Opcode method +func TestMsgSysGetStageBinaryOpcode(t *testing.T) { + pkt := &MsgSysGetStageBinary{} + if pkt.Opcode() != network.MSG_SYS_GET_STAGE_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_STAGE_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysWaitStageBinaryOpcode tests Opcode method +func TestMsgSysWaitStageBinaryOpcode(t *testing.T) { + pkt := &MsgSysWaitStageBinary{} + if pkt.Opcode() != network.MSG_SYS_WAIT_STAGE_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_WAIT_STAGE_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysEnumerateClientOpcode tests Opcode method +func TestMsgSysEnumerateClientOpcode(t *testing.T) { + pkt := &MsgSysEnumerateClient{} + if pkt.Opcode() != network.MSG_SYS_ENUMERATE_CLIENT { + t.Errorf("Opcode() = %s, want MSG_SYS_ENUMERATE_CLIENT", pkt.Opcode()) + } +} + +// TestMsgSysEnumerateStageOpcode tests Opcode method +func TestMsgSysEnumerateStageOpcode(t *testing.T) { + pkt := &MsgSysEnumerateStage{} + if pkt.Opcode() != network.MSG_SYS_ENUMERATE_STAGE { + t.Errorf("Opcode() = %s, want MSG_SYS_ENUMERATE_STAGE", pkt.Opcode()) + } +} + +// TestMsgSysCreateMutexOpcode tests Opcode method +func TestMsgSysCreateMutexOpcode(t *testing.T) { + pkt := &MsgSysCreateMutex{} + if pkt.Opcode() != network.MSG_SYS_CREATE_MUTEX { + t.Errorf("Opcode() = %s, want MSG_SYS_CREATE_MUTEX", pkt.Opcode()) + } +} + +// TestMsgSysCreateOpenMutexOpcode tests Opcode method +func TestMsgSysCreateOpenMutexOpcode(t *testing.T) { + pkt := &MsgSysCreateOpenMutex{} + if pkt.Opcode() != network.MSG_SYS_CREATE_OPEN_MUTEX { + t.Errorf("Opcode() = %s, want MSG_SYS_CREATE_OPEN_MUTEX", pkt.Opcode()) + } +} + +// TestMsgSysDeleteMutexOpcode tests Opcode method +func TestMsgSysDeleteMutexOpcode(t *testing.T) { + pkt := &MsgSysDeleteMutex{} + if pkt.Opcode() != network.MSG_SYS_DELETE_MUTEX { + t.Errorf("Opcode() = %s, want MSG_SYS_DELETE_MUTEX", pkt.Opcode()) + } +} + +// TestMsgSysOpenMutexOpcode tests Opcode method +func TestMsgSysOpenMutexOpcode(t *testing.T) { + pkt := &MsgSysOpenMutex{} + if pkt.Opcode() != network.MSG_SYS_OPEN_MUTEX { + t.Errorf("Opcode() = %s, want MSG_SYS_OPEN_MUTEX", pkt.Opcode()) + } +} + +// TestMsgSysCloseMutexOpcode tests Opcode method +func TestMsgSysCloseMutexOpcode(t *testing.T) { + pkt := &MsgSysCloseMutex{} + if pkt.Opcode() != network.MSG_SYS_CLOSE_MUTEX { + t.Errorf("Opcode() = %s, want MSG_SYS_CLOSE_MUTEX", pkt.Opcode()) + } +} + +// TestMsgSysDeleteSemaphoreOpcode tests Opcode method +func TestMsgSysDeleteSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysDeleteSemaphore{} + if pkt.Opcode() != network.MSG_SYS_DELETE_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_DELETE_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysAcquireSemaphoreOpcode tests Opcode method +func TestMsgSysAcquireSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysAcquireSemaphore{} + if pkt.Opcode() != network.MSG_SYS_ACQUIRE_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_ACQUIRE_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysReleaseSemaphoreOpcode tests Opcode method +func TestMsgSysReleaseSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysReleaseSemaphore{} + if pkt.Opcode() != network.MSG_SYS_RELEASE_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_RELEASE_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysCheckSemaphoreOpcode tests Opcode method +func TestMsgSysCheckSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysCheckSemaphore{} + if pkt.Opcode() != network.MSG_SYS_CHECK_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_CHECK_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysCreateAcquireSemaphoreOpcode tests Opcode method +func TestMsgSysCreateAcquireSemaphoreOpcode(t *testing.T) { + pkt := &MsgSysCreateAcquireSemaphore{} + if pkt.Opcode() != network.MSG_SYS_CREATE_ACQUIRE_SEMAPHORE { + t.Errorf("Opcode() = %s, want MSG_SYS_CREATE_ACQUIRE_SEMAPHORE", pkt.Opcode()) + } +} + +// TestMsgSysOperateRegisterOpcode tests Opcode method +func TestMsgSysOperateRegisterOpcode(t *testing.T) { + pkt := &MsgSysOperateRegister{} + if pkt.Opcode() != network.MSG_SYS_OPERATE_REGISTER { + t.Errorf("Opcode() = %s, want MSG_SYS_OPERATE_REGISTER", pkt.Opcode()) + } +} + +// TestMsgSysLoadRegisterOpcode tests Opcode method +func TestMsgSysLoadRegisterOpcode(t *testing.T) { + pkt := &MsgSysLoadRegister{} + if pkt.Opcode() != network.MSG_SYS_LOAD_REGISTER { + t.Errorf("Opcode() = %s, want MSG_SYS_LOAD_REGISTER", pkt.Opcode()) + } +} + +// TestMsgSysNotifyRegisterOpcode tests Opcode method +func TestMsgSysNotifyRegisterOpcode(t *testing.T) { + pkt := &MsgSysNotifyRegister{} + if pkt.Opcode() != network.MSG_SYS_NOTIFY_REGISTER { + t.Errorf("Opcode() = %s, want MSG_SYS_NOTIFY_REGISTER", pkt.Opcode()) + } +} + +// TestMsgSysCreateObjectOpcode tests Opcode method +func TestMsgSysCreateObjectOpcode(t *testing.T) { + pkt := &MsgSysCreateObject{} + if pkt.Opcode() != network.MSG_SYS_CREATE_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_CREATE_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysDeleteObjectOpcode tests Opcode method +func TestMsgSysDeleteObjectOpcode(t *testing.T) { + pkt := &MsgSysDeleteObject{} + if pkt.Opcode() != network.MSG_SYS_DELETE_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_DELETE_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysPositionObjectOpcode tests Opcode method +func TestMsgSysPositionObjectOpcode(t *testing.T) { + pkt := &MsgSysPositionObject{} + if pkt.Opcode() != network.MSG_SYS_POSITION_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_POSITION_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysRotateObjectOpcode tests Opcode method +func TestMsgSysRotateObjectOpcode(t *testing.T) { + pkt := &MsgSysRotateObject{} + if pkt.Opcode() != network.MSG_SYS_ROTATE_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_ROTATE_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysDuplicateObjectOpcode tests Opcode method +func TestMsgSysDuplicateObjectOpcode(t *testing.T) { + pkt := &MsgSysDuplicateObject{} + if pkt.Opcode() != network.MSG_SYS_DUPLICATE_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_DUPLICATE_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysSetObjectBinaryOpcode tests Opcode method +func TestMsgSysSetObjectBinaryOpcode(t *testing.T) { + pkt := &MsgSysSetObjectBinary{} + if pkt.Opcode() != network.MSG_SYS_SET_OBJECT_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_SET_OBJECT_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysGetObjectBinaryOpcode tests Opcode method +func TestMsgSysGetObjectBinaryOpcode(t *testing.T) { + pkt := &MsgSysGetObjectBinary{} + if pkt.Opcode() != network.MSG_SYS_GET_OBJECT_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_OBJECT_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysGetObjectOwnerOpcode tests Opcode method +func TestMsgSysGetObjectOwnerOpcode(t *testing.T) { + pkt := &MsgSysGetObjectOwner{} + if pkt.Opcode() != network.MSG_SYS_GET_OBJECT_OWNER { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_OBJECT_OWNER", pkt.Opcode()) + } +} + +// TestMsgSysUpdateObjectBinaryOpcode tests Opcode method +func TestMsgSysUpdateObjectBinaryOpcode(t *testing.T) { + pkt := &MsgSysUpdateObjectBinary{} + if pkt.Opcode() != network.MSG_SYS_UPDATE_OBJECT_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_UPDATE_OBJECT_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysCleanupObjectOpcode tests Opcode method +func TestMsgSysCleanupObjectOpcode(t *testing.T) { + pkt := &MsgSysCleanupObject{} + if pkt.Opcode() != network.MSG_SYS_CLEANUP_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_CLEANUP_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysInsertUserOpcode tests Opcode method +func TestMsgSysInsertUserOpcode(t *testing.T) { + pkt := &MsgSysInsertUser{} + if pkt.Opcode() != network.MSG_SYS_INSERT_USER { + t.Errorf("Opcode() = %s, want MSG_SYS_INSERT_USER", pkt.Opcode()) + } +} + +// TestMsgSysDeleteUserOpcode tests Opcode method +func TestMsgSysDeleteUserOpcode(t *testing.T) { + pkt := &MsgSysDeleteUser{} + if pkt.Opcode() != network.MSG_SYS_DELETE_USER { + t.Errorf("Opcode() = %s, want MSG_SYS_DELETE_USER", pkt.Opcode()) + } +} + +// TestMsgSysSetUserBinaryOpcode tests Opcode method +func TestMsgSysSetUserBinaryOpcode(t *testing.T) { + pkt := &MsgSysSetUserBinary{} + if pkt.Opcode() != network.MSG_SYS_SET_USER_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_SET_USER_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysGetUserBinaryOpcode tests Opcode method +func TestMsgSysGetUserBinaryOpcode(t *testing.T) { + pkt := &MsgSysGetUserBinary{} + if pkt.Opcode() != network.MSG_SYS_GET_USER_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_USER_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysNotifyUserBinaryOpcode tests Opcode method +func TestMsgSysNotifyUserBinaryOpcode(t *testing.T) { + pkt := &MsgSysNotifyUserBinary{} + if pkt.Opcode() != network.MSG_SYS_NOTIFY_USER_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_NOTIFY_USER_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysUpdateRightOpcode tests Opcode method +func TestMsgSysUpdateRightOpcode(t *testing.T) { + pkt := &MsgSysUpdateRight{} + if pkt.Opcode() != network.MSG_SYS_UPDATE_RIGHT { + t.Errorf("Opcode() = %s, want MSG_SYS_UPDATE_RIGHT", pkt.Opcode()) + } +} + +// TestMsgSysAuthQueryOpcode tests Opcode method +func TestMsgSysAuthQueryOpcode(t *testing.T) { + pkt := &MsgSysAuthQuery{} + if pkt.Opcode() != network.MSG_SYS_AUTH_QUERY { + t.Errorf("Opcode() = %s, want MSG_SYS_AUTH_QUERY", pkt.Opcode()) + } +} + +// TestMsgSysAuthDataOpcode tests Opcode method +func TestMsgSysAuthDataOpcode(t *testing.T) { + pkt := &MsgSysAuthData{} + if pkt.Opcode() != network.MSG_SYS_AUTH_DATA { + t.Errorf("Opcode() = %s, want MSG_SYS_AUTH_DATA", pkt.Opcode()) + } +} + +// TestMsgSysAuthTerminalOpcode tests Opcode method +func TestMsgSysAuthTerminalOpcode(t *testing.T) { + pkt := &MsgSysAuthTerminal{} + if pkt.Opcode() != network.MSG_SYS_AUTH_TERMINAL { + t.Errorf("Opcode() = %s, want MSG_SYS_AUTH_TERMINAL", pkt.Opcode()) + } +} + +// TestMsgSysRightsReloadOpcode tests Opcode method +func TestMsgSysRightsReloadOpcode(t *testing.T) { + pkt := &MsgSysRightsReload{} + if pkt.Opcode() != network.MSG_SYS_RIGHTS_RELOAD { + t.Errorf("Opcode() = %s, want MSG_SYS_RIGHTS_RELOAD", pkt.Opcode()) + } +} + +// TestMsgSysTerminalLogOpcode tests Opcode method +func TestMsgSysTerminalLogOpcode(t *testing.T) { + pkt := &MsgSysTerminalLog{} + if pkt.Opcode() != network.MSG_SYS_TERMINAL_LOG { + t.Errorf("Opcode() = %s, want MSG_SYS_TERMINAL_LOG", pkt.Opcode()) + } +} + +// TestMsgSysIssueLogkeyOpcode tests Opcode method +func TestMsgSysIssueLogkeyOpcode(t *testing.T) { + pkt := &MsgSysIssueLogkey{} + if pkt.Opcode() != network.MSG_SYS_ISSUE_LOGKEY { + t.Errorf("Opcode() = %s, want MSG_SYS_ISSUE_LOGKEY", pkt.Opcode()) + } +} + +// TestMsgSysRecordLogOpcode tests Opcode method +func TestMsgSysRecordLogOpcode(t *testing.T) { + pkt := &MsgSysRecordLog{} + if pkt.Opcode() != network.MSG_SYS_RECORD_LOG { + t.Errorf("Opcode() = %s, want MSG_SYS_RECORD_LOG", pkt.Opcode()) + } +} + +// TestMsgSysEchoOpcode tests Opcode method +func TestMsgSysEchoOpcode(t *testing.T) { + pkt := &MsgSysEcho{} + if pkt.Opcode() != network.MSG_SYS_ECHO { + t.Errorf("Opcode() = %s, want MSG_SYS_ECHO", pkt.Opcode()) + } +} + +// TestMsgSysGetFileOpcode tests Opcode method +func TestMsgSysGetFileOpcode(t *testing.T) { + pkt := &MsgSysGetFile{} + if pkt.Opcode() != network.MSG_SYS_GET_FILE { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_FILE", pkt.Opcode()) + } +} + +// TestMsgSysHideClientOpcode tests Opcode method +func TestMsgSysHideClientOpcode(t *testing.T) { + pkt := &MsgSysHideClient{} + if pkt.Opcode() != network.MSG_SYS_HIDE_CLIENT { + t.Errorf("Opcode() = %s, want MSG_SYS_HIDE_CLIENT", pkt.Opcode()) + } +} + +// TestMsgSysSetStatusOpcode tests Opcode method +func TestMsgSysSetStatusOpcode(t *testing.T) { + pkt := &MsgSysSetStatus{} + if pkt.Opcode() != network.MSG_SYS_SET_STATUS { + t.Errorf("Opcode() = %s, want MSG_SYS_SET_STATUS", pkt.Opcode()) + } +} + +// TestMsgSysStageDestructOpcode tests Opcode method +func TestMsgSysStageDestructOpcode(t *testing.T) { + pkt := &MsgSysStageDestruct{} + if pkt.Opcode() != network.MSG_SYS_STAGE_DESTRUCT { + t.Errorf("Opcode() = %s, want MSG_SYS_STAGE_DESTRUCT", pkt.Opcode()) + } +} + +// TestMsgSysLeaveStageOpcode tests Opcode method +func TestMsgSysLeaveStageOpcode(t *testing.T) { + pkt := &MsgSysLeaveStage{} + if pkt.Opcode() != network.MSG_SYS_LEAVE_STAGE { + t.Errorf("Opcode() = %s, want MSG_SYS_LEAVE_STAGE", pkt.Opcode()) + } +} + +// TestMsgSysReserveStageOpcode tests Opcode method +func TestMsgSysReserveStageOpcode(t *testing.T) { + pkt := &MsgSysReserveStage{} + if pkt.Opcode() != network.MSG_SYS_RESERVE_STAGE { + t.Errorf("Opcode() = %s, want MSG_SYS_RESERVE_STAGE", pkt.Opcode()) + } +} + +// TestMsgSysUnreserveStageOpcode tests Opcode method +func TestMsgSysUnreserveStageOpcode(t *testing.T) { + pkt := &MsgSysUnreserveStage{} + if pkt.Opcode() != network.MSG_SYS_UNRESERVE_STAGE { + t.Errorf("Opcode() = %s, want MSG_SYS_UNRESERVE_STAGE", pkt.Opcode()) + } +} + +// TestMsgSysSetStagePassOpcode tests Opcode method +func TestMsgSysSetStagePassOpcode(t *testing.T) { + pkt := &MsgSysSetStagePass{} + if pkt.Opcode() != network.MSG_SYS_SET_STAGE_PASS { + t.Errorf("Opcode() = %s, want MSG_SYS_SET_STAGE_PASS", pkt.Opcode()) + } +} + +// TestMsgSysLockGlobalSemaOpcode tests Opcode method +func TestMsgSysLockGlobalSemaOpcode(t *testing.T) { + pkt := &MsgSysLockGlobalSema{} + if pkt.Opcode() != network.MSG_SYS_LOCK_GLOBAL_SEMA { + t.Errorf("Opcode() = %s, want MSG_SYS_LOCK_GLOBAL_SEMA", pkt.Opcode()) + } +} + +// TestMsgSysUnlockGlobalSemaOpcode tests Opcode method +func TestMsgSysUnlockGlobalSemaOpcode(t *testing.T) { + pkt := &MsgSysUnlockGlobalSema{} + if pkt.Opcode() != network.MSG_SYS_UNLOCK_GLOBAL_SEMA { + t.Errorf("Opcode() = %s, want MSG_SYS_UNLOCK_GLOBAL_SEMA", pkt.Opcode()) + } +} + +// TestMsgSysTransBinaryOpcode tests Opcode method +func TestMsgSysTransBinaryOpcode(t *testing.T) { + pkt := &MsgSysTransBinary{} + if pkt.Opcode() != network.MSG_SYS_TRANS_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_TRANS_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysCollectBinaryOpcode tests Opcode method +func TestMsgSysCollectBinaryOpcode(t *testing.T) { + pkt := &MsgSysCollectBinary{} + if pkt.Opcode() != network.MSG_SYS_COLLECT_BINARY { + t.Errorf("Opcode() = %s, want MSG_SYS_COLLECT_BINARY", pkt.Opcode()) + } +} + +// TestMsgSysGetStateOpcode tests Opcode method +func TestMsgSysGetStateOpcode(t *testing.T) { + pkt := &MsgSysGetState{} + if pkt.Opcode() != network.MSG_SYS_GET_STATE { + t.Errorf("Opcode() = %s, want MSG_SYS_GET_STATE", pkt.Opcode()) + } +} + +// TestMsgSysSerializeOpcode tests Opcode method +func TestMsgSysSerializeOpcode(t *testing.T) { + pkt := &MsgSysSerialize{} + if pkt.Opcode() != network.MSG_SYS_SERIALIZE { + t.Errorf("Opcode() = %s, want MSG_SYS_SERIALIZE", pkt.Opcode()) + } +} + +// TestMsgSysEnumlobbyOpcode tests Opcode method +func TestMsgSysEnumlobbyOpcode(t *testing.T) { + pkt := &MsgSysEnumlobby{} + if pkt.Opcode() != network.MSG_SYS_ENUMLOBBY { + t.Errorf("Opcode() = %s, want MSG_SYS_ENUMLOBBY", pkt.Opcode()) + } +} + +// TestMsgSysEnumuserOpcode tests Opcode method +func TestMsgSysEnumuserOpcode(t *testing.T) { + pkt := &MsgSysEnumuser{} + if pkt.Opcode() != network.MSG_SYS_ENUMUSER { + t.Errorf("Opcode() = %s, want MSG_SYS_ENUMUSER", pkt.Opcode()) + } +} + +// TestMsgSysInfokyserverOpcode tests Opcode method +func TestMsgSysInfokyserverOpcode(t *testing.T) { + pkt := &MsgSysInfokyserver{} + if pkt.Opcode() != network.MSG_SYS_INFOKYSERVER { + t.Errorf("Opcode() = %s, want MSG_SYS_INFOKYSERVER", pkt.Opcode()) + } +} + +// TestMsgSysExtendThresholdOpcode tests Opcode method +func TestMsgSysExtendThresholdOpcode(t *testing.T) { + pkt := &MsgSysExtendThreshold{} + if pkt.Opcode() != network.MSG_SYS_EXTEND_THRESHOLD { + t.Errorf("Opcode() = %s, want MSG_SYS_EXTEND_THRESHOLD", pkt.Opcode()) + } +} + +// TestMsgSysAddObjectOpcode tests Opcode method +func TestMsgSysAddObjectOpcode(t *testing.T) { + pkt := &MsgSysAddObject{} + if pkt.Opcode() != network.MSG_SYS_ADD_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_ADD_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysDelObjectOpcode tests Opcode method +func TestMsgSysDelObjectOpcode(t *testing.T) { + pkt := &MsgSysDelObject{} + if pkt.Opcode() != network.MSG_SYS_DEL_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_DEL_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysDispObjectOpcode tests Opcode method +func TestMsgSysDispObjectOpcode(t *testing.T) { + pkt := &MsgSysDispObject{} + if pkt.Opcode() != network.MSG_SYS_DISP_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_DISP_OBJECT", pkt.Opcode()) + } +} + +// TestMsgSysHideObjectOpcode tests Opcode method +func TestMsgSysHideObjectOpcode(t *testing.T) { + pkt := &MsgSysHideObject{} + if pkt.Opcode() != network.MSG_SYS_HIDE_OBJECT { + t.Errorf("Opcode() = %s, want MSG_SYS_HIDE_OBJECT", pkt.Opcode()) + } +} diff --git a/network/mhfpacket/msg_sys_stage_test.go b/network/mhfpacket/msg_sys_stage_test.go new file mode 100644 index 000000000..db6a0f602 --- /dev/null +++ b/network/mhfpacket/msg_sys_stage_test.go @@ -0,0 +1,332 @@ +package mhfpacket + +import ( + "io" + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" +) + +func TestStagePacketOpcodes(t *testing.T) { + tests := []struct { + name string + pkt MHFPacket + expect network.PacketID + }{ + {"MsgSysCreateStage", &MsgSysCreateStage{}, network.MSG_SYS_CREATE_STAGE}, + {"MsgSysEnterStage", &MsgSysEnterStage{}, network.MSG_SYS_ENTER_STAGE}, + {"MsgSysMoveStage", &MsgSysMoveStage{}, network.MSG_SYS_MOVE_STAGE}, + {"MsgSysBackStage", &MsgSysBackStage{}, network.MSG_SYS_BACK_STAGE}, + {"MsgSysLockStage", &MsgSysLockStage{}, network.MSG_SYS_LOCK_STAGE}, + {"MsgSysUnlockStage", &MsgSysUnlockStage{}, network.MSG_SYS_UNLOCK_STAGE}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.pkt.Opcode(); got != tt.expect { + t.Errorf("Opcode() = %v, want %v", got, tt.expect) + } + }) + } +} + +func TestMsgSysCreateStageFields(t *testing.T) { + tests := []struct { + name string + ackHandle uint32 + unk0 uint8 + playerCount uint8 + stageID string + }{ + {"empty stage", 1, 1, 4, ""}, + {"mezeporta", 0x12345678, 2, 8, "sl1Ns200p0a0u0"}, + {"quest room", 100, 1, 4, "q1234"}, + {"max players", 0xFFFFFFFF, 2, 16, "max_stage"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.ackHandle) + bf.WriteUint8(tt.unk0) + bf.WriteUint8(tt.playerCount) + stageIDBytes := []byte(tt.stageID) + bf.WriteUint8(uint8(len(stageIDBytes))) + bf.WriteBytes(append(stageIDBytes, 0x00)) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCreateStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.ackHandle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.ackHandle) + } + if pkt.Unk0 != tt.unk0 { + t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0) + } + if pkt.PlayerCount != tt.playerCount { + t.Errorf("PlayerCount = %d, want %d", pkt.PlayerCount, tt.playerCount) + } + if pkt.StageID != tt.stageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.stageID) + } + }) + } +} + +func TestMsgSysEnterStageFields(t *testing.T) { + tests := []struct { + name string + handle uint32 + unk bool + stageID string + }{ + {"enter town", 1, false, "town01"}, + {"force enter", 2, true, "quest_stage"}, + {"rasta bar", 999, false, "sl1Ns211p0a0u0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.handle) + bf.WriteBool(tt.unk) + stageIDBytes := []byte(tt.stageID) + bf.WriteUint8(uint8(len(stageIDBytes))) + bf.WriteBytes(append(stageIDBytes, 0x00)) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysEnterStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.handle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.handle) + } + if pkt.Unk != tt.unk { + t.Errorf("Unk = %v, want %v", pkt.Unk, tt.unk) + } + if pkt.StageID != tt.stageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.stageID) + } + }) + } +} + +func TestMsgSysMoveStageFields(t *testing.T) { + tests := []struct { + name string + handle uint32 + unkBool uint8 + stageID string + }{ + {"move to area", 1, 0, "area01"}, + {"move to quest", 0xABCD, 1, "quest12345"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.handle) + bf.WriteUint8(tt.unkBool) + stageIDBytes := []byte(tt.stageID) + bf.WriteUint8(uint8(len(stageIDBytes))) + bf.WriteBytes(stageIDBytes) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysMoveStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.handle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.handle) + } + if pkt.UnkBool != tt.unkBool { + t.Errorf("UnkBool = %d, want %d", pkt.UnkBool, tt.unkBool) + } + if pkt.StageID != tt.stageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.stageID) + } + }) + } +} + +func TestMsgSysLockStageFields(t *testing.T) { + tests := []struct { + name string + handle uint32 + unk0 uint8 + unk1 uint8 + stageID string + }{ + {"lock room", 1, 1, 1, "room01"}, + {"private party", 0x1234, 1, 1, "party_stage"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.handle) + bf.WriteUint8(tt.unk0) + bf.WriteUint8(tt.unk1) + stageIDBytes := []byte(tt.stageID) + bf.WriteUint8(uint8(len(stageIDBytes))) + bf.WriteBytes(append(stageIDBytes, 0x00)) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysLockStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.handle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.handle) + } + // Unk0 and Unk1 are read but discarded by Parse, so we only verify + // that Parse consumed the bytes without error + if pkt.StageID != tt.stageID { + t.Errorf("StageID = %q, want %q", pkt.StageID, tt.stageID) + } + }) + } +} + +func TestMsgSysUnlockStageFields(t *testing.T) { + tests := []struct { + name string + unk0 uint16 + }{ + {"zero", 0}, + {"typical", 1}, + {"max", 0xFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint16(tt.unk0) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysUnlockStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + // MsgSysUnlockStage is an empty struct; Parse reads and discards a uint16. + // We just verify Parse doesn't error. + }) + } +} + +func TestMsgSysBackStageFields(t *testing.T) { + tests := []struct { + name string + handle uint32 + }{ + {"small handle", 1}, + {"large handle", 0xDEADBEEF}, + {"zero", 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(tt.handle) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysBackStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.AckHandle != tt.handle { + t.Errorf("AckHandle = %d, want %d", pkt.AckHandle, tt.handle) + } + }) + } +} + +func TestStageIDEdgeCases(t *testing.T) { + t.Run("long stage ID", func(t *testing.T) { + // Stage ID with max length (255 bytes) + longID := make([]byte, 200) + for i := range longID { + longID[i] = 'a' + byte(i%26) + } + + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) + bf.WriteUint8(1) + bf.WriteUint8(4) + bf.WriteUint8(uint8(len(longID))) + bf.WriteBytes(append(longID, 0x00)) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysCreateStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + if pkt.StageID != string(longID) { + t.Errorf("StageID length = %d, want %d", len(pkt.StageID), len(longID)) + } + }) + + t.Run("stage ID with null terminator", func(t *testing.T) { + // String terminated with null byte + stageID := "test\x00extra" + + bf := byteframe.NewByteFrame() + bf.WriteUint32(1) + bf.WriteUint8(0) + bf.WriteUint8(uint8(len(stageID))) + bf.WriteBytes([]byte(stageID)) + bf.Seek(0, io.SeekStart) + + pkt := &MsgSysEnterStage{} + err := pkt.Parse(bf, &clientctx.ClientContext{}) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + + // Should truncate at null + if pkt.StageID != "test" { + t.Errorf("StageID = %q, want %q (should truncate at null)", pkt.StageID, "test") + } + }) +} + +func TestStagePacketFromOpcode(t *testing.T) { + stageOpcodes := []network.PacketID{ + network.MSG_SYS_CREATE_STAGE, + network.MSG_SYS_ENTER_STAGE, + network.MSG_SYS_BACK_STAGE, + network.MSG_SYS_MOVE_STAGE, + network.MSG_SYS_LOCK_STAGE, + network.MSG_SYS_UNLOCK_STAGE, + } + + for _, opcode := range stageOpcodes { + t.Run(opcode.String(), func(t *testing.T) { + pkt := FromOpcode(opcode) + if pkt == nil { + t.Fatalf("FromOpcode(%s) returned nil", opcode) + } + if pkt.Opcode() != opcode { + t.Errorf("Opcode() = %s, want %s", pkt.Opcode(), opcode) + } + }) + } +} diff --git a/network/packetid_test.go b/network/packetid_test.go new file mode 100644 index 000000000..3b9f1d91d --- /dev/null +++ b/network/packetid_test.go @@ -0,0 +1,211 @@ +package network + +import ( + "testing" +) + +func TestPacketIDType(t *testing.T) { + // PacketID is based on uint16 + var p PacketID = 0xFFFF + if uint16(p) != 0xFFFF { + t.Errorf("PacketID max value = %d, want %d", uint16(p), 0xFFFF) + } +} + +func TestPacketIDConstants(t *testing.T) { + // Test critical packet IDs are correct + tests := []struct { + name string + id PacketID + expect uint16 + }{ + {"MSG_HEAD", MSG_HEAD, 0}, + {"MSG_SYS_END", MSG_SYS_END, 0x10}, + {"MSG_SYS_NOP", MSG_SYS_NOP, 0x11}, + {"MSG_SYS_ACK", MSG_SYS_ACK, 0x12}, + {"MSG_SYS_LOGIN", MSG_SYS_LOGIN, 0x14}, + {"MSG_SYS_LOGOUT", MSG_SYS_LOGOUT, 0x15}, + {"MSG_SYS_PING", MSG_SYS_PING, 0x17}, + {"MSG_SYS_TIME", MSG_SYS_TIME, 0x1A}, + {"MSG_SYS_CREATE_STAGE", MSG_SYS_CREATE_STAGE, 0x20}, + {"MSG_SYS_ENTER_STAGE", MSG_SYS_ENTER_STAGE, 0x22}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if uint16(tt.id) != tt.expect { + t.Errorf("%s = 0x%X, want 0x%X", tt.name, uint16(tt.id), tt.expect) + } + }) + } +} + +func TestPacketIDString(t *testing.T) { + // Test that String() method works for known packet IDs + tests := []struct { + id PacketID + contains string + }{ + {MSG_HEAD, "MSG_HEAD"}, + {MSG_SYS_PING, "MSG_SYS_PING"}, + {MSG_SYS_END, "MSG_SYS_END"}, + {MSG_SYS_NOP, "MSG_SYS_NOP"}, + {MSG_SYS_ACK, "MSG_SYS_ACK"}, + {MSG_SYS_LOGIN, "MSG_SYS_LOGIN"}, + {MSG_SYS_LOGOUT, "MSG_SYS_LOGOUT"}, + } + + for _, tt := range tests { + t.Run(tt.contains, func(t *testing.T) { + got := tt.id.String() + if got != tt.contains { + t.Errorf("String() = %q, want %q", got, tt.contains) + } + }) + } +} + +func TestPacketIDUnknown(t *testing.T) { + // Unknown packet ID should still have a valid string representation + unknown := PacketID(0xFFFF) + str := unknown.String() + if str == "" { + t.Error("String() for unknown PacketID should not be empty") + } +} + +func TestPacketIDZero(t *testing.T) { + // MSG_HEAD should be 0 + if MSG_HEAD != 0 { + t.Errorf("MSG_HEAD = %d, want 0", MSG_HEAD) + } +} + +func TestSystemPacketIDRange(t *testing.T) { + // System packets should be in a specific range + systemPackets := []PacketID{ + MSG_SYS_reserve01, + MSG_SYS_reserve02, + MSG_SYS_reserve03, + MSG_SYS_ADD_OBJECT, + MSG_SYS_DEL_OBJECT, + MSG_SYS_END, + MSG_SYS_NOP, + MSG_SYS_ACK, + MSG_SYS_LOGIN, + MSG_SYS_LOGOUT, + MSG_SYS_PING, + MSG_SYS_TIME, + } + + for _, pkt := range systemPackets { + // System packets should have IDs > 0 (MSG_HEAD is 0) + if pkt < MSG_SYS_reserve01 { + t.Errorf("System packet %s has ID %d, should be >= MSG_SYS_reserve01", pkt, pkt) + } + } +} + +func TestMHFPacketIDRange(t *testing.T) { + // MHF packets start at MSG_MHF_SAVEDATA (0x60) + mhfPackets := []PacketID{ + MSG_MHF_SAVEDATA, + MSG_MHF_LOADDATA, + MSG_MHF_ENUMERATE_QUEST, + MSG_MHF_ACQUIRE_TITLE, + MSG_MHF_ACQUIRE_DIST_ITEM, + MSG_MHF_ACQUIRE_MONTHLY_ITEM, + } + + for _, pkt := range mhfPackets { + // MHF packets should be >= MSG_MHF_SAVEDATA + if pkt < MSG_MHF_SAVEDATA { + t.Errorf("MHF packet %s has ID %d, should be >= MSG_MHF_SAVEDATA (%d)", pkt, pkt, MSG_MHF_SAVEDATA) + } + } +} + +func TestStagePacketIDsSequential(t *testing.T) { + // Stage-related packets should be sequential + stagePackets := []PacketID{ + MSG_SYS_CREATE_STAGE, + MSG_SYS_STAGE_DESTRUCT, + MSG_SYS_ENTER_STAGE, + MSG_SYS_BACK_STAGE, + MSG_SYS_MOVE_STAGE, + MSG_SYS_LEAVE_STAGE, + MSG_SYS_LOCK_STAGE, + MSG_SYS_UNLOCK_STAGE, + } + + for i := 1; i < len(stagePackets); i++ { + if stagePackets[i] != stagePackets[i-1]+1 { + t.Errorf("Stage packets not sequential: %s (%d) should follow %s (%d)", + stagePackets[i], stagePackets[i], stagePackets[i-1], stagePackets[i-1]) + } + } +} + +func TestPacketIDUniqueness(t *testing.T) { + // Sample of important packet IDs should be unique + packets := []PacketID{ + MSG_HEAD, + MSG_SYS_END, + MSG_SYS_NOP, + MSG_SYS_ACK, + MSG_SYS_LOGIN, + MSG_SYS_LOGOUT, + MSG_SYS_PING, + MSG_SYS_TIME, + MSG_SYS_CREATE_STAGE, + MSG_SYS_ENTER_STAGE, + MSG_MHF_SAVEDATA, + MSG_MHF_LOADDATA, + } + + seen := make(map[PacketID]bool) + for _, pkt := range packets { + if seen[pkt] { + t.Errorf("Duplicate PacketID: %s (%d)", pkt, pkt) + } + seen[pkt] = true + } +} + +func TestAcquirePacketIDs(t *testing.T) { + // Verify acquire-related packet IDs exist and are correct type + acquirePackets := []PacketID{ + MSG_MHF_ACQUIRE_DIST_ITEM, + MSG_MHF_ACQUIRE_TITLE, + MSG_MHF_ACQUIRE_ITEM, + MSG_MHF_ACQUIRE_MONTHLY_ITEM, + MSG_MHF_ACQUIRE_CAFE_ITEM, + MSG_MHF_ACQUIRE_GUILD_TRESURE, + } + + for _, pkt := range acquirePackets { + str := pkt.String() + if str == "" { + t.Errorf("PacketID %d should have a string representation", pkt) + } + } +} + +func TestGuildPacketIDs(t *testing.T) { + // Verify guild-related packet IDs + guildPackets := []PacketID{ + MSG_MHF_CREATE_GUILD, + MSG_MHF_OPERATE_GUILD, + MSG_MHF_OPERATE_GUILD_MEMBER, + MSG_MHF_INFO_GUILD, + MSG_MHF_ENUMERATE_GUILD, + MSG_MHF_UPDATE_GUILD, + } + + for _, pkt := range guildPackets { + // All guild packets should be MHF packets + if pkt < MSG_MHF_SAVEDATA { + t.Errorf("Guild packet %s should be an MHF packet (>= 0x60)", pkt) + } + } +} diff --git a/server/channelserver/handlers_register_test.go b/server/channelserver/handlers_register_test.go new file mode 100644 index 000000000..6a265e5ca --- /dev/null +++ b/server/channelserver/handlers_register_test.go @@ -0,0 +1,229 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/mhfpacket" +) + +// createMockServerWithRaviente creates a mock server with raviente and semaphore +// initialized, which the base createMockServer() does not do. +func createMockServerWithRaviente() *Server { + s := createMockServer() + s.raviente = &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + } + s.semaphore = make(map[string]*Semaphore) + return s +} + +func TestRavienteInitialization(t *testing.T) { + r := &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + } + if r == nil { + t.Fatal("Raviente is nil") + } + if len(r.register) != 30 { + t.Errorf("register length = %d, want 30", len(r.register)) + } + if len(r.state) != 30 { + t.Errorf("state length = %d, want 30", len(r.state)) + } + if len(r.support) != 30 { + t.Errorf("support length = %d, want 30", len(r.support)) + } + // All values should be zero-initialized + for i, v := range r.register { + if v != 0 { + t.Errorf("register[%d] = %d, want 0", i, v) + } + } + for i, v := range r.state { + if v != 0 { + t.Errorf("state[%d] = %d, want 0", i, v) + } + } + for i, v := range r.support { + if v != 0 { + t.Errorf("support[%d] = %d, want 0", i, v) + } + } + if r.id != 0 { + t.Errorf("id = %d, want 0", r.id) + } +} + +func TestRavienteMutex(t *testing.T) { + r := &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + } + + // Test that we can lock and unlock without deadlock + r.Lock() + r.register[0] = 42 + r.Unlock() + + r.Lock() + val := r.register[0] + r.Unlock() + + if val != 42 { + t.Errorf("register[0] = %d, want 42", val) + } +} + +func TestRavienteDataAccess(t *testing.T) { + r := &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + } + + // Write and verify register data + r.register[0] = 100 + r.register[4] = 200 + r.register[29] = 300 + + if r.register[0] != 100 { + t.Errorf("register[0] = %d, want 100", r.register[0]) + } + if r.register[4] != 200 { + t.Errorf("register[4] = %d, want 200", r.register[4]) + } + if r.register[29] != 300 { + t.Errorf("register[29] = %d, want 300", r.register[29]) + } + + // Write and verify state data + r.state[0] = 500 + r.state[28] = 600 + + if r.state[0] != 500 { + t.Errorf("state[0] = %d, want 500", r.state[0]) + } + if r.state[28] != 600 { + t.Errorf("state[28] = %d, want 600", r.state[28]) + } + + // Write and verify support data + r.support[0] = 700 + r.support[24] = 800 + + if r.support[0] != 700 { + t.Errorf("support[0] = %d, want 700", r.support[0]) + } + if r.support[24] != 800 { + t.Errorf("support[24] = %d, want 800", r.support[24]) + } +} + +func TestRavienteID(t *testing.T) { + r := &Raviente{ + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + } + + r.id = 12345 + if r.id != 12345 { + t.Errorf("id = %d, want 12345", r.id) + } + + r.id = 0xFFFF + if r.id != 0xFFFF { + t.Errorf("id = %d, want %d", r.id, uint16(0xFFFF)) + } +} + +func TestCreateMockServerWithRaviente(t *testing.T) { + s := createMockServerWithRaviente() + if s == nil { + t.Fatal("createMockServerWithRaviente() returned nil") + } + if s.raviente == nil { + t.Fatal("raviente should not be nil") + } + if s.semaphore == nil { + t.Fatal("semaphore should not be nil") + } + if len(s.raviente.register) != 30 { + t.Errorf("raviente register length = %d, want 30", len(s.raviente.register)) + } + if len(s.raviente.state) != 30 { + t.Errorf("raviente state length = %d, want 30", len(s.raviente.state)) + } + if len(s.raviente.support) != 30 { + t.Errorf("raviente support length = %d, want 30", len(s.raviente.support)) + } +} + +func TestHandlerTableRegistered(t *testing.T) { + s := createMockServer() + if s == nil { + t.Fatal("createMockServer() returned nil") + } + + // Verify handler table is populated + if len(handlerTable) == 0 { + t.Error("handlers table should not be empty") + } + + // Check that key handler types are registered + // (these are critical handlers that must always be present) + criticalHandlers := []string{ + "handleMsgSysCreateStage", + "handleMsgSysStageDestruct", + } + _ = criticalHandlers // We just verify the table is non-empty since handler function names aren't directly accessible + + // Verify minimum handler count + if len(handlerTable) < 50 { + t.Errorf("handlers count = %d, expected at least 50", len(handlerTable)) + } +} + +func TestHandlerTableNilSession(t *testing.T) { + // This test verifies that the handler table exists and has entries + // but doesn't call handlers (which would require a real session) + _ = createMockServer() + + count := 0 + for range handlerTable { + count++ + } + + if count == 0 { + t.Error("No handlers registered") + } +} + +func TestMockServerPacketHandling(t *testing.T) { + s := createMockServerWithRaviente() + session := createMockSession(1, s) + + // Verify the session and server are properly linked + if session.server != s { + t.Error("Session server reference mismatch") + } + + // Verify byteframe can be created for packet construction + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) // AckHandle + if len(bf.Data()) != 4 { + t.Errorf("ByteFrame length = %d, want 4", len(bf.Data())) + } + + // Verify packet types can be instantiated + pkt := &mhfpacket.MsgSysAck{} + if pkt == nil { + t.Error("Failed to create MsgSysAck") + } +} diff --git a/server/entranceserver/entrance_server_test.go b/server/entranceserver/entrance_server_test.go new file mode 100644 index 000000000..c04a8925e --- /dev/null +++ b/server/entranceserver/entrance_server_test.go @@ -0,0 +1,522 @@ +package entranceserver + +import ( + "net" + "testing" + "time" + + _config "erupe-ce/config" + + "go.uber.org/zap" +) + +func TestNewServer(t *testing.T) { + cfg := &Config{ + Logger: nil, + DB: nil, + ErupeConfig: &_config.Config{}, + } + + s := NewServer(cfg) + if s == nil { + t.Fatal("NewServer() returned nil") + } + if s.isShuttingDown { + t.Error("New server should not be shutting down") + } + if s.erupeConfig == nil { + t.Error("erupeConfig should not be nil") + } +} + +func TestNewServerWithNilConfig(t *testing.T) { + cfg := &Config{} + s := NewServer(cfg) + if s == nil { + t.Fatal("NewServer() returned nil for empty config") + } +} + +func TestServerType(t *testing.T) { + s := &Server{} + if s.isShuttingDown { + t.Error("Zero value server should not be shutting down") + } + if s.listener != nil { + t.Error("Zero value server should have nil listener") + } +} + +func TestConfigFields(t *testing.T) { + cfg := &Config{ + Logger: nil, + DB: nil, + ErupeConfig: nil, + } + + if cfg.Logger != nil { + t.Error("Config Logger should be nil") + } + if cfg.DB != nil { + t.Error("Config DB should be nil") + } + if cfg.ErupeConfig != nil { + t.Error("Config ErupeConfig should be nil") + } +} + +func TestServerShutdownFlag(t *testing.T) { + cfg := &Config{ + ErupeConfig: &_config.Config{}, + } + s := NewServer(cfg) + + if s.isShuttingDown { + t.Error("New server should not be shutting down") + } + + s.Lock() + s.isShuttingDown = true + s.Unlock() + + if !s.isShuttingDown { + t.Error("Server should be shutting down after flag is set") + } +} + +func TestServerConfigStorage(t *testing.T) { + erupeConfig := &_config.Config{ + Host: "192.168.1.100", + Entrance: _config.Entrance{ + Enabled: true, + Port: 53310, + Entries: []_config.EntranceServerInfo{ + { + Name: "Test Server", + IP: "127.0.0.1", + Type: 1, + }, + }, + }, + } + + cfg := &Config{ + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + + if s.erupeConfig.Host != "192.168.1.100" { + t.Errorf("Host = %s, want 192.168.1.100", s.erupeConfig.Host) + } + if s.erupeConfig.Entrance.Port != 53310 { + t.Errorf("Entrance.Port = %d, want 53310", s.erupeConfig.Entrance.Port) + } +} + +func TestServerEntranceEntries(t *testing.T) { + entries := []_config.EntranceServerInfo{ + { + Name: "World 1", + IP: "10.0.0.1", + Type: 1, + Recommended: 1, + Channels: []_config.EntranceChannelInfo{ + {Port: 54001, MaxPlayers: 100}, + {Port: 54002, MaxPlayers: 100}, + }, + }, + { + Name: "World 2", + IP: "10.0.0.2", + Type: 2, + Recommended: 0, + Channels: []_config.EntranceChannelInfo{ + {Port: 54003, MaxPlayers: 50}, + }, + }, + } + + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 53310, + Entries: entries, + }, + } + + cfg := &Config{ErupeConfig: erupeConfig} + s := NewServer(cfg) + + if len(s.erupeConfig.Entrance.Entries) != 2 { + t.Errorf("Entries count = %d, want 2", len(s.erupeConfig.Entrance.Entries)) + } + + if s.erupeConfig.Entrance.Entries[0].Name != "World 1" { + t.Errorf("First entry name = %s, want World 1", s.erupeConfig.Entrance.Entries[0].Name) + } + + if len(s.erupeConfig.Entrance.Entries[0].Channels) != 2 { + t.Errorf("First entry channels = %d, want 2", len(s.erupeConfig.Entrance.Entries[0].Channels)) + } +} + +func TestEncryptDecryptRoundTrip(t *testing.T) { + tests := []struct { + name string + data []byte + key byte + }{ + {"empty", []byte{}, 0x00}, + {"single byte", []byte{0x42}, 0x00}, + {"multiple bytes", []byte{0x01, 0x02, 0x03, 0x04}, 0x00}, + {"with key", []byte{0xDE, 0xAD, 0xBE, 0xEF}, 0x55}, + {"max key", []byte{0x01, 0x02}, 0xFF}, + {"long data", make([]byte, 100), 0x42}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypted := EncryptBin8(tt.data, tt.key) + decrypted := DecryptBin8(encrypted, tt.key) + + if len(decrypted) != len(tt.data) { + t.Errorf("decrypted length = %d, want %d", len(decrypted), len(tt.data)) + return + } + + for i := range tt.data { + if decrypted[i] != tt.data[i] { + t.Errorf("decrypted[%d] = 0x%X, want 0x%X", i, decrypted[i], tt.data[i]) + } + } + }) + } +} + +func TestCalcSum32Deterministic(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + + sum1 := CalcSum32(data) + sum2 := CalcSum32(data) + + if sum1 != sum2 { + t.Errorf("CalcSum32 not deterministic: got 0x%X and 0x%X", sum1, sum2) + } +} + +func TestCalcSum32DifferentInputs(t *testing.T) { + data1 := []byte{0x01, 0x02, 0x03} + data2 := []byte{0x01, 0x02, 0x04} + + sum1 := CalcSum32(data1) + sum2 := CalcSum32(data2) + + if sum1 == sum2 { + t.Error("Different inputs should produce different checksums") + } +} + +func TestEncryptBin8KeyVariation(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04} + + enc1 := EncryptBin8(data, 0x00) + enc2 := EncryptBin8(data, 0x01) + enc3 := EncryptBin8(data, 0xFF) + + if bytesEqual(enc1, enc2) { + t.Error("Different keys should produce different encrypted data (0x00 vs 0x01)") + } + if bytesEqual(enc2, enc3) { + t.Error("Different keys should produce different encrypted data (0x01 vs 0xFF)") + } +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestEncryptBin8LengthPreservation(t *testing.T) { + lengths := []int{0, 1, 7, 8, 9, 100, 1000} + + for _, length := range lengths { + data := make([]byte, length) + for i := range data { + data[i] = byte(i % 256) + } + + encrypted := EncryptBin8(data, 0x42) + if len(encrypted) != length { + t.Errorf("EncryptBin8 length %d changed to %d", length, len(encrypted)) + } + } +} + +func TestCalcSum32LargeInput(t *testing.T) { + data := make([]byte, 10000) + for i := range data { + data[i] = byte(i % 256) + } + + sum := CalcSum32(data) + sum2 := CalcSum32(data) + if sum != sum2 { + t.Errorf("CalcSum32 inconsistent for large input: 0x%X vs 0x%X", sum, sum2) + } +} + +func TestServerMutexLocking(t *testing.T) { + cfg := &Config{ErupeConfig: &_config.Config{}} + s := NewServer(cfg) + + s.Lock() + s.isShuttingDown = true + s.Unlock() + + s.Lock() + result := s.isShuttingDown + s.Unlock() + + if !result { + t.Error("Mutex should protect isShuttingDown flag") + } +} + +func TestServerStartAndShutdown(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + + if s.listener == nil { + t.Error("Server listener should not be nil after Start()") + } + + s.Lock() + if s.isShuttingDown { + t.Error("Server should not be shutting down after Start()") + } + s.Unlock() + + s.Shutdown() + + s.Lock() + if !s.isShuttingDown { + t.Error("Server should be shutting down after Shutdown()") + } + s.Unlock() +} + +func TestServerStartWithInvalidPort(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Port: 1, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err == nil { + s.Shutdown() + t.Error("Start() should fail with invalid port") + } +} + +func TestServerListenerAddress(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr() + if addr == nil { + t.Error("Listener address should not be nil") + } + + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + t.Error("Listener address should be a TCP address") + } + + if tcpAddr.Port == 0 { + t.Error("Listener port should be assigned") + } +} + +func TestServerAcceptClientsExitsOnShutdown(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + + time.Sleep(10 * time.Millisecond) + + s.Shutdown() + + time.Sleep(10 * time.Millisecond) + + s.Lock() + if !s.isShuttingDown { + t.Error("Server should be marked as shutting down") + } + s.Unlock() +} + +func TestServerHandleConnectionImmediateClose(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() error: %v", err) + } + conn.Close() + + time.Sleep(50 * time.Millisecond) +} + +func TestServerHandleConnectionShortInit(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() error: %v", err) + } + _, _ = conn.Write([]byte{0, 0, 0, 0}) + conn.Close() + + time.Sleep(50 * time.Millisecond) +} + +func TestServerMultipleConnections(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Entrance: _config.Entrance{ + Enabled: true, + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + + conns := make([]net.Conn, 3) + for i := range conns { + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() %d error: %v", i, err) + } + conns[i] = conn + } + + time.Sleep(50 * time.Millisecond) + + for _, conn := range conns { + conn.Close() + } +} diff --git a/server/signserver/dbutils_test.go b/server/signserver/dbutils_test.go new file mode 100644 index 000000000..b3c18443f --- /dev/null +++ b/server/signserver/dbutils_test.go @@ -0,0 +1,825 @@ +package signserver + +import ( + "database/sql" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +func TestCharacterStruct(t *testing.T) { + c := character{ + ID: 12345, + IsFemale: true, + IsNewCharacter: false, + Name: "TestHunter", + UnkDescString: "Test description", + HR: 999, + GR: 300, + WeaponType: 5, + LastLogin: 1700000000, + } + + if c.ID != 12345 { + t.Errorf("ID = %d, want 12345", c.ID) + } + if c.IsFemale != true { + t.Error("IsFemale should be true") + } + if c.IsNewCharacter != false { + t.Error("IsNewCharacter should be false") + } + if c.Name != "TestHunter" { + t.Errorf("Name = %s, want TestHunter", c.Name) + } + if c.UnkDescString != "Test description" { + t.Errorf("UnkDescString = %s, want Test description", c.UnkDescString) + } + if c.HR != 999 { + t.Errorf("HR = %d, want 999", c.HR) + } + if c.GR != 300 { + t.Errorf("GR = %d, want 300", c.GR) + } + if c.WeaponType != 5 { + t.Errorf("WeaponType = %d, want 5", c.WeaponType) + } + if c.LastLogin != 1700000000 { + t.Errorf("LastLogin = %d, want 1700000000", c.LastLogin) + } +} + +func TestCharacterStructDefaults(t *testing.T) { + c := character{} + + if c.ID != 0 { + t.Errorf("default ID = %d, want 0", c.ID) + } + if c.IsFemale != false { + t.Error("default IsFemale should be false") + } + if c.IsNewCharacter != false { + t.Error("default IsNewCharacter should be false") + } + if c.Name != "" { + t.Errorf("default Name = %s, want empty", c.Name) + } + if c.HR != 0 { + t.Errorf("default HR = %d, want 0", c.HR) + } + if c.GR != 0 { + t.Errorf("default GR = %d, want 0", c.GR) + } + if c.WeaponType != 0 { + t.Errorf("default WeaponType = %d, want 0", c.WeaponType) + } +} + +func TestMembersStruct(t *testing.T) { + m := members{ + CID: 100, + ID: 200, + Name: "FriendName", + } + + if m.CID != 100 { + t.Errorf("CID = %d, want 100", m.CID) + } + if m.ID != 200 { + t.Errorf("ID = %d, want 200", m.ID) + } + if m.Name != "FriendName" { + t.Errorf("Name = %s, want FriendName", m.Name) + } +} + +func TestMembersStructDefaults(t *testing.T) { + m := members{} + + if m.CID != 0 { + t.Errorf("default CID = %d, want 0", m.CID) + } + if m.ID != 0 { + t.Errorf("default ID = %d, want 0", m.ID) + } + if m.Name != "" { + t.Errorf("default Name = %s, want empty", m.Name) + } +} + +func TestCharacterWeaponTypes(t *testing.T) { + weaponTypes := []uint16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} + + for _, wt := range weaponTypes { + c := character{WeaponType: wt} + if c.WeaponType != wt { + t.Errorf("WeaponType = %d, want %d", c.WeaponType, wt) + } + } +} + +func TestCharacterHRRange(t *testing.T) { + tests := []struct { + name string + hr uint16 + }{ + {"min", 0}, + {"beginner", 1}, + {"hr30", 30}, + {"hr50", 50}, + {"hr99", 99}, + {"hr299", 299}, + {"hr998", 998}, + {"hr999", 999}, + {"max uint16", 65535}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{HR: tt.hr} + if c.HR != tt.hr { + t.Errorf("HR = %d, want %d", c.HR, tt.hr) + } + }) + } +} + +func TestCharacterGRRange(t *testing.T) { + tests := []struct { + name string + gr uint16 + }{ + {"min", 0}, + {"gr1", 1}, + {"gr100", 100}, + {"gr300", 300}, + {"gr999", 999}, + {"max uint16", 65535}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{GR: tt.gr} + if c.GR != tt.gr { + t.Errorf("GR = %d, want %d", c.GR, tt.gr) + } + }) + } +} + +func TestCharacterIDRange(t *testing.T) { + tests := []struct { + name string + id uint32 + }{ + {"min", 0}, + {"small", 1}, + {"medium", 1000000}, + {"large", 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{ID: tt.id} + if c.ID != tt.id { + t.Errorf("ID = %d, want %d", c.ID, tt.id) + } + }) + } +} + +func TestCharacterGender(t *testing.T) { + male := character{IsFemale: false} + if male.IsFemale != false { + t.Error("Male character should have IsFemale = false") + } + + female := character{IsFemale: true} + if female.IsFemale != true { + t.Error("Female character should have IsFemale = true") + } +} + +func TestCharacterNewStatus(t *testing.T) { + newChar := character{IsNewCharacter: true} + if newChar.IsNewCharacter != true { + t.Error("New character should have IsNewCharacter = true") + } + + existingChar := character{IsNewCharacter: false} + if existingChar.IsNewCharacter != false { + t.Error("Existing character should have IsNewCharacter = false") + } +} + +func TestCharacterNameLength(t *testing.T) { + names := []string{ + "", + "A", + "Hunter", + "LongHunterName123", + } + + for _, name := range names { + c := character{Name: name} + if c.Name != name { + t.Errorf("Name = %s, want %s", c.Name, name) + } + } +} + +func TestCharacterLastLogin(t *testing.T) { + tests := []struct { + name string + lastLogin uint32 + }{ + {"zero", 0}, + {"past", 1600000000}, + {"present", 1700000000}, + {"future", 1800000000}, + {"max", 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{LastLogin: tt.lastLogin} + if c.LastLogin != tt.lastLogin { + t.Errorf("LastLogin = %d, want %d", c.LastLogin, tt.lastLogin) + } + }) + } +} + +func TestMembersCIDAssignment(t *testing.T) { + m := members{CID: 12345} + if m.CID != 12345 { + t.Errorf("CID = %d, want 12345", m.CID) + } +} + +func TestMultipleCharacters(t *testing.T) { + chars := []character{ + {ID: 1, Name: "Char1", HR: 100}, + {ID: 2, Name: "Char2", HR: 200}, + {ID: 3, Name: "Char3", HR: 300}, + } + + for i, c := range chars { + expectedID := uint32(i + 1) + if c.ID != expectedID { + t.Errorf("chars[%d].ID = %d, want %d", i, c.ID, expectedID) + } + } +} + +func TestMultipleMembers(t *testing.T) { + membersList := []members{ + {CID: 1, ID: 10, Name: "Friend1"}, + {CID: 1, ID: 20, Name: "Friend2"}, + {CID: 2, ID: 30, Name: "Friend3"}, + } + + if membersList[0].CID != membersList[1].CID { + t.Error("First two members should share the same CID") + } + + if membersList[1].CID == membersList[2].CID { + t.Error("Third member should have different CID") + } +} + +// Helper to create a test server with mocked database +func newTestServerWithMock(t *testing.T) (*Server, sqlmock.Sqlmock) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create sqlmock: %v", err) + } + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + server := &Server{ + logger: zap.NewNop(), + db: sqlxDB, + } + + return server, mock +} + +func TestGetCharactersForUser(t *testing.T) { + server, mock := newTestServerWithMock(t) + + rows := sqlmock.NewRows([]string{"id", "is_female", "is_new_character", "name", "unk_desc_string", "hr", "gr", "weapon_type", "last_login"}). + AddRow(1, false, false, "Hunter1", "desc1", 100, 50, 3, 1700000000). + AddRow(2, true, false, "Hunter2", "desc2", 200, 100, 7, 1700000001) + + mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hr, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id"). + WithArgs(uint32(1)). + WillReturnRows(rows) + + chars, err := server.getCharactersForUser(1) + if err != nil { + t.Errorf("getCharactersForUser() error: %v", err) + } + + if len(chars) != 2 { + t.Errorf("getCharactersForUser() returned %d characters, want 2", len(chars)) + } + + if chars[0].Name != "Hunter1" { + t.Errorf("First character name = %s, want Hunter1", chars[0].Name) + } + + if chars[1].IsFemale != true { + t.Error("Second character should be female") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetCharactersForUserNoCharacters(t *testing.T) { + server, mock := newTestServerWithMock(t) + + rows := sqlmock.NewRows([]string{"id", "is_female", "is_new_character", "name", "unk_desc_string", "hr", "gr", "weapon_type", "last_login"}) + + mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hr, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id"). + WithArgs(uint32(1)). + WillReturnRows(rows) + + chars, err := server.getCharactersForUser(1) + if err != nil { + t.Errorf("getCharactersForUser() error: %v", err) + } + + if len(chars) != 0 { + t.Errorf("getCharactersForUser() returned %d characters, want 0", len(chars)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetCharactersForUserDBError(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hr, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id"). + WithArgs(uint32(1)). + WillReturnError(sql.ErrConnDone) + + _, err := server.getCharactersForUser(1) + if err == nil { + t.Error("getCharactersForUser() should return error") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetLastCID(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT last_character FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"last_character"}).AddRow(12345)) + + lastCID := server.getLastCID(1) + if lastCID != 12345 { + t.Errorf("getLastCID() = %d, want 12345", lastCID) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetLastCIDNoResult(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT last_character FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnError(sql.ErrNoRows) + + lastCID := server.getLastCID(1) + if lastCID != 0 { + t.Errorf("getLastCID() with no result = %d, want 0", lastCID) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetUserRights(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT rights FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"rights"}).AddRow(30)) + + rights := server.getUserRights(1) + if rights == 0 { + t.Error("getUserRights() should return non-zero value") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetReturnExpiry(t *testing.T) { + server, mock := newTestServerWithMock(t) + + recentLogin := time.Now().Add(-time.Hour * 24) + mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(recentLogin)) + + mock.ExpectQuery("SELECT return_expires FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"return_expires"}).AddRow(time.Now().Add(time.Hour * 24 * 30))) + + mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2"). + WithArgs(sqlmock.AnyArg(), uint32(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + expiry := server.getReturnExpiry(1) + + if expiry.Before(time.Now()) { + t.Error("getReturnExpiry() should return future date") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetReturnExpiryInactiveUser(t *testing.T) { + server, mock := newTestServerWithMock(t) + + oldLogin := time.Now().Add(-time.Hour * 24 * 100) + mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(oldLogin)) + + mock.ExpectExec("UPDATE users SET return_expires=\\$1 WHERE id=\\$2"). + WithArgs(sqlmock.AnyArg(), uint32(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2"). + WithArgs(sqlmock.AnyArg(), uint32(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + expiry := server.getReturnExpiry(1) + + if expiry.Before(time.Now()) { + t.Error("getReturnExpiry() should return future date for inactive user") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetReturnExpiryDBError(t *testing.T) { + server, mock := newTestServerWithMock(t) + + recentLogin := time.Now().Add(-time.Hour * 24) + mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(recentLogin)) + + mock.ExpectQuery("SELECT return_expires FROM users WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnError(sql.ErrNoRows) + + mock.ExpectExec("UPDATE users SET return_expires=\\$1 WHERE id=\\$2"). + WithArgs(sqlmock.AnyArg(), uint32(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2"). + WithArgs(sqlmock.AnyArg(), uint32(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + expiry := server.getReturnExpiry(1) + + if expiry.IsZero() { + t.Error("getReturnExpiry() should return non-zero time even on error") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestNewUserChara(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM characters WHERE user_id = \\$1 AND is_new_character = true"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + + mock.ExpectExec("INSERT INTO characters"). + WithArgs(uint32(1), sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := server.newUserChara(1) + if err != nil { + t.Errorf("newUserChara() error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestNewUserCharaAlreadyHasNewChar(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM characters WHERE user_id = \\$1 AND is_new_character = true"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + err := server.newUserChara(1) + if err != nil { + t.Errorf("newUserChara() should return nil when user already has new char: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestNewUserCharaCountError(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM characters WHERE user_id = \\$1 AND is_new_character = true"). + WithArgs(uint32(1)). + WillReturnError(sql.ErrConnDone) + + err := server.newUserChara(1) + if err == nil { + t.Error("newUserChara() should return error when count query fails") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestNewUserCharaInsertError(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM characters WHERE user_id = \\$1 AND is_new_character = true"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + + mock.ExpectExec("INSERT INTO characters"). + WithArgs(uint32(1), sqlmock.AnyArg()). + WillReturnError(sql.ErrConnDone) + + err := server.newUserChara(1) + if err == nil { + t.Error("newUserChara() should return error when insert fails") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestRegisterDBAccount(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("INSERT INTO users \\(username, password, return_expires\\) VALUES \\(\\$1, \\$2, \\$3\\) RETURNING id"). + WithArgs("newuser", sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + + uid, err := server.registerDBAccount("newuser", "password123") + if err != nil { + t.Errorf("registerDBAccount() error: %v", err) + } + if uid != 1 { + t.Errorf("registerDBAccount() uid = %d, want 1", uid) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestRegisterDBAccountDuplicateUser(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("INSERT INTO users \\(username, password, return_expires\\) VALUES \\(\\$1, \\$2, \\$3\\) RETURNING id"). + WithArgs("existinguser", sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnError(sql.ErrNoRows) + + _, err := server.registerDBAccount("existinguser", "password123") + if err == nil { + t.Error("registerDBAccount() should return error for duplicate user") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestDeleteCharacter(t *testing.T) { + server, mock := newTestServerWithMock(t) + + // validateToken: SELECT count(*) FROM sign_sessions WHERE token = $1 + // When tokenID=0, query has no AND clause but both args are still passed to QueryRow + mock.ExpectQuery("SELECT count\\(\\*\\) FROM sign_sessions WHERE token = \\$1"). + WithArgs("validtoken", uint32(0)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + mock.ExpectQuery("SELECT is_new_character FROM characters WHERE id = \\$1"). + WithArgs(123). + WillReturnRows(sqlmock.NewRows([]string{"is_new_character"}).AddRow(false)) + + mock.ExpectExec("UPDATE characters SET deleted = true WHERE id = \\$1"). + WithArgs(123). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := server.deleteCharacter(123, "validtoken", 0) + if err != nil { + t.Errorf("deleteCharacter() error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestDeleteNewCharacter(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM sign_sessions WHERE token = \\$1"). + WithArgs("validtoken", uint32(0)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + mock.ExpectQuery("SELECT is_new_character FROM characters WHERE id = \\$1"). + WithArgs(123). + WillReturnRows(sqlmock.NewRows([]string{"is_new_character"}).AddRow(true)) + + mock.ExpectExec("DELETE FROM characters WHERE id = \\$1"). + WithArgs(123). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := server.deleteCharacter(123, "validtoken", 0) + if err != nil { + t.Errorf("deleteCharacter() error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestDeleteCharacterInvalidToken(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM sign_sessions WHERE token = \\$1"). + WithArgs("invalidtoken", uint32(0)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + + err := server.deleteCharacter(123, "invalidtoken", 0) + if err == nil { + t.Error("deleteCharacter() should return error for invalid token") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestDeleteCharacterDeleteError(t *testing.T) { + server, mock := newTestServerWithMock(t) + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM sign_sessions WHERE token = \\$1"). + WithArgs("validtoken", uint32(0)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + mock.ExpectQuery("SELECT is_new_character FROM characters WHERE id = \\$1"). + WithArgs(123). + WillReturnRows(sqlmock.NewRows([]string{"is_new_character"}).AddRow(false)) + + mock.ExpectExec("UPDATE characters SET deleted = true WHERE id = \\$1"). + WithArgs(123). + WillReturnError(sql.ErrConnDone) + + err := server.deleteCharacter(123, "validtoken", 0) + if err == nil { + t.Error("deleteCharacter() should return error when update fails") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetFriendsForCharactersEmpty(t *testing.T) { + server, _ := newTestServerWithMock(t) + + chars := []character{} + + friends := server.getFriendsForCharacters(chars) + if len(friends) != 0 { + t.Errorf("getFriendsForCharacters() for empty chars = %d, want 0", len(friends)) + } +} + +func TestGetGuildmatesForCharactersEmpty(t *testing.T) { + server, _ := newTestServerWithMock(t) + + chars := []character{} + + guildmates := server.getGuildmatesForCharacters(chars) + if len(guildmates) != 0 { + t.Errorf("getGuildmatesForCharacters() for empty chars = %d, want 0", len(guildmates)) + } +} + +func TestGetFriendsForCharacters(t *testing.T) { + server, mock := newTestServerWithMock(t) + + chars := []character{ + {ID: 1, Name: "Hunter1"}, + } + + mock.ExpectQuery("SELECT friends FROM characters WHERE id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"friends"}).AddRow("2,3")) + + mock.ExpectQuery("SELECT id, name FROM characters WHERE id=2 OR id=3"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(2, "Friend1"). + AddRow(3, "Friend2")) + + friends := server.getFriendsForCharacters(chars) + if len(friends) != 2 { + t.Errorf("getFriendsForCharacters() = %d, want 2", len(friends)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetGuildmatesForCharacters(t *testing.T) { + server, mock := newTestServerWithMock(t) + + chars := []character{ + {ID: 1, Name: "Hunter1"}, + } + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM guild_characters WHERE character_id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + mock.ExpectQuery("SELECT guild_id FROM guild_characters WHERE character_id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"guild_id"}).AddRow(100)) + + mock.ExpectQuery("SELECT character_id AS id, c.name FROM guild_characters gc JOIN characters c ON c.id = gc.character_id WHERE guild_id=\\$1 AND character_id!=\\$2"). + WithArgs(100, uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(2, "Guildmate1"). + AddRow(3, "Guildmate2")) + + guildmates := server.getGuildmatesForCharacters(chars) + if len(guildmates) != 2 { + t.Errorf("getGuildmatesForCharacters() = %d, want 2", len(guildmates)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} + +func TestGetGuildmatesNotInGuild(t *testing.T) { + server, mock := newTestServerWithMock(t) + + chars := []character{ + {ID: 1, Name: "Hunter1"}, + } + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM guild_characters WHERE character_id=\\$1"). + WithArgs(uint32(1)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + + guildmates := server.getGuildmatesForCharacters(chars) + if len(guildmates) != 0 { + t.Errorf("getGuildmatesForCharacters() for non-guild member = %d, want 0", len(guildmates)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %v", err) + } +} diff --git a/server/signserver/session_test.go b/server/signserver/session_test.go new file mode 100644 index 000000000..8955b7874 --- /dev/null +++ b/server/signserver/session_test.go @@ -0,0 +1,393 @@ +package signserver + +import ( + "bytes" + "io" + "net" + "sync" + "testing" + "time" + + "erupe-ce/common/byteframe" + _config "erupe-ce/config" + "erupe-ce/network" + + "go.uber.org/zap" +) + +// mockConn implements net.Conn for testing +type mockConn struct { + readBuf *bytes.Buffer + writeBuf *bytes.Buffer + closed bool + mu sync.Mutex +} + +func newMockConn() *mockConn { + return &mockConn{ + readBuf: new(bytes.Buffer), + writeBuf: new(bytes.Buffer), + } +} + +func (m *mockConn) Read(b []byte) (n int, err error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return 0, io.EOF + } + return m.readBuf.Read(b) +} + +func (m *mockConn) Write(b []byte) (n int, err error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return 0, io.ErrClosedPipe + } + return m.writeBuf.Write(b) +} + +func (m *mockConn) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + m.closed = true + return nil +} + +func (m *mockConn) LocalAddr() net.Addr { + return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53312} +} + +func (m *mockConn) RemoteAddr() net.Addr { + return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345} +} + +func (m *mockConn) SetDeadline(t time.Time) error { return nil } +func (m *mockConn) SetReadDeadline(t time.Time) error { return nil } +func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil } + +func TestSessionStruct(t *testing.T) { + logger := zap.NewNop() + conn := newMockConn() + + s := &Session{ + logger: logger, + server: nil, + rawConn: conn, + cryptConn: network.NewCryptConn(conn), + } + + if s.logger != logger { + t.Error("Session logger not set correctly") + } + if s.rawConn != conn { + t.Error("Session rawConn not set correctly") + } + if s.cryptConn == nil { + t.Error("Session cryptConn should not be nil") + } +} + +func TestSessionStructDefaults(t *testing.T) { + s := &Session{} + + if s.logger != nil { + t.Error("Default Session logger should be nil") + } + if s.server != nil { + t.Error("Default Session server should be nil") + } + if s.rawConn != nil { + t.Error("Default Session rawConn should be nil") + } + if s.cryptConn != nil { + t.Error("Default Session cryptConn should be nil") + } +} + +func TestSessionMutex(t *testing.T) { + s := &Session{} + + s.Lock() + s.Unlock() + + done := make(chan bool) + go func() { + s.Lock() + time.Sleep(10 * time.Millisecond) + s.Unlock() + done <- true + }() + + time.Sleep(5 * time.Millisecond) + + s.Lock() + s.Unlock() + + <-done +} + +func TestHandlePacketUnknownRequest(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + } + + server := &Server{ + logger: logger, + erupeConfig: erupeConfig, + } + + conn := newMockConn() + session := &Session{ + logger: logger, + server: server, + rawConn: conn, + cryptConn: network.NewCryptConn(conn), + } + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("UNKNOWN:100")) + bf.WriteNullTerminatedBytes([]byte("data")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Errorf("handlePacket() returned error: %v", err) + } +} + +func TestHandlePacketWithDevModeLogging(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + DebugOptions: _config.DebugOptions{ + LogInboundMessages: true, + }, + } + + server := &Server{ + logger: logger, + erupeConfig: erupeConfig, + } + + conn := newMockConn() + session := &Session{ + logger: logger, + server: server, + rawConn: conn, + cryptConn: network.NewCryptConn(conn), + } + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("TEST:100")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Errorf("handlePacket() with dev mode returned error: %v", err) + } +} + +func TestHandlePacketRequestTypes(t *testing.T) { + tests := []struct { + name string + reqType string + }{ + {"unknown", "UNKNOWN:100"}, + {"invalid", "INVALID"}, + {"empty_version", "TEST:"}, + {"no_version", "NOVERSION"}, + {"special_chars", "TEST@#$:100"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{} + server := &Server{ + logger: logger, + erupeConfig: erupeConfig, + } + + conn := newMockConn() + session := &Session{ + logger: logger, + server: server, + rawConn: conn, + cryptConn: network.NewCryptConn(conn), + } + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte(tt.reqType)) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Errorf("handlePacket(%s) returned error: %v", tt.reqType, err) + } + }) + } +} + +func TestMockConnImplementsNetConn(t *testing.T) { + var _ net.Conn = (*mockConn)(nil) +} + +func TestMockConnReadWrite(t *testing.T) { + conn := newMockConn() + + testData := []byte("hello") + conn.readBuf.Write(testData) + + buf := make([]byte, len(testData)) + n, err := conn.Read(buf) + if err != nil { + t.Errorf("Read() error: %v", err) + } + if n != len(testData) { + t.Errorf("Read() n = %d, want %d", n, len(testData)) + } + if !bytes.Equal(buf, testData) { + t.Errorf("Read() data = %v, want %v", buf, testData) + } + + outData := []byte("world") + n, err = conn.Write(outData) + if err != nil { + t.Errorf("Write() error: %v", err) + } + if n != len(outData) { + t.Errorf("Write() n = %d, want %d", n, len(outData)) + } + if !bytes.Equal(conn.writeBuf.Bytes(), outData) { + t.Errorf("Write() buffer = %v, want %v", conn.writeBuf.Bytes(), outData) + } +} + +func TestMockConnClose(t *testing.T) { + conn := newMockConn() + + err := conn.Close() + if err != nil { + t.Errorf("Close() error: %v", err) + } + + if !conn.closed { + t.Error("conn.closed should be true after Close()") + } + + buf := make([]byte, 10) + _, err = conn.Read(buf) + if err != io.EOF { + t.Errorf("Read() after close should return EOF, got: %v", err) + } + + _, err = conn.Write([]byte("test")) + if err != io.ErrClosedPipe { + t.Errorf("Write() after close should return ErrClosedPipe, got: %v", err) + } +} + +func TestMockConnAddresses(t *testing.T) { + conn := newMockConn() + + local := conn.LocalAddr() + if local == nil { + t.Error("LocalAddr() should not be nil") + } + if local.String() != "127.0.0.1:53312" { + t.Errorf("LocalAddr() = %s, want 127.0.0.1:53312", local.String()) + } + + remote := conn.RemoteAddr() + if remote == nil { + t.Error("RemoteAddr() should not be nil") + } + if remote.String() != "127.0.0.1:12345" { + t.Errorf("RemoteAddr() = %s, want 127.0.0.1:12345", remote.String()) + } +} + +func TestMockConnDeadlines(t *testing.T) { + conn := newMockConn() + deadline := time.Now().Add(time.Second) + + if err := conn.SetDeadline(deadline); err != nil { + t.Errorf("SetDeadline() error: %v", err) + } + if err := conn.SetReadDeadline(deadline); err != nil { + t.Errorf("SetReadDeadline() error: %v", err) + } + if err := conn.SetWriteDeadline(deadline); err != nil { + t.Errorf("SetWriteDeadline() error: %v", err) + } +} + +func TestSessionWithCryptConn(t *testing.T) { + conn := newMockConn() + cryptConn := network.NewCryptConn(conn) + + if cryptConn == nil { + t.Fatal("NewCryptConn() returned nil") + } + + session := &Session{ + rawConn: conn, + cryptConn: cryptConn, + } + + if session.cryptConn != cryptConn { + t.Error("Session cryptConn not set correctly") + } +} + +func TestSessionWorkWithDevModeLogging(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + DebugOptions: _config.DebugOptions{ + LogInboundMessages: true, + }, + } + + server := &Server{ + logger: logger, + erupeConfig: erupeConfig, + } + + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + defer serverConn.Close() + + session := &Session{ + logger: logger, + server: server, + rawConn: serverConn, + cryptConn: network.NewCryptConn(serverConn), + } + + clientConn.Close() + + session.work() +} + +func TestSessionWorkWithEmptyRead(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + } + + server := &Server{ + logger: logger, + erupeConfig: erupeConfig, + } + + clientConn, serverConn := net.Pipe() + defer serverConn.Close() + + session := &Session{ + logger: logger, + server: server, + rawConn: serverConn, + cryptConn: network.NewCryptConn(serverConn), + } + + clientConn.Close() + + session.work() +} diff --git a/server/signserver/sign_server_test.go b/server/signserver/sign_server_test.go new file mode 100644 index 000000000..d52d40767 --- /dev/null +++ b/server/signserver/sign_server_test.go @@ -0,0 +1,582 @@ +package signserver + +import ( + "fmt" + "net" + "testing" + "time" + + _config "erupe-ce/config" + + "go.uber.org/zap" +) + +// makeSignInFailureResp creates a 1-byte failure response for the given RespID. +func makeSignInFailureResp(id RespID) []byte { + return []byte{uint8(id)} +} + +func TestRespIDConstants(t *testing.T) { + tests := []struct { + respID RespID + value uint8 + }{ + {SIGN_UNKNOWN, 0}, + {SIGN_SUCCESS, 1}, + {SIGN_EFAILED, 2}, + {SIGN_EILLEGAL, 3}, + {SIGN_EALERT, 4}, + {SIGN_EABORT, 5}, + {SIGN_ERESPONSE, 6}, + {SIGN_EDATABASE, 7}, + {SIGN_EABSENCE, 8}, + {SIGN_ERESIGN, 9}, + {SIGN_ESUSPEND_D, 10}, + {SIGN_ELOCK, 11}, + {SIGN_EPASS, 12}, + {SIGN_ERIGHT, 13}, + {SIGN_EAUTH, 14}, + {SIGN_ESUSPEND, 15}, + {SIGN_EELIMINATE, 16}, + {SIGN_ECLOSE, 17}, + {SIGN_ECLOSE_EX, 18}, + {SIGN_EINTERVAL, 19}, + {SIGN_EMOVED, 20}, + {SIGN_ENOTREADY, 21}, + {SIGN_EALREADY, 22}, + {SIGN_EIPADDR, 23}, + {SIGN_EHANGAME, 24}, + {SIGN_UPD_ONLY, 25}, + {SIGN_EMBID, 26}, + {SIGN_ECOGCODE, 27}, + {SIGN_ETOKEN, 28}, + {SIGN_ECOGLINK, 29}, + {SIGN_EMAINTE, 30}, + {SIGN_EMAINTE_NOUPDATE, 31}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("RespID_%d", tt.value), func(t *testing.T) { + if uint8(tt.respID) != tt.value { + t.Errorf("RespID = %d, want %d", uint8(tt.respID), tt.value) + } + }) + } +} + +func TestRespIDType(t *testing.T) { + var r RespID = 0xFF + if uint8(r) != 0xFF { + t.Errorf("RespID max value = %d, want %d", uint8(r), 0xFF) + } +} + +func TestMakeSignInFailureResp(t *testing.T) { + tests := []RespID{ + SIGN_UNKNOWN, + SIGN_EFAILED, + SIGN_EILLEGAL, + SIGN_ESUSPEND, + SIGN_EELIMINATE, + SIGN_EIPADDR, + } + + for _, respID := range tests { + t.Run(fmt.Sprintf("RespID_%d", respID), func(t *testing.T) { + resp := makeSignInFailureResp(respID) + + if len(resp) != 1 { + t.Errorf("makeSignInFailureResp() len = %d, want 1", len(resp)) + } + if resp[0] != uint8(respID) { + t.Errorf("makeSignInFailureResp() = %d, want %d", resp[0], uint8(respID)) + } + }) + } +} + +func TestMakeSignInFailureRespAllCodes(t *testing.T) { + for i := uint8(0); i <= 40; i++ { + resp := makeSignInFailureResp(RespID(i)) + if len(resp) != 1 { + t.Errorf("makeSignInFailureResp(%d) len = %d, want 1", i, len(resp)) + } + if resp[0] != i { + t.Errorf("makeSignInFailureResp(%d) = %d", i, resp[0]) + } + } +} + +func TestSignSuccessIsOne(t *testing.T) { + if SIGN_SUCCESS != 1 { + t.Errorf("SIGN_SUCCESS = %d, must be 1", SIGN_SUCCESS) + } +} + +func TestSignUnknownIsZero(t *testing.T) { + if SIGN_UNKNOWN != 0 { + t.Errorf("SIGN_UNKNOWN = %d, must be 0", SIGN_UNKNOWN) + } +} + +func TestRespIDValues(t *testing.T) { + tests := []struct { + name string + respID RespID + value uint8 + }{ + {"SIGN_UNKNOWN", SIGN_UNKNOWN, 0}, + {"SIGN_SUCCESS", SIGN_SUCCESS, 1}, + {"SIGN_EFAILED", SIGN_EFAILED, 2}, + {"SIGN_EILLEGAL", SIGN_EILLEGAL, 3}, + {"SIGN_ESUSPEND", SIGN_ESUSPEND, 15}, + {"SIGN_EELIMINATE", SIGN_EELIMINATE, 16}, + {"SIGN_EIPADDR", SIGN_EIPADDR, 23}, + {"SIGN_EMAINTE", SIGN_EMAINTE, 30}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if uint8(tt.respID) != tt.value { + t.Errorf("%s = %d, want %d", tt.name, uint8(tt.respID), tt.value) + } + }) + } +} + +func TestUnknownRespIDRange(t *testing.T) { + unknownIDs := []RespID{UNK_32, UNK_33, UNK_34, UNK_35} + expectedValues := []uint8{32, 33, 34, 35} + + for i, id := range unknownIDs { + if uint8(id) != expectedValues[i] { + t.Errorf("Unknown ID %d = %d, want %d", i, uint8(id), expectedValues[i]) + } + } +} + +func TestSpecialRespIDs(t *testing.T) { + if SIGN_XBRESPONSE != 36 { + t.Errorf("SIGN_XBRESPONSE = %d, want 36", SIGN_XBRESPONSE) + } + if SIGN_EPSI != 37 { + t.Errorf("SIGN_EPSI = %d, want 37", SIGN_EPSI) + } + if SIGN_EMBID_PSI != 38 { + t.Errorf("SIGN_EMBID_PSI = %d, want 38", SIGN_EMBID_PSI) + } +} + +func TestMakeSignInFailureRespBoundary(t *testing.T) { + resp := makeSignInFailureResp(RespID(0)) + if resp[0] != 0 { + t.Errorf("makeSignInFailureResp(0) = %d, want 0", resp[0]) + } + + resp = makeSignInFailureResp(RespID(255)) + if resp[0] != 255 { + t.Errorf("makeSignInFailureResp(255) = %d, want 255", resp[0]) + } +} + +func TestErrorRespIDsAreDifferent(t *testing.T) { + seen := make(map[RespID]bool) + errorCodes := []RespID{ + SIGN_UNKNOWN, SIGN_SUCCESS, SIGN_EFAILED, SIGN_EILLEGAL, + SIGN_EALERT, SIGN_EABORT, SIGN_ERESPONSE, SIGN_EDATABASE, + SIGN_EABSENCE, SIGN_ERESIGN, SIGN_ESUSPEND_D, SIGN_ELOCK, + SIGN_EPASS, SIGN_ERIGHT, SIGN_EAUTH, SIGN_ESUSPEND, + SIGN_EELIMINATE, SIGN_ECLOSE, SIGN_ECLOSE_EX, SIGN_EINTERVAL, + SIGN_EMOVED, SIGN_ENOTREADY, SIGN_EALREADY, SIGN_EIPADDR, + SIGN_EHANGAME, SIGN_UPD_ONLY, SIGN_EMBID, SIGN_ECOGCODE, + SIGN_ETOKEN, SIGN_ECOGLINK, SIGN_EMAINTE, SIGN_EMAINTE_NOUPDATE, + } + + for _, code := range errorCodes { + if seen[code] { + t.Errorf("Duplicate RespID value: %d", code) + } + seen[code] = true + } +} + +func TestFailureRespIsMinimal(t *testing.T) { + for i := RespID(0); i <= SIGN_EMBID_PSI; i++ { + if i == SIGN_SUCCESS { + continue + } + resp := makeSignInFailureResp(i) + if len(resp) != 1 { + t.Errorf("makeSignInFailureResp(%d) should be 1 byte, got %d", i, len(resp)) + } + } +} + +func TestNewServer(t *testing.T) { + cfg := &Config{ + Logger: nil, + DB: nil, + ErupeConfig: nil, + } + + s := NewServer(cfg) + if s == nil { + t.Fatal("NewServer() returned nil") + } + if s.isShuttingDown { + t.Error("New server should not be shutting down") + } +} + +func TestNewServerWithNilConfig(t *testing.T) { + cfg := &Config{} + s := NewServer(cfg) + if s == nil { + t.Fatal("NewServer() returned nil for empty config") + } +} + +func TestServerType(t *testing.T) { + s := &Server{} + if s.isShuttingDown { + t.Error("Zero value server should not be shutting down") + } +} + +func TestConfigFields(t *testing.T) { + cfg := &Config{ + Logger: nil, + DB: nil, + ErupeConfig: nil, + } + + if cfg.Logger != nil { + t.Error("Config Logger should be nil") + } + if cfg.DB != nil { + t.Error("Config DB should be nil") + } + if cfg.ErupeConfig != nil { + t.Error("Config ErupeConfig should be nil") + } +} + +func TestServerStartAndShutdown(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + if s == nil { + t.Fatal("NewServer() returned nil") + } + + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + + if s.listener == nil { + t.Error("Server listener should not be nil after Start()") + } + + s.Lock() + if s.isShuttingDown { + t.Error("Server should not be shutting down after Start()") + } + s.Unlock() + + s.Shutdown() + + s.Lock() + if !s.isShuttingDown { + t.Error("Server should be shutting down after Shutdown()") + } + s.Unlock() +} + +func TestServerStartWithInvalidPort(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: -1, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + + if err == nil { + s.Shutdown() + t.Error("Start() should fail with invalid port") + } +} + +func TestServerMutex(t *testing.T) { + s := &Server{} + + s.Lock() + s.Unlock() + + done := make(chan bool) + go func() { + s.Lock() + time.Sleep(10 * time.Millisecond) + s.Unlock() + done <- true + }() + + time.Sleep(5 * time.Millisecond) + + s.Lock() + s.Unlock() + + <-done +} + +func TestServerShutdownIdempotent(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + + s.Shutdown() + + s.Lock() + if !s.isShuttingDown { + t.Error("Server should be shutting down") + } + s.Unlock() +} + +func TestServerAcceptClientsExitsOnShutdown(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + + time.Sleep(10 * time.Millisecond) + + s.Shutdown() + + time.Sleep(10 * time.Millisecond) + + s.Lock() + if !s.isShuttingDown { + t.Error("Server should be marked as shutting down") + } + s.Unlock() +} + +func TestServerHandleConnection(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() error: %v", err) + } + defer conn.Close() + + nullInit := make([]byte, 8) + _, err = conn.Write(nullInit) + if err != nil { + t.Fatalf("Write() error: %v", err) + } + + time.Sleep(50 * time.Millisecond) +} + +func TestServerHandleConnectionWithShortInit(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() error: %v", err) + } + + _, _ = conn.Write([]byte{0, 0, 0, 0}) + conn.Close() + + time.Sleep(50 * time.Millisecond) +} + +func TestServerHandleConnectionImmediateClose(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() error: %v", err) + } + conn.Close() + + time.Sleep(50 * time.Millisecond) +} + +func TestServerMultipleConnections(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr().String() + + conns := make([]net.Conn, 3) + for i := range conns { + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("Dial() %d error: %v", i, err) + } + conns[i] = conn + + nullInit := make([]byte, 8) + _, _ = conn.Write(nullInit) + } + + time.Sleep(50 * time.Millisecond) + + for _, conn := range conns { + conn.Close() + } +} + +func TestServerListenerAddress(t *testing.T) { + logger := zap.NewNop() + erupeConfig := &_config.Config{ + Sign: _config.Sign{ + Port: 0, + }, + } + + cfg := &Config{ + Logger: logger, + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + err := s.Start() + if err != nil { + t.Fatalf("Start() error: %v", err) + } + defer s.Shutdown() + + addr := s.listener.Addr() + if addr == nil { + t.Error("Listener address should not be nil") + } + + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + t.Error("Listener address should be a TCP address") + } + + if tcpAddr.Port == 0 { + t.Error("Listener port should be assigned") + } +}