package mhfpacket import ( "testing" "erupe-ce/common/byteframe" cfg "erupe-ce/config" "erupe-ce/network" "erupe-ce/network/clientctx" ) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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(), &clientctx.ClientContext{RealClientMode: cfg.ZZ}) if err == nil { t.Error("Build() should return error (not implemented)") } } func TestMsgMhfEnumerateGuacotBuild_NotImplemented(t *testing.T) { pkt := &MsgMhfEnumerateGuacot{} err := pkt.Build(byteframe.NewByteFrame(), &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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, &clientctx.ClientContext{RealClientMode: cfg.ZZ}) 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)) } }