test: increase total coverage from 40.7% to 46.1%

Add comprehensive mhfpacket Parse/Build/Opcode tests covering nearly
all packet types (78.3% -> 95.7%). Add channelserver tests for
BackportQuest and GuildIcon Scan/Value round-trips.
This commit is contained in:
Houmgaor
2026-02-08 16:17:17 +01:00
parent 81b2b85a8b
commit 6d18de01eb
7 changed files with 4497 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,810 @@
package mhfpacket
import (
"bytes"
"io"
"testing"
"erupe-ce/common/byteframe"
"erupe-ce/network/clientctx"
)
// --- 5-stmt packets (medium complexity) ---
// TestParseMediumVoteFesta verifies Parse for MsgMhfVoteFesta.
// Fields: AckHandle(u32), Unk(u32), GuildID(u32), TrialID(u32)
func TestParseMediumVoteFesta(t *testing.T) {
tests := []struct {
name string
ack uint32
unk uint32
guildID uint32
trialID uint32
}{
{"typical", 0x11223344, 1, 500, 42},
{"zero", 0, 0, 0, 0},
{"max", 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint32(tt.unk)
bf.WriteUint32(tt.guildID)
bf.WriteUint32(tt.trialID)
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfVoteFesta{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.Unk != tt.unk {
t.Errorf("Unk = %d, want %d", pkt.Unk, tt.unk)
}
if pkt.GuildID != tt.guildID {
t.Errorf("GuildID = %d, want %d", pkt.GuildID, tt.guildID)
}
if pkt.TrialID != tt.trialID {
t.Errorf("TrialID = %d, want %d", pkt.TrialID, tt.trialID)
}
})
}
}
// TestParseMediumAcquireSemaphore verifies Parse for MsgSysAcquireSemaphore.
// Fields: AckHandle(u32), SemaphoreIDLength(u8), SemaphoreID(string via bfutil.UpToNull)
func TestParseMediumAcquireSemaphore(t *testing.T) {
tests := []struct {
name string
ack uint32
semaphoreID string
}{
{"typical", 0xAABBCCDD, "quest_semaphore"},
{"short", 1, "s"},
{"empty", 0, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
// SemaphoreIDLength includes the null terminator in the read
idBytes := []byte(tt.semaphoreID)
idBytes = append(idBytes, 0x00) // null terminator
bf.WriteUint8(uint8(len(idBytes)))
bf.WriteBytes(idBytes)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysAcquireSemaphore{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.SemaphoreID != tt.semaphoreID {
t.Errorf("SemaphoreID = %q, want %q", pkt.SemaphoreID, tt.semaphoreID)
}
})
}
}
// TestParseMediumCheckSemaphore verifies Parse for MsgSysCheckSemaphore.
// Fields: AckHandle(u32), semaphoreIDLength(u8), SemaphoreID(string via bfutil.UpToNull)
func TestParseMediumCheckSemaphore(t *testing.T) {
tests := []struct {
name string
ack uint32
semaphoreID string
}{
{"typical", 0x12345678, "global_semaphore"},
{"short id", 42, "x"},
{"empty id", 0, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
idBytes := []byte(tt.semaphoreID)
idBytes = append(idBytes, 0x00)
bf.WriteUint8(uint8(len(idBytes)))
bf.WriteBytes(idBytes)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysCheckSemaphore{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.SemaphoreID != tt.semaphoreID {
t.Errorf("SemaphoreID = %q, want %q", pkt.SemaphoreID, tt.semaphoreID)
}
})
}
}
// TestParseMediumGetUserBinary verifies Parse for MsgSysGetUserBinary.
// Fields: AckHandle(u32), CharID(u32), BinaryType(u8)
func TestParseMediumGetUserBinary(t *testing.T) {
tests := []struct {
name string
ack uint32
charID uint32
binaryType uint8
}{
{"typical", 0xDEADBEEF, 12345, 1},
{"zero", 0, 0, 0},
{"max", 0xFFFFFFFF, 0xFFFFFFFF, 255},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint32(tt.charID)
bf.WriteUint8(tt.binaryType)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysGetUserBinary{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.CharID != tt.charID {
t.Errorf("CharID = %d, want %d", pkt.CharID, tt.charID)
}
if pkt.BinaryType != tt.binaryType {
t.Errorf("BinaryType = %d, want %d", pkt.BinaryType, tt.binaryType)
}
})
}
}
// TestParseMediumSetObjectBinary verifies Parse for MsgSysSetObjectBinary.
// Fields: ObjID(u32), DataSize(u16), RawDataPayload([]byte of DataSize)
func TestParseMediumSetObjectBinary(t *testing.T) {
tests := []struct {
name string
objID uint32
payload []byte
}{
{"typical", 42, []byte{0x01, 0x02, 0x03, 0x04}},
{"empty", 0, []byte{}},
{"large", 0xCAFEBABE, []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.objID)
bf.WriteUint16(uint16(len(tt.payload)))
bf.WriteBytes(tt.payload)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysSetObjectBinary{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.ObjID != tt.objID {
t.Errorf("ObjID = %d, want %d", pkt.ObjID, tt.objID)
}
if pkt.DataSize != uint16(len(tt.payload)) {
t.Errorf("DataSize = %d, want %d", pkt.DataSize, len(tt.payload))
}
if !bytes.Equal(pkt.RawDataPayload, tt.payload) {
t.Errorf("RawDataPayload = %v, want %v", pkt.RawDataPayload, tt.payload)
}
})
}
}
// TestParseMediumSetUserBinary verifies Parse for MsgSysSetUserBinary.
// Fields: BinaryType(u8), DataSize(u16), RawDataPayload([]byte of DataSize)
func TestParseMediumSetUserBinary(t *testing.T) {
tests := []struct {
name string
binaryType uint8
payload []byte
}{
{"typical", 1, []byte{0xDE, 0xAD, 0xBE, 0xEF}},
{"empty", 0, []byte{}},
{"max type", 255, []byte{0x01}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint8(tt.binaryType)
bf.WriteUint16(uint16(len(tt.payload)))
bf.WriteBytes(tt.payload)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysSetUserBinary{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.BinaryType != tt.binaryType {
t.Errorf("BinaryType = %d, want %d", pkt.BinaryType, tt.binaryType)
}
if pkt.DataSize != uint16(len(tt.payload)) {
t.Errorf("DataSize = %d, want %d", pkt.DataSize, len(tt.payload))
}
if !bytes.Equal(pkt.RawDataPayload, tt.payload) {
t.Errorf("RawDataPayload = %v, want %v", pkt.RawDataPayload, tt.payload)
}
})
}
}
// --- 4-stmt packets ---
// TestParseMediumGetUdRanking verifies Parse for MsgMhfGetUdRanking.
// Fields: AckHandle(u32), Unk0(u8)
func TestParseMediumGetUdRanking(t *testing.T) {
tests := []struct {
name string
ack uint32
unk0 uint8
}{
{"typical", 0x11223344, 5},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 255},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint8(tt.unk0)
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfGetUdRanking{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.Unk0 != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0)
}
})
}
}
// TestParseMediumGetUdTacticsRanking verifies Parse for MsgMhfGetUdTacticsRanking.
// Fields: AckHandle(u32), GuildID(u32)
func TestParseMediumGetUdTacticsRanking(t *testing.T) {
tests := []struct {
name string
ack uint32
guildID uint32
}{
{"typical", 0xAABBCCDD, 500},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 0xFFFFFFFF},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint32(tt.guildID)
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfGetUdTacticsRanking{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.GuildID != tt.guildID {
t.Errorf("GuildID = %d, want %d", pkt.GuildID, tt.guildID)
}
})
}
}
// TestParseMediumRegistGuildTresure verifies Parse for MsgMhfRegistGuildTresure.
// Fields: AckHandle(u32), DataLen(u16), Data([]byte), trailing u32 (discarded)
func TestParseMediumRegistGuildTresure(t *testing.T) {
tests := []struct {
name string
ack uint32
data []byte
}{
{"typical", 0x12345678, []byte{0x01, 0x02, 0x03}},
{"empty data", 1, []byte{}},
{"larger data", 0xDEADBEEF, []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint16(uint16(len(tt.data)))
bf.WriteBytes(tt.data)
bf.WriteUint32(0) // trailing uint32 that is read and discarded
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfRegistGuildTresure{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if !bytes.Equal(pkt.Data, tt.data) {
t.Errorf("Data = %v, want %v", pkt.Data, tt.data)
}
})
}
}
// TestParseMediumUpdateMyhouseInfo verifies Parse for MsgMhfUpdateMyhouseInfo.
// Fields: AckHandle(u32), Unk0([]byte of 0x16A bytes)
func TestParseMediumUpdateMyhouseInfo(t *testing.T) {
t.Run("typical", func(t *testing.T) {
bf := byteframe.NewByteFrame()
ack := uint32(0xCAFEBABE)
bf.WriteUint32(ack)
// 0x16A = 362 bytes
payload := make([]byte, 0x16A)
for i := range payload {
payload[i] = byte(i % 256)
}
bf.WriteBytes(payload)
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfUpdateMyhouseInfo{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
if len(pkt.Unk0) != 0x16A {
t.Errorf("Unk0 length = %d, want %d", len(pkt.Unk0), 0x16A)
}
if !bytes.Equal(pkt.Unk0, payload) {
t.Error("Unk0 content mismatch")
}
})
t.Run("zero values", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0)
bf.WriteBytes(make([]byte, 0x16A))
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfUpdateMyhouseInfo{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 0 {
t.Errorf("AckHandle = 0x%X, want 0", pkt.AckHandle)
}
if len(pkt.Unk0) != 0x16A {
t.Errorf("Unk0 length = %d, want %d", len(pkt.Unk0), 0x16A)
}
})
}
// TestParseMediumRightsReload verifies Parse for MsgSysRightsReload.
// Fields: AckHandle(u32), Unk0(byte/u8)
func TestParseMediumRightsReload(t *testing.T) {
tests := []struct {
name string
ack uint32
unk0 byte
}{
{"typical", 0x55667788, 1},
{"zero", 0, 0},
{"max", 0xFFFFFFFF, 255},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(tt.ack)
bf.WriteUint8(tt.unk0)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysRightsReload{}
if err := pkt.Parse(bf, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != tt.ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, tt.ack)
}
if pkt.Unk0 != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0)
}
})
}
}
// --- 3-stmt packets (AckHandle-only Parse) ---
// TestParseMediumAckHandleOnlyBatch tests Parse for all 3-stmt packets that only
// read a single AckHandle uint32. These are verified to parse correctly and
// return the expected AckHandle value.
func TestParseMediumAckHandleOnlyBatch(t *testing.T) {
packets := []struct {
name string
pkt MHFPacket
// getAck extracts the AckHandle from the parsed packet
getAck func() uint32
}{
{
"MsgMhfGetUdBonusQuestInfo",
&MsgMhfGetUdBonusQuestInfo{},
nil,
},
{
"MsgMhfGetUdDailyPresentList",
&MsgMhfGetUdDailyPresentList{},
nil,
},
{
"MsgMhfGetUdGuildMapInfo",
&MsgMhfGetUdGuildMapInfo{},
nil,
},
{
"MsgMhfGetUdMonsterPoint",
&MsgMhfGetUdMonsterPoint{},
nil,
},
{
"MsgMhfGetUdMyRanking",
&MsgMhfGetUdMyRanking{},
nil,
},
{
"MsgMhfGetUdNormaPresentList",
&MsgMhfGetUdNormaPresentList{},
nil,
},
{
"MsgMhfGetUdRankingRewardList",
&MsgMhfGetUdRankingRewardList{},
nil,
},
{
"MsgMhfGetUdSelectedColorInfo",
&MsgMhfGetUdSelectedColorInfo{},
nil,
},
{
"MsgMhfGetUdShopCoin",
&MsgMhfGetUdShopCoin{},
nil,
},
{
"MsgMhfGetUdTacticsBonusQuest",
&MsgMhfGetUdTacticsBonusQuest{},
nil,
},
{
"MsgMhfGetUdTacticsFirstQuestBonus",
&MsgMhfGetUdTacticsFirstQuestBonus{},
nil,
},
{
"MsgMhfGetUdTacticsFollower",
&MsgMhfGetUdTacticsFollower{},
nil,
},
{
"MsgMhfGetUdTacticsLog",
&MsgMhfGetUdTacticsLog{},
nil,
},
{
"MsgMhfGetUdTacticsPoint",
&MsgMhfGetUdTacticsPoint{},
nil,
},
{
"MsgMhfGetUdTacticsRewardList",
&MsgMhfGetUdTacticsRewardList{},
nil,
},
{
"MsgMhfReceiveCafeDurationBonus",
&MsgMhfReceiveCafeDurationBonus{},
nil,
},
{
"MsgSysDeleteSemaphore",
&MsgSysDeleteSemaphore{},
nil,
},
{
"MsgSysReleaseSemaphore",
&MsgSysReleaseSemaphore{},
nil,
},
}
ctx := &clientctx.ClientContext{}
ackValues := []uint32{0x12345678, 0, 0xFFFFFFFF, 0xDEADBEEF}
for _, tc := range packets {
for _, ackVal := range ackValues {
t.Run(tc.name+"/ack_"+ackHex(ackVal), func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(ackVal)
bf.Seek(0, io.SeekStart)
err := tc.pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
})
}
}
}
// TestParseMediumAckHandleOnlyVerifyValues tests each 3-stmt AckHandle-only
// packet individually, verifying that the AckHandle field is correctly populated.
func TestParseMediumAckHandleOnlyVerifyValues(t *testing.T) {
ctx := &clientctx.ClientContext{}
ack := uint32(0xCAFEBABE)
makeFrame := func() *byteframe.ByteFrame {
bf := byteframe.NewByteFrame()
bf.WriteUint32(ack)
bf.Seek(0, io.SeekStart)
return bf
}
t.Run("MsgMhfGetUdBonusQuestInfo", func(t *testing.T) {
pkt := &MsgMhfGetUdBonusQuestInfo{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdDailyPresentList", func(t *testing.T) {
pkt := &MsgMhfGetUdDailyPresentList{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdGuildMapInfo", func(t *testing.T) {
pkt := &MsgMhfGetUdGuildMapInfo{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdMonsterPoint", func(t *testing.T) {
pkt := &MsgMhfGetUdMonsterPoint{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdMyRanking", func(t *testing.T) {
pkt := &MsgMhfGetUdMyRanking{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdNormaPresentList", func(t *testing.T) {
pkt := &MsgMhfGetUdNormaPresentList{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdRankingRewardList", func(t *testing.T) {
pkt := &MsgMhfGetUdRankingRewardList{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdSelectedColorInfo", func(t *testing.T) {
pkt := &MsgMhfGetUdSelectedColorInfo{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdShopCoin", func(t *testing.T) {
pkt := &MsgMhfGetUdShopCoin{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsBonusQuest", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsBonusQuest{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsFirstQuestBonus", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsFirstQuestBonus{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsFollower", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsFollower{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsLog", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsLog{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsPoint", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsPoint{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfGetUdTacticsRewardList", func(t *testing.T) {
pkt := &MsgMhfGetUdTacticsRewardList{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgMhfReceiveCafeDurationBonus", func(t *testing.T) {
pkt := &MsgMhfReceiveCafeDurationBonus{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgSysDeleteSemaphore", func(t *testing.T) {
pkt := &MsgSysDeleteSemaphore{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
t.Run("MsgSysReleaseSemaphore", func(t *testing.T) {
pkt := &MsgSysReleaseSemaphore{}
if err := pkt.Parse(makeFrame(), ctx); err != nil {
t.Fatal(err)
}
if pkt.AckHandle != ack {
t.Errorf("AckHandle = 0x%X, want 0x%X", pkt.AckHandle, ack)
}
})
}
// TestParseMediumDeleteUser verifies that MsgSysDeleteUser.Parse returns
// NOT IMPLEMENTED error (Parse is not implemented, only Build is).
func TestParseMediumDeleteUser(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(12345)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysDeleteUser{}
err := pkt.Parse(bf, nil)
if err == nil {
t.Fatal("Parse() should return error for NOT IMPLEMENTED")
}
if err.Error() != "NOT IMPLEMENTED" {
t.Errorf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED")
}
}
// TestParseMediumInsertUser verifies that MsgSysInsertUser.Parse returns
// NOT IMPLEMENTED error (Parse is not implemented, only Build is).
func TestParseMediumInsertUser(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(12345)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysInsertUser{}
err := pkt.Parse(bf, nil)
if err == nil {
t.Fatal("Parse() should return error for NOT IMPLEMENTED")
}
if err.Error() != "NOT IMPLEMENTED" {
t.Errorf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED")
}
}
// ackHex returns a hex string for a uint32 ack value, used for test naming.
func ackHex(v uint32) string {
const hex = "0123456789ABCDEF"
buf := make([]byte, 8)
for i := 7; i >= 0; i-- {
buf[i] = hex[v&0xF]
v >>= 4
}
return string(buf)
}

View File

@@ -0,0 +1,238 @@
package mhfpacket
import (
"io"
"testing"
"erupe-ce/common/byteframe"
"erupe-ce/network/clientctx"
)
// TestParseSmallNotImplemented tests Parse for packets whose Parse method returns
// "NOT IMPLEMENTED". We verify that Parse returns a non-nil error and does not panic.
func TestParseSmallNotImplemented(t *testing.T) {
packets := []struct {
name string
pkt MHFPacket
}{
// MHF packets - NOT IMPLEMENTED
{"MsgMhfAcceptReadReward", &MsgMhfAcceptReadReward{}},
{"MsgMhfAddRewardSongCount", &MsgMhfAddRewardSongCount{}},
{"MsgMhfCaravanMyRank", &MsgMhfCaravanMyRank{}},
{"MsgMhfCaravanMyScore", &MsgMhfCaravanMyScore{}},
{"MsgMhfCaravanRanking", &MsgMhfCaravanRanking{}},
{"MsgMhfDebugPostValue", &MsgMhfDebugPostValue{}},
{"MsgMhfEnterTournamentQuest", &MsgMhfEnterTournamentQuest{}},
{"MsgMhfGetBreakSeibatuLevelReward", &MsgMhfGetBreakSeibatuLevelReward{}},
{"MsgMhfGetCaAchievementHist", &MsgMhfGetCaAchievementHist{}},
{"MsgMhfGetCaUniqueID", &MsgMhfGetCaUniqueID{}},
{"MsgMhfGetDailyMissionMaster", &MsgMhfGetDailyMissionMaster{}},
{"MsgMhfGetDailyMissionPersonal", &MsgMhfGetDailyMissionPersonal{}},
{"MsgMhfGetExtraInfo", &MsgMhfGetExtraInfo{}},
{"MsgMhfGetFixedSeibatuRankingTable", &MsgMhfGetFixedSeibatuRankingTable{}},
{"MsgMhfGetRandFromTable", &MsgMhfGetRandFromTable{}},
{"MsgMhfGetRestrictionEvent", &MsgMhfGetRestrictionEvent{}},
{"MsgMhfGetSenyuDailyCount", &MsgMhfGetSenyuDailyCount{}},
{"MsgMhfKickExportForce", &MsgMhfKickExportForce{}},
{"MsgMhfPaymentAchievement", &MsgMhfPaymentAchievement{}},
{"MsgMhfPlayFreeGacha", &MsgMhfPlayFreeGacha{}},
{"MsgMhfPostBoostTimeLimit", &MsgMhfPostBoostTimeLimit{}},
{"MsgMhfPostGemInfo", &MsgMhfPostGemInfo{}},
{"MsgMhfPostRyoudama", &MsgMhfPostRyoudama{}},
{"MsgMhfPostSeibattle", &MsgMhfPostSeibattle{}},
{"MsgMhfReadBeatLevelAllRanking", &MsgMhfReadBeatLevelAllRanking{}},
{"MsgMhfReadBeatLevelMyRanking", &MsgMhfReadBeatLevelMyRanking{}},
{"MsgMhfReadLastWeekBeatRanking", &MsgMhfReadLastWeekBeatRanking{}},
{"MsgMhfRegistSpabiTime", &MsgMhfRegistSpabiTime{}},
{"MsgMhfResetAchievement", &MsgMhfResetAchievement{}},
{"MsgMhfResetTitle", &MsgMhfResetTitle{}},
{"MsgMhfSetCaAchievement", &MsgMhfSetCaAchievement{}},
{"MsgMhfSetDailyMissionPersonal", &MsgMhfSetDailyMissionPersonal{}},
{"MsgMhfSetUdTacticsFollower", &MsgMhfSetUdTacticsFollower{}},
{"MsgMhfStampcardPrize", &MsgMhfStampcardPrize{}},
{"MsgMhfUnreserveSrg", &MsgMhfUnreserveSrg{}},
{"MsgMhfUpdateForceGuildRank", &MsgMhfUpdateForceGuildRank{}},
{"MsgMhfUseUdShopCoin", &MsgMhfUseUdShopCoin{}},
// SYS packets - NOT IMPLEMENTED
{"MsgSysAuthData", &MsgSysAuthData{}},
{"MsgSysAuthQuery", &MsgSysAuthQuery{}},
{"MsgSysAuthTerminal", &MsgSysAuthTerminal{}},
{"MsgSysCloseMutex", &MsgSysCloseMutex{}},
{"MsgSysCollectBinary", &MsgSysCollectBinary{}},
{"MsgSysCreateMutex", &MsgSysCreateMutex{}},
{"MsgSysCreateOpenMutex", &MsgSysCreateOpenMutex{}},
{"MsgSysDeleteMutex", &MsgSysDeleteMutex{}},
{"MsgSysEnumlobby", &MsgSysEnumlobby{}},
{"MsgSysEnumuser", &MsgSysEnumuser{}},
{"MsgSysGetObjectBinary", &MsgSysGetObjectBinary{}},
{"MsgSysGetObjectOwner", &MsgSysGetObjectOwner{}},
{"MsgSysGetState", &MsgSysGetState{}},
{"MsgSysInfokyserver", &MsgSysInfokyserver{}},
{"MsgSysOpenMutex", &MsgSysOpenMutex{}},
{"MsgSysRotateObject", &MsgSysRotateObject{}},
{"MsgSysSerialize", &MsgSysSerialize{}},
{"MsgSysTransBinary", &MsgSysTransBinary{}},
}
ctx := &clientctx.ClientContext{}
for _, tc := range packets {
t.Run(tc.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
// Write some padding bytes so Parse has data available if it tries to read.
bf.WriteUint32(0)
bf.Seek(0, io.SeekStart)
err := tc.pkt.Parse(bf, ctx)
if err == nil {
t.Fatalf("Parse() expected error for NOT IMPLEMENTED packet, got nil")
}
if err.Error() != "NOT IMPLEMENTED" {
t.Fatalf("Parse() error = %q, want %q", err.Error(), "NOT IMPLEMENTED")
}
})
}
}
// TestParseSmallNoData tests Parse for packets with no fields that return nil.
func TestParseSmallNoData(t *testing.T) {
packets := []struct {
name string
pkt MHFPacket
}{
{"MsgSysCleanupObject", &MsgSysCleanupObject{}},
{"MsgSysUnreserveStage", &MsgSysUnreserveStage{}},
}
ctx := &clientctx.ClientContext{}
for _, tc := range packets {
t.Run(tc.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
err := tc.pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v, want nil", err)
}
})
}
}
// TestParseSmallLogout tests Parse for MsgSysLogout which reads a single uint8 field.
func TestParseSmallLogout(t *testing.T) {
tests := []struct {
name string
unk0 uint8
}{
{"hardcoded 1", 1},
{"zero", 0},
{"max", 255},
}
ctx := &clientctx.ClientContext{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint8(tt.unk0)
bf.Seek(0, io.SeekStart)
pkt := &MsgSysLogout{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.Unk0 != tt.unk0 {
t.Errorf("Unk0 = %d, want %d", pkt.Unk0, tt.unk0)
}
})
}
}
// TestParseSmallEnumerateHouse tests Parse for MsgMhfEnumerateHouse which reads
// AckHandle, CharID, Method, Unk, lenName, and optional Name.
func TestParseSmallEnumerateHouse(t *testing.T) {
ctx := &clientctx.ClientContext{}
t.Run("no name", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0x11223344) // AckHandle
bf.WriteUint32(0xDEADBEEF) // CharID
bf.WriteUint8(2) // Method
bf.WriteUint16(100) // Unk
bf.WriteUint8(0) // lenName = 0 (no name)
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfEnumerateHouse{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 0x11223344 {
t.Errorf("AckHandle = 0x%X, want 0x11223344", pkt.AckHandle)
}
if pkt.CharID != 0xDEADBEEF {
t.Errorf("CharID = 0x%X, want 0xDEADBEEF", pkt.CharID)
}
if pkt.Method != 2 {
t.Errorf("Method = %d, want 2", pkt.Method)
}
if pkt.Unk != 100 {
t.Errorf("Unk = %d, want 100", pkt.Unk)
}
if pkt.Name != "" {
t.Errorf("Name = %q, want empty", pkt.Name)
}
})
t.Run("with name", func(t *testing.T) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(1) // AckHandle
bf.WriteUint32(42) // CharID
bf.WriteUint8(1) // Method
bf.WriteUint16(200) // Unk
// The name is SJIS null-terminated bytes. Use ASCII-compatible bytes.
nameBytes := []byte("Test\x00")
bf.WriteUint8(uint8(len(nameBytes))) // lenName > 0
bf.WriteBytes(nameBytes) // null-terminated name
bf.Seek(0, io.SeekStart)
pkt := &MsgMhfEnumerateHouse{}
err := pkt.Parse(bf, ctx)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if pkt.AckHandle != 1 {
t.Errorf("AckHandle = %d, want 1", pkt.AckHandle)
}
if pkt.CharID != 42 {
t.Errorf("CharID = %d, want 42", pkt.CharID)
}
if pkt.Method != 1 {
t.Errorf("Method = %d, want 1", pkt.Method)
}
if pkt.Unk != 200 {
t.Errorf("Unk = %d, want 200", pkt.Unk)
}
if pkt.Name != "Test" {
t.Errorf("Name = %q, want %q", pkt.Name, "Test")
}
})
}
// TestParseSmallNotImplementedDoesNotPanic ensures that calling Parse on NOT IMPLEMENTED
// packets with a nil ClientContext does not cause a nil pointer dereference panic.
func TestParseSmallNotImplementedDoesNotPanic(t *testing.T) {
packets := []MHFPacket{
&MsgMhfAcceptReadReward{},
&MsgSysAuthData{},
&MsgSysSerialize{},
}
for _, pkt := range packets {
t.Run("nil_ctx", func(t *testing.T) {
bf := byteframe.NewByteFrame()
err := pkt.Parse(bf, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
}

View File

@@ -0,0 +1,249 @@
package channelserver
import (
"encoding/json"
"testing"
)
func TestGuildIconScan_Bytes(t *testing.T) {
jsonData := []byte(`{"Parts":[{"Index":1,"ID":100,"Page":2,"Size":3,"Rotation":4,"Red":255,"Green":128,"Blue":0,"PosX":50,"PosY":60}]}`)
gi := &GuildIcon{}
err := gi.Scan(jsonData)
if err != nil {
t.Fatalf("Scan([]byte) error = %v", err)
}
if len(gi.Parts) != 1 {
t.Fatalf("Parts length = %d, want 1", len(gi.Parts))
}
part := gi.Parts[0]
if part.Index != 1 {
t.Errorf("Index = %d, want 1", part.Index)
}
if part.ID != 100 {
t.Errorf("ID = %d, want 100", part.ID)
}
if part.Page != 2 {
t.Errorf("Page = %d, want 2", part.Page)
}
if part.Size != 3 {
t.Errorf("Size = %d, want 3", part.Size)
}
if part.Rotation != 4 {
t.Errorf("Rotation = %d, want 4", part.Rotation)
}
if part.Red != 255 {
t.Errorf("Red = %d, want 255", part.Red)
}
if part.Green != 128 {
t.Errorf("Green = %d, want 128", part.Green)
}
if part.Blue != 0 {
t.Errorf("Blue = %d, want 0", part.Blue)
}
if part.PosX != 50 {
t.Errorf("PosX = %d, want 50", part.PosX)
}
if part.PosY != 60 {
t.Errorf("PosY = %d, want 60", part.PosY)
}
}
func TestGuildIconScan_String(t *testing.T) {
jsonStr := `{"Parts":[{"Index":5,"ID":200,"Page":1,"Size":2,"Rotation":0,"Red":100,"Green":50,"Blue":25,"PosX":300,"PosY":400}]}`
gi := &GuildIcon{}
err := gi.Scan(jsonStr)
if err != nil {
t.Fatalf("Scan(string) error = %v", err)
}
if len(gi.Parts) != 1 {
t.Fatalf("Parts length = %d, want 1", len(gi.Parts))
}
if gi.Parts[0].ID != 200 {
t.Errorf("ID = %d, want 200", gi.Parts[0].ID)
}
if gi.Parts[0].PosX != 300 {
t.Errorf("PosX = %d, want 300", gi.Parts[0].PosX)
}
}
func TestGuildIconScan_MultipleParts(t *testing.T) {
jsonData := []byte(`{"Parts":[{"Index":0,"ID":1,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":1,"ID":2,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0},{"Index":2,"ID":3,"Page":0,"Size":0,"Rotation":0,"Red":0,"Green":0,"Blue":0,"PosX":0,"PosY":0}]}`)
gi := &GuildIcon{}
err := gi.Scan(jsonData)
if err != nil {
t.Fatalf("Scan() error = %v", err)
}
if len(gi.Parts) != 3 {
t.Fatalf("Parts length = %d, want 3", len(gi.Parts))
}
for i, part := range gi.Parts {
if part.Index != uint16(i) {
t.Errorf("Parts[%d].Index = %d, want %d", i, part.Index, i)
}
}
}
func TestGuildIconScan_EmptyParts(t *testing.T) {
gi := &GuildIcon{}
err := gi.Scan([]byte(`{"Parts":[]}`))
if err != nil {
t.Fatalf("Scan() error = %v", err)
}
if len(gi.Parts) != 0 {
t.Errorf("Parts length = %d, want 0", len(gi.Parts))
}
}
func TestGuildIconScan_InvalidJSON(t *testing.T) {
gi := &GuildIcon{}
err := gi.Scan([]byte(`{invalid`))
if err == nil {
t.Error("Scan() with invalid JSON should return error")
}
}
func TestGuildIconScan_InvalidJSONString(t *testing.T) {
gi := &GuildIcon{}
err := gi.Scan("{invalid")
if err == nil {
t.Error("Scan() with invalid JSON string should return error")
}
}
func TestGuildIconScan_UnsupportedType(t *testing.T) {
gi := &GuildIcon{}
// Passing an unsupported type should not error (just no-op)
err := gi.Scan(12345)
if err != nil {
t.Errorf("Scan(int) unexpected error = %v", err)
}
}
func TestGuildIconValue(t *testing.T) {
gi := &GuildIcon{
Parts: []GuildIconPart{
{Index: 1, ID: 100, Page: 2, Size: 3, Rotation: 4, Red: 255, Green: 128, Blue: 0, PosX: 50, PosY: 60},
},
}
val, err := gi.Value()
if err != nil {
t.Fatalf("Value() error = %v", err)
}
jsonBytes, ok := val.([]byte)
if !ok {
t.Fatalf("Value() returned %T, want []byte", val)
}
// Verify round-trip
gi2 := &GuildIcon{}
err = json.Unmarshal(jsonBytes, gi2)
if err != nil {
t.Fatalf("json.Unmarshal error = %v", err)
}
if len(gi2.Parts) != 1 {
t.Fatalf("round-trip Parts length = %d, want 1", len(gi2.Parts))
}
if gi2.Parts[0].ID != 100 {
t.Errorf("round-trip ID = %d, want 100", gi2.Parts[0].ID)
}
if gi2.Parts[0].Red != 255 {
t.Errorf("round-trip Red = %d, want 255", gi2.Parts[0].Red)
}
}
func TestGuildIconValue_Empty(t *testing.T) {
gi := &GuildIcon{}
val, err := gi.Value()
if err != nil {
t.Fatalf("Value() error = %v", err)
}
if val == nil {
t.Error("Value() should not return nil")
}
}
func TestGuildIconScanValueRoundTrip(t *testing.T) {
original := &GuildIcon{
Parts: []GuildIconPart{
{Index: 0, ID: 10, Page: 1, Size: 2, Rotation: 45, Red: 200, Green: 150, Blue: 100, PosX: 500, PosY: 600},
{Index: 1, ID: 20, Page: 3, Size: 4, Rotation: 90, Red: 50, Green: 75, Blue: 255, PosX: 100, PosY: 200},
},
}
// Value -> Scan round trip
val, err := original.Value()
if err != nil {
t.Fatalf("Value() error = %v", err)
}
restored := &GuildIcon{}
err = restored.Scan(val)
if err != nil {
t.Fatalf("Scan() error = %v", err)
}
if len(restored.Parts) != len(original.Parts) {
t.Fatalf("Parts length = %d, want %d", len(restored.Parts), len(original.Parts))
}
for i := range original.Parts {
if restored.Parts[i] != original.Parts[i] {
t.Errorf("Parts[%d] mismatch: got %+v, want %+v", i, restored.Parts[i], original.Parts[i])
}
}
}
func TestFestivalColourCodes(t *testing.T) {
tests := []struct {
colour FestivalColour
code uint8
}{
{FestivalColourBlue, 0x00},
{FestivalColourRed, 0x01},
{FestivalColourNone, 0xFF},
}
for _, tt := range tests {
t.Run(string(tt.colour), func(t *testing.T) {
code, ok := FestivalColourCodes[tt.colour]
if !ok {
t.Fatalf("FestivalColourCodes missing key %s", tt.colour)
}
if code != tt.code {
t.Errorf("FestivalColourCodes[%s] = %d, want %d", tt.colour, code, tt.code)
}
})
}
}
func TestFestivalColourConstants(t *testing.T) {
if FestivalColourNone != "none" {
t.Errorf("FestivalColourNone = %s, want none", FestivalColourNone)
}
if FestivalColourRed != "red" {
t.Errorf("FestivalColourRed = %s, want red", FestivalColourRed)
}
if FestivalColourBlue != "blue" {
t.Errorf("FestivalColourBlue = %s, want blue", FestivalColourBlue)
}
}
func TestGuildApplicationTypeConstants(t *testing.T) {
if GuildApplicationTypeApplied != "applied" {
t.Errorf("GuildApplicationTypeApplied = %s, want applied", GuildApplicationTypeApplied)
}
if GuildApplicationTypeInvited != "invited" {
t.Errorf("GuildApplicationTypeInvited = %s, want invited", GuildApplicationTypeInvited)
}
}

View File

@@ -0,0 +1,128 @@
package channelserver
import (
"encoding/binary"
"testing"
_config "erupe-ce/config"
)
func TestBackportQuest_Basic(t *testing.T) {
// Set up config for the test
oldConfig := _config.ErupeConfig
defer func() { _config.ErupeConfig = oldConfig }()
_config.ErupeConfig = &_config.Config{}
_config.ErupeConfig.RealClientMode = _config.ZZ
// Create a quest data buffer large enough for BackportQuest to work with.
// The function reads a uint32 from data[0:4] as offset, then works at offset+96.
// We need at least offset + 96 + 108 + 6*8 bytes.
// Set offset (wp base) = 0, so wp starts at 96, rp at 100.
data := make([]byte, 512)
binary.LittleEndian.PutUint32(data[0:4], 0) // offset = 0
// Fill some data at the rp positions so we can verify copies
for i := 100; i < 400; i++ {
data[i] = byte(i & 0xFF)
}
result := BackportQuest(data)
if result == nil {
t.Fatal("BackportQuest returned nil")
}
if len(result) != len(data) {
t.Errorf("BackportQuest changed data length: got %d, want %d", len(result), len(data))
}
}
func TestBackportQuest_S6Mode(t *testing.T) {
oldConfig := _config.ErupeConfig
defer func() { _config.ErupeConfig = oldConfig }()
_config.ErupeConfig = &_config.Config{}
_config.ErupeConfig.RealClientMode = _config.S6
data := make([]byte, 512)
binary.LittleEndian.PutUint32(data[0:4], 0)
for i := 0; i < len(data); i++ {
data[i+4] = byte(i % 256)
if i+4 >= len(data)-1 {
break
}
}
// Set some values at data[8:12] so we can check they get copied to data[16:20]
binary.LittleEndian.PutUint32(data[8:12], 0xDEADBEEF)
result := BackportQuest(data)
if result == nil {
t.Fatal("BackportQuest returned nil")
}
// In S6 mode, data[16:20] should be copied from data[8:12]
got := binary.LittleEndian.Uint32(result[16:20])
if got != 0xDEADBEEF {
t.Errorf("S6 mode: data[16:20] = 0x%X, want 0xDEADBEEF", got)
}
}
func TestBackportQuest_G91Mode_PatternReplacement(t *testing.T) {
oldConfig := _config.ErupeConfig
defer func() { _config.ErupeConfig = oldConfig }()
_config.ErupeConfig = &_config.Config{}
_config.ErupeConfig.RealClientMode = _config.G91
data := make([]byte, 512)
binary.LittleEndian.PutUint32(data[0:4], 0)
// Insert an armor sphere pattern at a known location
// Pattern: 0x0A, 0x00, 0x01, 0x33 -> should replace bytes at +2 with 0xD7, 0x00
offset := 300
data[offset] = 0x0A
data[offset+1] = 0x00
data[offset+2] = 0x01
data[offset+3] = 0x33
result := BackportQuest(data)
// After BackportQuest, the pattern's last 2 bytes should be replaced
if result[offset+2] != 0xD7 || result[offset+3] != 0x00 {
t.Errorf("G91 pattern replacement failed: got [0x%X, 0x%X], want [0xD7, 0x00]",
result[offset+2], result[offset+3])
}
}
func TestBackportQuest_F5Mode(t *testing.T) {
oldConfig := _config.ErupeConfig
defer func() { _config.ErupeConfig = oldConfig }()
_config.ErupeConfig = &_config.Config{}
_config.ErupeConfig.RealClientMode = _config.F5
data := make([]byte, 512)
binary.LittleEndian.PutUint32(data[0:4], 0)
result := BackportQuest(data)
if result == nil {
t.Fatal("BackportQuest returned nil")
}
}
func TestBackportQuest_G101Mode(t *testing.T) {
oldConfig := _config.ErupeConfig
defer func() { _config.ErupeConfig = oldConfig }()
_config.ErupeConfig = &_config.Config{}
_config.ErupeConfig.RealClientMode = _config.G101
data := make([]byte, 512)
binary.LittleEndian.PutUint32(data[0:4], 0)
result := BackportQuest(data)
if result == nil {
t.Fatal("BackportQuest returned nil")
}
}