package main import ( "encoding/binary" "eq2emu/internal/common/opcodes" "eq2emu/internal/packets" "eq2emu/internal/udp" "fmt" "log" "time" ) // LoginRequest represents parsed login request data type LoginRequest struct { Username string Password string Version uint16 } // opcodeToPacketName maps opcodes to packet definition names var opcodeToPacketName = map[uint16]string{ opcodes.OpLoginRequestMsg: "LS_LoginRequest", opcodes.OpAllWSDescRequestMsg: "LS_WorldListRequest", opcodes.OpAllCharactersDescRequestMsg: "LS_CharacterListRequest", opcodes.OpCreateCharacterRequestMsg: "LS_CreateCharacterRequest", opcodes.OpDeleteCharacterRequestMsg: "LS_DeleteCharacterRequest", opcodes.OpPlayCharacterRequestMsg: "LS_PlayCharacterRequest", } // parsePacketWithDefinition parses packet using definitions or fails func (lc *LoginClient) parsePacketWithDefinition(opcode uint16, data []byte) (map[string]any, error) { packetName, exists := opcodeToPacketName[opcode] if !exists { return nil, fmt.Errorf("no packet name mapping for opcode 0x%04X", opcode) } packetDef, exists := packets.GetPacket(packetName) if !exists { return nil, fmt.Errorf("no packet definition found for %s", packetName) } // Use client version for parsing, default flags result, err := packetDef.Parse(data, uint32(lc.version), 0) if err != nil { return nil, fmt.Errorf("packet parsing failed for %s: %w", packetName, err) } return result, nil } // parseLoginRequest parses the login request packet data func (lc *LoginClient) parseLoginRequest(data []byte) (*LoginRequest, error) { parsed, err := lc.parsePacketWithDefinition(opcodes.OpLoginRequestMsg, data) if err != nil { return nil, err } req := &LoginRequest{} if username, ok := parsed["username"].(string); ok { req.Username = username } if password, ok := parsed["password"].(string); ok { req.Password = password } if version, ok := parsed["version"].(uint16); ok { req.Version = version } return req, nil } // sendLoginDenied sends login failure response func (lc *LoginClient) sendLoginDenied() { data := make([]byte, 12) data[0] = 1 // reply_code: Invalid username or password binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF) binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF) packet := &udp.ApplicationPacket{ Opcode: opcodes.OpLoginReplyMsg, Data: data, } lc.connection.SendPacket(packet) // Disconnect after short delay time.AfterFunc(1*time.Second, func() { lc.Disconnect() }) } // sendLoginDeniedBadVersion sends bad version response func (lc *LoginClient) sendLoginDeniedBadVersion() { data := make([]byte, 12) data[0] = 6 // reply_code: Version mismatch binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF) binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF) packet := &udp.ApplicationPacket{ Opcode: opcodes.OpLoginReplyMsg, Data: data, } lc.connection.SendPacket(packet) time.AfterFunc(1*time.Second, func() { lc.Disconnect() }) } // sendLoginAccepted sends successful login response func (lc *LoginClient) sendLoginAccepted() { config := lc.server.GetConfig() // Build login response packet data := make([]byte, 64) // Base size, will expand as needed offset := 0 // Account ID binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID)) offset += 4 // Login response code (0 = success) data[offset] = 0 offset++ // Do not force SOGA flag data[offset] = 1 offset++ // Subscription level binary.LittleEndian.PutUint32(data[offset:], config.DefaultSubscriptionLevel) offset += 4 // Race flags (enabled races) binary.LittleEndian.PutUint32(data[offset:], 0x1FFFFF) offset += 4 // Class flags (enabled classes) binary.LittleEndian.PutUint32(data[offset:], 0x7FFFFFE) offset += 4 // Username (16-bit string) username := lc.account.Username binary.LittleEndian.PutUint16(data[offset:], uint16(len(username))) offset += 2 copy(data[offset:], username) offset += len(username) // Expansion flags binary.LittleEndian.PutUint16(data[offset:], config.ExpansionFlag) offset += 2 // Additional flags data[offset] = 0xFF data[offset+1] = 0xFF data[offset+2] = 0xFF offset += 3 // Class access flag data[offset] = 0xFF offset++ // Enabled races binary.LittleEndian.PutUint32(data[offset:], config.EnabledRaces) offset += 4 // Cities flag data[offset] = config.CitiesFlag offset++ packet := &udp.ApplicationPacket{ Opcode: opcodes.OpLoginReplyMsg, Data: data[:offset], } lc.connection.SendPacket(packet) } // sendWorldList sends available world servers to client func (lc *LoginClient) sendWorldList() { worlds := lc.server.worldList.GetActiveWorlds() // Build world list packet data := make([]byte, 0, 1024) // Number of worlds worldCount := uint8(len(worlds)) data = append(data, worldCount) for _, world := range worlds { // World ID worldID := make([]byte, 4) binary.LittleEndian.PutUint32(worldID, uint32(world.ID)) data = append(data, worldID...) // World name (16-bit string) nameLen := make([]byte, 2) binary.LittleEndian.PutUint16(nameLen, uint16(len(world.Name))) data = append(data, nameLen...) data = append(data, []byte(world.Name)...) // World status flags var flags uint8 if world.Online { flags |= 0x01 } if world.Locked { flags |= 0x02 } if world.Hidden { flags |= 0x04 } data = append(data, flags) // Population (0-3, where 3 = full) data = append(data, world.PopulationLevel) } packet := &udp.ApplicationPacket{ Opcode: opcodes.OpWorldListMsg, Data: data, } lc.connection.SendPacket(packet) } // sendCharacterList sends character list to client func (lc *LoginClient) sendCharacterList() { if lc.account == nil { return } data := make([]byte, 0, 2048) // Number of characters charCount := uint8(len(lc.account.Characters)) data = append(data, charCount) // Character data for _, char := range lc.account.Characters { if char.Deleted { continue } // Character ID charID := make([]byte, 4) binary.LittleEndian.PutUint32(charID, uint32(char.ID)) data = append(data, charID...) // Server ID serverID := make([]byte, 4) binary.LittleEndian.PutUint32(serverID, uint32(char.ServerID)) data = append(data, serverID...) // Character name (16-bit string) nameLen := make([]byte, 2) binary.LittleEndian.PutUint16(nameLen, uint16(len(char.Name))) data = append(data, nameLen...) data = append(data, []byte(char.Name)...) // Character stats data = append(data, byte(char.Race)) data = append(data, byte(char.Gender)) data = append(data, byte(char.Class)) data = append(data, byte(char.Level)) // Creation timestamp timestamp := make([]byte, 4) binary.LittleEndian.PutUint32(timestamp, uint32(char.CreatedDate.Unix())) data = append(data, timestamp...) } // Account info accountID := make([]byte, 4) binary.LittleEndian.PutUint32(accountID, uint32(lc.account.ID)) data = append(data, accountID...) // Max characters data = append(data, 0xFF, 0xFF, 0xFF, 0xFF) // unknown1 data = append(data, 0x00, 0x00) // unknown2 data = append(data, 0x07, 0x00, 0x00, 0x00) // max chars (7) data = append(data, 0x00) // unknown4 packet := &udp.ApplicationPacket{ Opcode: opcodes.OpAllCharactersDescReplyMsg, Data: data, } lc.connection.SendPacket(packet) } // handleCharacterCreateRequest processes character creation func (lc *LoginClient) handleCharacterCreateRequest(packet *udp.ApplicationPacket) { if !lc.authenticated { lc.Disconnect() return } parsed, err := lc.parsePacketWithDefinition(opcodes.OpCreateCharacterRequestMsg, packet.Data) if err != nil { log.Printf("Failed to parse character create request: %v", err) lc.sendCharacterCreateFailed(1) return } serverID, ok := parsed["server_id"].(uint32) if !ok { lc.sendCharacterCreateFailed(1) return } name, ok := parsed["character_name"].(string) if !ok { lc.sendCharacterCreateFailed(1) return } // Validate character name if len(name) < 3 || len(name) > 20 { lc.sendCharacterCreateFailed(9) // Bad name length return } // Check if name is taken exists, err := lc.server.database.CharacterNameExists(name, int32(serverID)) if err != nil { log.Printf("Error checking character name: %v", err) lc.sendCharacterCreateFailed(1) return } if exists { lc.sendCharacterCreateFailed(12) // Name taken return } // Create character in database char := &Character{ AccountID: lc.account.ID, ServerID: int32(serverID), Name: name, Level: 1, Race: 1, // Would be parsed from packet Gender: 1, // Would be parsed from packet Class: 1, // Would be parsed from packet CreatedDate: time.Now(), } charID, err := lc.server.database.CreateCharacter(char) if err != nil { log.Printf("Error creating character: %v", err) lc.sendCharacterCreateFailed(1) return } char.ID = charID lc.account.Characters = append(lc.account.Characters, char) lc.sendCharacterCreateSuccess(char) lc.sendCharacterList() // Refresh character list } // sendCharacterCreateSuccess sends successful character creation response func (lc *LoginClient) sendCharacterCreateSuccess(char *Character) { data := make([]byte, 64) offset := 0 // Account ID binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID)) offset += 4 // Response code (0 = success) binary.LittleEndian.PutUint32(data[offset:], 0) offset += 4 // Character name nameLen := uint16(len(char.Name)) binary.LittleEndian.PutUint16(data[offset:], nameLen) offset += 2 copy(data[offset:], char.Name) offset += int(nameLen) packet := &udp.ApplicationPacket{ Opcode: opcodes.OpCreateCharacterReplyMsg, Data: data[:offset], } lc.connection.SendPacket(packet) } // sendCharacterCreateFailed sends character creation failure response func (lc *LoginClient) sendCharacterCreateFailed(reason uint8) { data := make([]byte, 16) binary.LittleEndian.PutUint32(data[0:], uint32(lc.account.ID)) data[4] = reason packet := &udp.ApplicationPacket{ Opcode: opcodes.OpCreateCharacterReplyMsg, Data: data, } lc.connection.SendPacket(packet) } // handleCharacterDeleteRequest processes character deletion func (lc *LoginClient) handleCharacterDeleteRequest(packet *udp.ApplicationPacket) { if !lc.authenticated { lc.Disconnect() return } parsed, err := lc.parsePacketWithDefinition(opcodes.OpDeleteCharacterRequestMsg, packet.Data) if err != nil { log.Printf("Failed to parse character delete request: %v", err) return } charID, ok := parsed["character_id"].(uint32) if !ok { return } serverID, ok := parsed["server_id"].(uint32) if !ok { return } // Verify character belongs to this account var char *Character for _, c := range lc.account.Characters { if c.ID == int32(charID) && c.ServerID == int32(serverID) { char = c break } } if char == nil { log.Printf("Account %d attempted to delete character %d that doesn't belong to them", lc.account.ID, charID) return } // Mark character as deleted err = lc.server.database.DeleteCharacter(int32(charID), lc.account.ID) if err != nil { log.Printf("Error deleting character: %v", err) return } char.Deleted = true // Send deletion response data := make([]byte, 24) data[0] = 1 // Success binary.LittleEndian.PutUint32(data[4:], serverID) binary.LittleEndian.PutUint32(data[8:], charID) binary.LittleEndian.PutUint32(data[12:], uint32(lc.account.ID)) responsePacket := &udp.ApplicationPacket{ Opcode: opcodes.OpDeleteCharacterReplyMsg, Data: data, } lc.connection.SendPacket(responsePacket) lc.sendCharacterList() // Refresh character list } // handlePlayCharacterRequest processes character selection for gameplay func (lc *LoginClient) handlePlayCharacterRequest(packet *udp.ApplicationPacket) { if !lc.authenticated { lc.Disconnect() return } parsed, err := lc.parsePacketWithDefinition(opcodes.OpPlayCharacterRequestMsg, packet.Data) if err != nil { log.Printf("Failed to parse play character request: %v", err) lc.sendPlayFailed(1) return } charID, ok := parsed["character_id"].(uint32) if !ok { lc.sendPlayFailed(1) return } serverID, ok := parsed["server_id"].(uint32) if !ok { lc.sendPlayFailed(1) return } // Find world server world := lc.server.worldList.GetWorld(int32(serverID)) if world == nil || !world.Online { lc.sendPlayFailed(2) // Server unavailable return } // Verify character ownership var char *Character for _, c := range lc.account.Characters { if c.ID == int32(charID) && c.ServerID == int32(serverID) { char = c break } } if char == nil { lc.sendPlayFailed(1) // Character not found return } lc.pendingPlayCharID = int32(charID) // Send play request to world server err = lc.server.worldList.SendPlayRequest(world, lc.account.ID, int32(charID)) if err != nil { log.Printf("Error sending play request to world server: %v", err) lc.sendPlayFailed(1) return } // World server will respond with connection details log.Printf("Account %s requesting to play character %s on server %s", lc.account.Username, char.Name, world.Name) } // sendPlayFailed sends play character failure response func (lc *LoginClient) sendPlayFailed(reason uint8) { data := make([]byte, 16) data[0] = reason binary.LittleEndian.PutUint32(data[4:], uint32(lc.account.ID)) packet := &udp.ApplicationPacket{ Opcode: opcodes.OpPlayCharacterReplyMsg, Data: data, } lc.connection.SendPacket(packet) }