package login import ( "crypto/md5" "encoding/binary" "fmt" "log" "strings" "sync" "time" "eq2emu/internal/common/opcodes" "eq2emu/internal/packets" "eq2emu/internal/udp" ) // ClientState represents the current state of a login client type ClientState int const ( ClientStateNew ClientState = iota ClientStateAuthenticating ClientStateAuthenticated ClientStateCharacterSelect ClientStateDisconnected ) // String returns the string representation of the client state func (cs ClientState) String() string { switch cs { case ClientStateNew: return "New" case ClientStateAuthenticating: return "Authenticating" case ClientStateAuthenticated: return "Authenticated" case ClientStateCharacterSelect: return "CharacterSelect" case ClientStateDisconnected: return "Disconnected" default: return "Unknown" } } // Client represents a connected login client type Client struct { connection *udp.Connection database *LoginDB // Client information accountID int32 accountName string accountEmail string accessLevel int16 // Authentication data loginKey string sessionKey string ipAddress string // Client state state ClientState clientVersion uint16 protocolVersion uint16 // Character data characters []CharacterInfo // Timing connectTime time.Time lastActivity time.Time // Synchronization mu sync.RWMutex } // CharacterInfo represents character information for the character select screen type CharacterInfo struct { ID int32 `json:"id"` AccountID int32 `json:"account_id"` Name string `json:"name"` Race int8 `json:"race"` Class int8 `json:"class"` Gender int8 `json:"gender"` Level int16 `json:"level"` Zone int32 `json:"zone"` ZoneInstance int32 `json:"zone_instance"` ServerID int16 `json:"server_id"` LastPlayed int64 `json:"last_played"` CreatedDate int64 `json:"created_date"` DeletedDate int64 `json:"deleted_date"` // Appearance data ModelType int16 `json:"model_type"` SogaModelType int16 `json:"soga_model_type"` HeadType int16 `json:"head_type"` SogaHeadType int16 `json:"soga_head_type"` WingType int16 `json:"wing_type"` ChestType int16 `json:"chest_type"` LegsType int16 `json:"legs_type"` SogaChestType int16 `json:"soga_chest_type"` SogaLegsType int16 `json:"soga_legs_type"` HairType int16 `json:"hair_type"` FacialHairType int16 `json:"facial_hair_type"` SogaHairType int16 `json:"soga_hair_type"` SogaFacialHairType int16 `json:"soga_facial_hair_type"` // Colors HairTypeColor int16 `json:"hair_type_color"` HairTypeHighlight int16 `json:"hair_type_highlight"` HairColor1 int16 `json:"hair_color1"` HairColor2 int16 `json:"hair_color2"` HairHighlight int16 `json:"hair_highlight"` EyeColor1 int16 `json:"eye_color1"` EyeColor2 int16 `json:"eye_color2"` SkinColor int16 `json:"skin_color"` // Soga colors SogaHairTypeColor int16 `json:"soga_hair_type_color"` SogaHairTypeHighlight int16 `json:"soga_hair_type_highlight"` SogaHairColor1 int16 `json:"soga_hair_color1"` SogaHairColor2 int16 `json:"soga_hair_color2"` SogaHairHighlight int16 `json:"soga_hair_highlight"` SogaEyeColor1 int16 `json:"soga_eye_color1"` SogaEyeColor2 int16 `json:"soga_eye_color2"` SogaSkinColor int16 `json:"soga_skin_color"` // Additional data CurrentLanguage int8 `json:"current_language"` ChosenLanguage int8 `json:"chosen_language"` } // NewClient creates a new login client func NewClient(conn *udp.Connection, db *LoginDB) *Client { now := time.Now() client := &Client{ connection: conn, database: db, state: ClientStateNew, connectTime: now, lastActivity: now, ipAddress: conn.GetClientAddr().String(), characters: make([]CharacterInfo, 0), } return client } // HandlePacket processes an incoming packet from the client func (c *Client) HandlePacket(data []byte) error { c.mu.Lock() defer c.mu.Unlock() c.lastActivity = time.Now() if len(data) < 2 { return fmt.Errorf("packet too short") } // Extract opcode opcode := binary.LittleEndian.Uint16(data[:2]) payload := data[2:] log.Printf("Client %s: Received opcode 0x%04x, size %d", c.ipAddress, opcode, len(payload)) switch opcode { case opcodes.OpLoginRequestMsg: return c.handleLoginRequest(payload) case opcodes.OpAllCharactersDescRequestMsg: return c.handleCharacterSelectRequest(payload) case opcodes.OpPlayCharacterRequestMsg: return c.handlePlayCharacterRequest(payload) case opcodes.OpDeleteCharacterRequestMsg: return c.handleDeleteCharacterRequest(payload) case opcodes.OpCreateCharacterRequestMsg: return c.handleCreateCharacterRequest(payload) default: log.Printf("Client %s: Unknown opcode 0x%04x", c.ipAddress, opcode) return nil } } // handleLoginRequest processes a login request from the client func (c *Client) handleLoginRequest(payload []byte) error { if c.state != ClientStateNew { return fmt.Errorf("invalid state for login request: %s", c.state) } // Parse the packet using the proper packet system clientVersion := uint32(562) // Assume version 562 for now data, err := packets.ParsePacketFields(payload, "LoginRequest", clientVersion) if err != nil { return fmt.Errorf("failed to parse login request: %w", err) } // Extract fields based on the XML definition accessCode := getStringField(data, "accesscode") unknown1 := getStringField(data, "unknown1") username := getStringField(data, "username") password := getStringField(data, "password") log.Printf("Login request - Username: %s, Access: %s, Unknown: %s", username, accessCode, unknown1) c.state = ClientStateAuthenticating // Authenticate user return c.authenticateUser(username, password) } // Helper function to safely extract string fields from packet data func getStringField(data map[string]any, field string) string { if val, ok := data[field]; ok { if str, ok := val.(string); ok { return str } } return "" } // authenticateUser authenticates the user credentials func (c *Client) authenticateUser(username, password string) error { username = strings.TrimSpace(strings.Trim(username, "\x00")) // Hash the password (MD5 for EQ2 compatibility) hasher := md5.New() hasher.Write([]byte(password)) hashedPassword := fmt.Sprintf("%x", hasher.Sum(nil)) // Query database for account account, err := c.database.GetLoginAccount(username, hashedPassword) if err != nil { log.Printf("Authentication failed for %s: %v", username, err) return c.sendLoginReply(0, "Invalid username or password") } // Check account status if account.Status != "Active" { log.Printf("Account %s is not active: %s", username, account.Status) return c.sendLoginReply(0, "Account is suspended") } // Store account information c.accountID = account.ID c.accountName = account.Username c.accountEmail = account.Email c.accessLevel = account.AccessLevel c.state = ClientStateAuthenticated log.Printf("User %s (ID: %d) authenticated successfully", username, account.ID) // Generate session key c.sessionKey = c.generateSessionKey() // Update last login c.database.UpdateLastLogin(c.accountID, c.ipAddress) // Send successful login reply return c.sendLoginReply(1, "Welcome to EverQuest II") } // sendLoginReply sends a login reply to the client func (c *Client) sendLoginReply(success int8, message string) error { // Build login reply using the packet system clientVersion := uint32(562) // TODO: Track actual client version data := map[string]any{ "login_response": success, "unknown": message, } if success == 1 { data["account_id"] = c.accountID // Add other required fields for successful login data["parental_control_flag"] = uint8(0) data["parental_control_timer"] = uint32(0) // TODO: Add more fields as needed based on client version } packet, err := packets.BuildPacket("LoginReplyMsg", data, clientVersion, 0) if err != nil { return fmt.Errorf("failed to build login reply packet: %w", err) } // Send the packet appPacket := &udp.ApplicationPacket{ Data: packet, } c.connection.SendPacket(appPacket) // If login successful, send character list if success == 1 { if err := c.loadCharacters(); err != nil { log.Printf("Failed to load characters: %v", err) } return c.sendCharacterList() } return nil } // loadCharacters loads the character list for this account func (c *Client) loadCharacters() error { characters, err := c.database.GetCharacters(c.accountID) if err != nil { return fmt.Errorf("failed to load characters: %w", err) } c.characters = make([]CharacterInfo, len(characters)) for i, char := range characters { c.characters[i] = CharacterInfo{ ID: char.ID, AccountID: char.AccountID, Name: strings.TrimSpace(strings.Trim(char.Name, "\x00")), Race: char.Race, Class: char.Class, Gender: char.Gender, Level: char.Level, Zone: char.Zone, ServerID: char.ServerID, LastPlayed: char.LastPlayed, CreatedDate: char.CreatedDate, } } return nil } // handleCharacterSelectRequest handles character selection func (c *Client) handleCharacterSelectRequest(payload []byte) error { if c.state != ClientStateAuthenticated { return fmt.Errorf("invalid state for character select: %s", c.state) } c.state = ClientStateCharacterSelect // Send character list if err := c.loadCharacters(); err != nil { return fmt.Errorf("failed to load characters: %w", err) } return c.sendCharacterList() } // sendCharacterList sends the character list to the client func (c *Client) sendCharacterList() error { // For now, send character profiles individually using CharSelectProfile packet // In the real implementation, this would be sent as part of LoginReplyMsg for _, char := range c.characters { data := map[string]any{ "version": uint32(562), "charid": uint32(char.ID), "server_id": uint32(char.ServerID), "name": char.Name, "unknown": uint8(0), "race": uint8(char.Race), "class": uint8(char.Class), "gender": uint8(char.Gender), "level": uint32(char.Level), "zone": "Qeynos Harbor", // TODO: Get actual zone name "unknown1": uint32(0), "unknown2": uint32(0), "created_date": uint32(char.CreatedDate), "last_played": uint32(char.LastPlayed), "unknown3": uint32(0), "unknown4": uint32(0), "zonename2": "Qeynos Harbor", "zonedesc": "The Harbor District", "unknown5": uint32(0), "server_name": "EQ2Go Server", "account_id": uint32(c.accountID), } // Add appearance data with defaults for now // TODO: Load actual character appearance data clientVersion := uint32(562) packet, err := packets.BuildPacket("CharSelectProfile", data, clientVersion, 0) if err != nil { log.Printf("Failed to build character profile packet: %v", err) continue } appPacket := &udp.ApplicationPacket{ Data: packet, } c.connection.SendPacket(appPacket) } return nil } // handlePlayCharacterRequest handles play character request func (c *Client) handlePlayCharacterRequest(payload []byte) error { if c.state != ClientStateCharacterSelect && c.state != ClientStateAuthenticated { return fmt.Errorf("invalid state for play character: %s", c.state) } if len(payload) < 8 { return fmt.Errorf("play character packet too short") } characterID := binary.LittleEndian.Uint32(payload[:4]) serverID := binary.LittleEndian.Uint16(payload[4:6]) log.Printf("Play character request - Character: %d, Server: %d", characterID, serverID) // Find character var character *CharacterInfo for i := range c.characters { if c.characters[i].ID == int32(characterID) { character = &c.characters[i] break } } if character == nil { return fmt.Errorf("character %d not found", characterID) } // TODO: Forward to world server return c.sendPlayCharacterReply(character, "127.0.0.1", 9000) } // sendPlayCharacterReply sends play character reply to client func (c *Client) sendPlayCharacterReply(character *CharacterInfo, worldIP string, worldPort int) error { data := map[string]any{ "response": uint8(1), // Success "server": worldIP, "port": uint16(worldPort), "account_id": uint32(c.accountID), "access_code": uint32(12345), // TODO: Generate proper access code } clientVersion := uint32(562) packet, err := packets.BuildPacket("PlayResponse", data, clientVersion, 0) if err != nil { return fmt.Errorf("failed to build play response packet: %w", err) } appPacket := &udp.ApplicationPacket{ Data: packet, } c.connection.SendPacket(appPacket) return nil } // handleDeleteCharacterRequest handles character deletion func (c *Client) handleDeleteCharacterRequest(payload []byte) error { // TODO: Implement character deletion return nil } // handleCreateCharacterRequest handles character creation func (c *Client) handleCreateCharacterRequest(payload []byte) error { // TODO: Implement character creation return nil } // generateSessionKey generates a unique session key for this client func (c *Client) generateSessionKey() string { now := time.Now() data := fmt.Sprintf("%d-%s-%d", c.accountID, c.ipAddress, now.Unix()) hasher := md5.New() hasher.Write([]byte(data)) return fmt.Sprintf("%x", hasher.Sum(nil)) } // Disconnect disconnects the client func (c *Client) Disconnect(reason string) { c.mu.Lock() defer c.mu.Unlock() if c.state == ClientStateDisconnected { return } log.Printf("Disconnecting client %s: %s", c.ipAddress, reason) c.state = ClientStateDisconnected if c.connection != nil { c.connection.Close() } } // GetState returns the current client state (thread-safe) func (c *Client) GetState() ClientState { c.mu.RLock() defer c.mu.RUnlock() return c.state } // GetAccountID returns the account ID (thread-safe) func (c *Client) GetAccountID() int32 { c.mu.RLock() defer c.mu.RUnlock() return c.accountID } // GetAccountName returns the account name (thread-safe) func (c *Client) GetAccountName() string { c.mu.RLock() defer c.mu.RUnlock() return c.accountName } // GetIPAddress returns the client IP address func (c *Client) GetIPAddress() string { return c.ipAddress } // GetConnection returns the UDP connection func (c *Client) GetConnection() *udp.Connection { return c.connection } // GetConnectTime returns when the client connected func (c *Client) GetConnectTime() time.Time { return c.connectTime } // GetLastActivity returns the last activity time func (c *Client) GetLastActivity() time.Time { c.mu.RLock() defer c.mu.RUnlock() return c.lastActivity } // IsTimedOut returns whether the client has timed out func (c *Client) IsTimedOut(timeout time.Duration) bool { c.mu.RLock() defer c.mu.RUnlock() return time.Since(c.lastActivity) > timeout }