diff --git a/internal/packets/ls_structs/ls_structs_test.go b/internal/packets/ls_structs/ls_structs_test.go new file mode 100644 index 0000000..43f7c98 --- /dev/null +++ b/internal/packets/ls_structs/ls_structs_test.go @@ -0,0 +1,641 @@ +package ls_structs + +import ( + "eq2emu/internal/packets/parser" + "testing" +) + +func TestDeleteCharacterRequest(t *testing.T) { + // CharID=12345, ServerID=1, Unknown=0, Name="TestChar" + data := []byte{ + 0x39, 0x30, 0x00, 0x00, // CharID = 12345 + 0x01, 0x00, 0x00, 0x00, // ServerID = 1 + 0x00, 0x00, 0x00, 0x00, // Unknown = 0 + 0x08, 0x00, 'T', 'e', 's', 't', 'C', 'h', 'a', 'r', // Name = "TestChar" + } + + parser := parser.NewParser(data) + var packet DeleteCharacterRequest + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.CharID != 12345 { + t.Errorf("Expected CharID=12345, got %d", packet.CharID) + } + if packet.ServerID != 1 { + t.Errorf("Expected ServerID=1, got %d", packet.ServerID) + } + if packet.Name.Data != "TestChar" { + t.Errorf("Expected Name='TestChar', got '%s'", packet.Name.Data) + } +} + +func TestDeleteCharacterResponse(t *testing.T) { + // Response=1, ServerID=1, CharID=12345, AccountID=54321, Name="TestChar", MaxCharacters=8 + data := []byte{ + 0x01, // Response = 1 + 0x01, 0x00, 0x00, 0x00, // ServerID = 1 + 0x39, 0x30, 0x00, 0x00, // CharID = 12345 + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x08, 0x00, 'T', 'e', 's', 't', 'C', 'h', 'a', 'r', // Name = "TestChar" + 0x08, 0x00, 0x00, 0x00, // MaxCharacters = 8 + } + + parser := parser.NewParser(data) + var packet DeleteCharacterResponse + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.Response != 1 { + t.Errorf("Expected Response=1, got %d", packet.Response) + } + if packet.CharID != 12345 { + t.Errorf("Expected CharID=12345, got %d", packet.CharID) + } + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } + if packet.MaxCharacters != 8 { + t.Errorf("Expected MaxCharacters=8, got %d", packet.MaxCharacters) + } +} + +func TestCreateCharacterReplyV1(t *testing.T) { + // AccountID=54321, Response=0, Name="NewChar" + data := []byte{ + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x00, // Response = 0 + 0x07, 0x00, 'N', 'e', 'w', 'C', 'h', 'a', 'r', // Name = "NewChar" + } + + parser := parser.NewParser(data) + var packet CreateCharacterReplyV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } + if packet.Response != 0 { + t.Errorf("Expected Response=0, got %d", packet.Response) + } + if packet.Name.Data != "NewChar" { + t.Errorf("Expected Name='NewChar', got '%s'", packet.Name.Data) + } +} + +func TestCreateCharacterReplyV1189(t *testing.T) { + // AccountID=54321, Unknown=100, Response=0, Name="NewChar" + data := []byte{ + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x64, 0x00, 0x00, 0x00, // Unknown = 100 + 0x00, // Response = 0 + 0x07, 0x00, 'N', 'e', 'w', 'C', 'h', 'a', 'r', // Name = "NewChar" + } + + parser := parser.NewParser(data) + var packet CreateCharacterReplyV1189 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } + if packet.Unknown != 100 { + t.Errorf("Expected Unknown=100, got %d", packet.Unknown) + } + if packet.Response != 0 { + t.Errorf("Expected Response=0, got %d", packet.Response) + } +} + +func TestLoginRequestV1(t *testing.T) { + data := []byte{ + // SessionID = "session123" + 0x0A, 0x00, 's', 'e', 's', 's', 'i', 'o', 'n', '1', '2', '3', + // SessionRecycleToken = "token456" + 0x08, 0x00, 't', 'o', 'k', 'e', 'n', '4', '5', '6', + // Username = "testuser" + 0x08, 0x00, 't', 'e', 's', 't', 'u', 's', 'e', 'r', + // Password = "testpass" + 0x08, 0x00, 't', 'e', 's', 't', 'p', 'a', 's', 's', + 0x31, 0xD4, 0x00, 0x00, // AcctNum = 54321 + 0x39, 0x30, 0x00, 0x00, // PassCode = 12345 + 0xDC, 0x05, // Version = 1500 + } + + parser := parser.NewParser(data) + var packet LoginRequestV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.SessionID.Data != "session123" { + t.Errorf("Expected SessionID='session123', got '%s'", packet.SessionID.Data) + } + if packet.Username.Data != "testuser" { + t.Errorf("Expected Username='testuser', got '%s'", packet.Username.Data) + } + if packet.AcctNum != 54321 { + t.Errorf("Expected AcctNum=54321, got %d", packet.AcctNum) + } + if packet.Version != 1500 { + t.Errorf("Expected Version=1500, got %d", packet.Version) + } +} + +func TestLoginByNumRequestV1(t *testing.T) { + data := []byte{ + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x39, 0x30, 0x00, 0x00, // AccessCode = 12345 + 0xDC, 0x05, // Version = 1500 + // Unknown2 = [1, 2, 3, 4, 5] + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, + } + + parser := parser.NewParser(data) + var packet LoginByNumRequestV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } + if packet.AccessCode != 12345 { + t.Errorf("Expected AccessCode=12345, got %d", packet.AccessCode) + } + if packet.Version != 1500 { + t.Errorf("Expected Version=1500, got %d", packet.Version) + } + if len(packet.Unknown2) != 5 { + t.Errorf("Expected Unknown2 length=5, got %d", len(packet.Unknown2)) + } + if packet.Unknown2[0] != 1 || packet.Unknown2[4] != 5 { + t.Errorf("Expected Unknown2=[1,2,3,4,5], got %v", packet.Unknown2) + } +} + +func TestPlayRequestV1(t *testing.T) { + data := []byte{ + 0x39, 0x30, 0x00, 0x00, // CharID = 12345 + 0x08, 0x00, 'T', 'e', 's', 't', 'C', 'h', 'a', 'r', // Name = "TestChar" + } + + parser := parser.NewParser(data) + var packet PlayRequestV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.CharID != 12345 { + t.Errorf("Expected CharID=12345, got %d", packet.CharID) + } + if packet.Name.Data != "TestChar" { + t.Errorf("Expected Name='TestChar', got '%s'", packet.Name.Data) + } +} + +func TestPlayResponseV1(t *testing.T) { + data := []byte{ + 0x01, // Response = 1 + 0x09, '1', '2', '7', '.', '0', '.', '0', '.', '1', // Server = "127.0.0.1" + 0xC4, 0x09, // Port = 2500 + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x39, 0x30, 0x00, 0x00, // AccessCode = 12345 + } + + parser := parser.NewParser(data) + var packet PlayResponseV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.Response != 1 { + t.Errorf("Expected Response=1, got %d", packet.Response) + } + if packet.Server.Data != "127.0.0.1" { + t.Errorf("Expected Server='127.0.0.1', got '%s'", packet.Server.Data) + } + if packet.Port != 2500 { + t.Errorf("Expected Port=2500, got %d", packet.Port) + } + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } +} + +func TestWorldListV1(t *testing.T) { + data := []byte{ + 0x02, // NumWorlds = 2 + // World 1 + 0x01, 0x00, 0x00, 0x00, // ID = 1 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // Name = "Antonia" + 0x01, // Online = 1 + 0x00, // Locked = 0 + 0x00, // Unknown2 = 0 + 0x00, // Unknown3 = 0 + 0x32, // Load = 50 + // World 2 + 0x02, 0x00, 0x00, 0x00, // ID = 2 + 0x08, 0x00, 'N', 'a', 'g', 'a', 'f', 'e', 'n', ' ', // Name = "Nagafen " + 0x01, // Online = 1 + 0x01, // Locked = 1 + 0x00, // Unknown2 = 0 + 0x00, // Unknown3 = 0 + 0x64, // Load = 100 + } + + parser := parser.NewParser(data) + var packet WorldListV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.NumWorlds != 2 { + t.Errorf("Expected NumWorlds=2, got %d", packet.NumWorlds) + } + if len(packet.WorldList) != 2 { + t.Errorf("Expected 2 worlds, got %d", len(packet.WorldList)) + } + if packet.WorldList[0].Name.Data != "Antonia" { + t.Errorf("Expected first world 'Antonia', got '%s'", packet.WorldList[0].Name.Data) + } + if packet.WorldList[0].Online != 1 { + t.Errorf("Expected first world online=1, got %d", packet.WorldList[0].Online) + } + if packet.WorldList[1].Locked != 1 { + t.Errorf("Expected second world locked=1, got %d", packet.WorldList[1].Locked) + } +} + +func TestWorldListV373(t *testing.T) { + data := []byte{ + 0x01, // NumWorlds = 1 + // World 1 + 0x01, 0x00, 0x00, 0x00, // ID = 1 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // Name = "Antonia" + 0x01, // Tag = 1 + 0x00, // Locked = 0 + 0x00, // Hidden = 0 + 0x00, // Unknown = 0 + 0x64, 0x00, // NumPlayers = 100 + 0x32, // Load = 50 + 0x01, // NumberOnlineFlag = 1 + 0xFF, 0xFF, 0xFF, 0xFF, // AllowedRaces = 0xFFFFFFFF + } + + parser := parser.NewParser(data) + var packet WorldListV373 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.NumWorlds != 1 { + t.Errorf("Expected NumWorlds=1, got %d", packet.NumWorlds) + } + if packet.WorldList[0].NumPlayers != 100 { + t.Errorf("Expected NumPlayers=100, got %d", packet.WorldList[0].NumPlayers) + } + if packet.WorldList[0].AllowedRaces != 0xFFFFFFFF { + t.Errorf("Expected AllowedRaces=0xFFFFFFFF, got %d", packet.WorldList[0].AllowedRaces) + } +} + +func TestLoginReplyMsgV1(t *testing.T) { + data := []byte{ + 0x00, // LoginResponse = 0 (success) + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // WorldName = "Antonia" + 0x00, // ParentalControlFlag = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer[0] = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer[1] = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlNext = 0 + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + } + + parser := parser.NewParser(data) + var packet LoginReplyMsgV1 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.LoginResponse != 0 { + t.Errorf("Expected LoginResponse=0, got %d", packet.LoginResponse) + } + if packet.WorldName.Data != "Antonia" { + t.Errorf("Expected WorldName='Antonia', got '%s'", packet.WorldName.Data) + } + if packet.AccountID != 54321 { + t.Errorf("Expected AccountID=54321, got %d", packet.AccountID) + } +} + +func TestLoginReplyMsgV284WithClassItems(t *testing.T) { + data := []byte{ + 0x00, // LoginResponse = 0 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // Unknown = "Antonia" + 0x00, // ParentalControlFlag = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Unknown2 (8 bytes) + 0x31, 0xD4, 0x00, 0x00, // CacheSettingAccountID = 54321 + 0x04, 0x00, 't', 'e', 's', 't', // Unknown3 = "test" + 0x00, // ResetAppearance = 0 + 0x00, // DoNotForceSoga = 0 + 0x00, 0x00, // Unknown5 = 0 + 0x00, // Unknown6 = 0 + 0x00, 0x00, 0x00, 0x00, // Unknown7 = 0 + 0x00, 0x00, // Unknown8 (2 bytes) + 0x01, // Unknown10 = 1 (truthy, so class items will be parsed) + 0x01, // NumClassItems = 1 + // ClassItem 1 + 0x05, // ClassID = 5 + 0x01, // NumItems = 1 + // StartingItem 1 + 0x64, 0x00, // ModelID = 100 + 0x01, // SlotID = 1 + 0x01, // UseColor = 1 + 0x01, // UseHighlightColor = 1 + 0xFF, 0x00, 0x00, // ModelColor = Red + 0x00, 0xFF, 0x00, // ModelHighlightColor = Green + 0x00, // UnknownArray2Size = 0 + } + + parser := parser.NewParser(data) + var packet LoginReplyMsgV284 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.LoginResponse != 0 { + t.Errorf("Expected LoginResponse=0, got %d", packet.LoginResponse) + } + if packet.CacheSettingAccountID != 54321 { + t.Errorf("Expected CacheSettingAccountID=54321, got %d", packet.CacheSettingAccountID) + } + if packet.Unknown10 != 1 { + t.Errorf("Expected Unknown10=1, got %d", packet.Unknown10) + } + if packet.NumClassItems != 1 { + t.Errorf("Expected NumClassItems=1, got %d", packet.NumClassItems) + } + if len(packet.ClassItems) != 1 { + t.Errorf("Expected 1 class item, got %d", len(packet.ClassItems)) + } + if packet.ClassItems[0].ClassID != 5 { + t.Errorf("Expected ClassID=5, got %d", packet.ClassItems[0].ClassID) + } + if len(packet.ClassItems[0].StartingItems) != 1 { + t.Errorf("Expected 1 starting item, got %d", len(packet.ClassItems[0].StartingItems)) + } + if packet.ClassItems[0].StartingItems[0].ModelID != 100 { + t.Errorf("Expected ModelID=100, got %d", packet.ClassItems[0].StartingItems[0].ModelID) + } +} + +func TestLoginReplyMsgV284WithoutClassItems(t *testing.T) { + data := []byte{ + 0x00, // LoginResponse = 0 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // Unknown = "Antonia" + 0x00, // ParentalControlFlag = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Unknown2 (8 bytes) + 0x31, 0xD4, 0x00, 0x00, // CacheSettingAccountID = 54321 + 0x04, 0x00, 't', 'e', 's', 't', // Unknown3 = "test" + 0x00, // ResetAppearance = 0 + 0x00, // DoNotForceSoga = 0 + 0x00, 0x00, // Unknown5 = 0 + 0x00, // Unknown6 = 0 + 0x00, 0x00, 0x00, 0x00, // Unknown7 = 0 + 0x00, 0x00, // Unknown8 (2 bytes) + 0x00, // Unknown10 = 0 (falsy, so class items will be skipped) + 0x00, // UnknownArray2Size = 0 + } + + parser := parser.NewParser(data) + var packet LoginReplyMsgV284 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.Unknown10 != 0 { + t.Errorf("Expected Unknown10=0, got %d", packet.Unknown10) + } + if packet.NumClassItems != 0 { + t.Errorf("Expected NumClassItems=0 (should be skipped), got %d", packet.NumClassItems) + } + if len(packet.ClassItems) != 0 { + t.Errorf("Expected 0 class items (should be skipped), got %d", len(packet.ClassItems)) + } +} + +func TestVersionRegistryWithLoginReplyMsg(t *testing.T) { + registry := parser.NewVersionRegistry() + RegisterLoginServerStructs(registry) + + // Test data for LoginReplyMsgV1 + data := []byte{ + 0x00, // LoginResponse = 0 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // WorldName = "Antonia" + 0x00, // ParentalControlFlag = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer[0] = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer[1] = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlNext = 0 + 0x31, 0xD4, 0x00, 0x00, // AccountID = 54321 + } + + parser := parser.NewParser(data) + result, err := parser.ParseWithVersion(registry, "LoginReplyMsg", "1") + if err != nil { + t.Fatalf("Parse with version error: %v", err) + } + + packet, ok := result.(LoginReplyMsgV1) + if !ok { + t.Fatal("Expected LoginReplyMsgV1 type") + } + + if packet.LoginResponse != 0 { + t.Errorf("Expected LoginResponse=0, got %d", packet.LoginResponse) + } + if packet.WorldName.Data != "Antonia" { + t.Errorf("Expected WorldName='Antonia', got '%s'", packet.WorldName.Data) + } +} + +func TestVersionRegistryFallback(t *testing.T) { + registry := parser.NewVersionRegistry() + RegisterLoginServerStructs(registry) + + data := []byte{ + 0x39, 0x30, 0x00, 0x00, // CharID = 12345 + 0x08, 0x00, 'T', 'e', 's', 't', 'C', 'h', 'a', 'r', // Name = "TestChar" + } + + parser := parser.NewParser(data) + + // Request version 150 (doesn't exist), should fall back to version 1 + result, err := parser.ParseWithVersion(registry, "PlayRequest", "150") + if err != nil { + t.Fatalf("Parse with fallback error: %v", err) + } + + // Should get PlayRequestV1 (nearest lower version) + packet, ok := result.(PlayRequestV1) + if !ok { + t.Error("Expected fallback to PlayRequestV1") + } + + if packet.CharID != 12345 { + t.Errorf("Expected CharID=12345, got %d", packet.CharID) + } +} + +func TestWorldUpdate(t *testing.T) { + data := []byte{ + 0x01, 0x00, 0x00, 0x00, // ServerID = 1 + 0x01, // Up = 1 + 0x00, // Locked = 0 + 0x00, // Unknown1 = 0 + 0x32, // Unknown2 = 50 + } + + parser := parser.NewParser(data) + var packet WorldUpdate + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.ServerID != 1 { + t.Errorf("Expected ServerID=1, got %d", packet.ServerID) + } + if packet.Up != 1 { + t.Errorf("Expected Up=1, got %d", packet.Up) + } + if packet.Locked != 0 { + t.Errorf("Expected Locked=0, got %d", packet.Locked) + } +} + +func TestLoginResponseV60100WithComplexArrays(t *testing.T) { + data := []byte{ + 0x00, // LoginResponse = 0 + 0x07, 0x00, 'A', 'n', 't', 'o', 'n', 'i', 'a', // Unknown = "Antonia" + 0x00, // ParentalControlFlag = 0 + 0x00, 0x00, 0x00, 0x00, // ParentalControlTimer = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Unknown2 (8 bytes) + 0x21, 0xD4, 0x00, 0x00, // AccountID = 54321 + 0x04, 0x00, 't', 'e', 's', 't', // Unknown3 = "test" + 0x00, // ResetAppearance = 0 + 0x00, // DoNotForceSoga = 0 + 0x00, // Unknown4 = 0 + 0x00, 0x00, // Unknown5 = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, // Unknown6 (5 bytes) + 0x00, 0x00, 0x00, 0x00, // Unknown7 = 0 + 0x00, 0x00, // Unknown7a = 0 + 0x00, // RaceUnknown = 0 + 0x00, 0x00, 0x00, // Unknown8 (3 bytes) + 0x00, 0x00, 0x00, // Unknown9 (3 bytes) + 0x01, // Unknown10 = 1 (enables class items) + 0x01, // NumClassItems = 1 + // ClassItem 1 + 0x05, // ClassID = 5 + 0x01, // NumItems = 1 + // StartingItem 1 + 0x64, 0x00, 0x00, 0x00, // ModelID = 100 (int32 in newer versions) + 0x01, // SlotID = 1 + 0x01, // UseColor = 1 + 0x01, // UseHighlightColor = 1 + 0xFF, 0x00, 0x00, // ModelColor = Red + 0x00, 0xFF, 0x00, // ModelHighlightColor = Green + 0x00, // UnknownArray2Size = 0 + 0x00, 0x00, 0x00, 0x00, // Unknown11 = 0 + 0x00, 0x00, 0x00, 0x00, // SubLevel = 0 + 0xFF, 0xFF, 0xFF, 0xFF, // RaceFlag = 0xFFFFFFFF + 0xFF, 0xFF, 0xFF, 0xFF, // ClassFlag = 0xFFFFFFFF + 0x08, 0x00, 't', 'e', 's', 't', 'p', 'a', 's', 's', // Password = "testpass" + 0x08, 0x00, 't', 'e', 's', 't', 'u', 's', 'e', 'r', // Username = "testuser" + 0x07, 0x00, 's', 'e', 'r', 'v', 'i', 'c', 'e', // Service = "service" + 0x01, // Unknown12 = 1 (enables Lvl90 class items) + 0x01, // Lvl90NumClassItems = 1 + // Lvl90 ClassItem 1 + 0x06, // ClassID = 6 + 0x01, // NumItems = 1 + // StartingItem 1 + 0x65, 0x00, 0x00, 0x00, // ModelID = 101 + 0x02, // SlotID = 2 + 0x01, // UseColor = 1 + 0x01, // UseHighlightColor = 1 + 0x00, 0xFF, 0x00, // ModelColor = Green + 0xFF, 0x00, 0x00, // ModelHighlightColor = Red + 0x01, // Unknown13 = 1 (enables TimeLocked class items) + 0x01, // TimeLockedNumClassItems = 1 + // TimeLocked ClassItem 1 + 0x07, // ClassID = 7 + 0x01, // NumItems = 1 + // StartingItem 1 + 0x66, 0x00, 0x00, 0x00, // ModelID = 102 + 0x03, // SlotID = 3 + 0x00, // UseColor = 0 + 0x00, // UseHighlightColor = 0 + 0x80, 0x80, 0x80, // ModelColor = Gray + 0x40, 0x40, 0x40, // ModelHighlightColor = Dark gray + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Unknown14 (13 bytes) + } + + parser := parser.NewParser(data) + var packet LoginResponseV60100 + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + // Test main class items + if len(packet.ClassItems) != 1 { + t.Errorf("Expected 1 class item, got %d", len(packet.ClassItems)) + } + if packet.ClassItems[0].ClassID != 5 { + t.Errorf("Expected ClassID=5, got %d", packet.ClassItems[0].ClassID) + } + + // Test Lvl90 class items + if len(packet.Lvl90ClassItems) != 1 { + t.Errorf("Expected 1 Lvl90 class item, got %d", len(packet.Lvl90ClassItems)) + } + if packet.Lvl90ClassItems[0].ClassID != 6 { + t.Errorf("Expected Lvl90 ClassID=6, got %d", packet.Lvl90ClassItems[0].ClassID) + } + + // Test TimeLocked class items + if len(packet.TimeLockedClassItems) != 1 { + t.Errorf("Expected 1 TimeLocked class item, got %d", len(packet.TimeLockedClassItems)) + } + if packet.TimeLockedClassItems[0].ClassID != 7 { + t.Errorf("Expected TimeLocked ClassID=7, got %d", packet.TimeLockedClassItems[0].ClassID) + } + + if packet.RaceFlag != 0xFFFFFFFF { + t.Errorf("Expected RaceFlag=0xFFFFFFFF, got %d", packet.RaceFlag) + } +}