package main import ( "eq2emu/internal/common/opcodes" "eq2emu/internal/udp" "fmt" "log" "time" ) // LoginClient represents a connected client session type LoginClient struct { connection *udp.Connection server *LoginServer account *Account lastActivity time.Time authenticated bool version uint16 sessionID string // Client state needsWorldList bool sentCharacterList bool pendingPlayCharID int32 createRequest *CharacterCreateRequest } // Account represents an authenticated user account type Account struct { ID int32 Username string LSAdmin bool WorldAdmin bool Characters []*Character LastLogin time.Time IPAddress string ClientVersion uint16 } // Character represents a character in the database type Character struct { ID int32 AccountID int32 ServerID int32 Name string Level int8 Race int8 Class int8 Gender int8 CreatedDate time.Time Deleted bool } // CharacterCreateRequest holds pending character creation data type CharacterCreateRequest struct { ServerID int32 Name string Race int8 Gender int8 Class int8 Face int8 Hair int8 HairColor int8 SkinColor int8 EyeColor int8 Timestamp time.Time } // NewLoginClient creates a new login client instance func NewLoginClient(conn *udp.Connection, server *LoginServer) *LoginClient { return &LoginClient{ connection: conn, server: server, lastActivity: time.Now(), sessionID: fmt.Sprintf("%d", conn.GetSessionID()), needsWorldList: true, sentCharacterList: false, } } // ProcessPacket handles incoming packets from the client func (lc *LoginClient) ProcessPacket(packet *udp.ApplicationPacket) { lc.lastActivity = time.Now() switch packet.Opcode { case opcodes.OpLoginRequestMsg: lc.handleLoginRequest(packet) case opcodes.OpAllWSDescRequestMsg: lc.handleWorldListRequest(packet) case opcodes.OpAllCharactersDescRequestMsg: lc.handleCharacterListRequest(packet) case opcodes.OpCreateCharacterRequestMsg: lc.handleCharacterCreateRequest(packet) case opcodes.OpDeleteCharacterRequestMsg: lc.handleCharacterDeleteRequest(packet) case opcodes.OpPlayCharacterRequestMsg: lc.handlePlayCharacterRequest(packet) case opcodes.OpKeymapLoadMsg: // Client keymap request - usually ignored break default: log.Printf("Unknown packet opcode from client %s: 0x%04X", lc.sessionID, packet.Opcode) } } // handleLoginRequest processes client login attempts func (lc *LoginClient) handleLoginRequest(packet *udp.ApplicationPacket) { lc.server.IncrementLoginAttempts() // Parse login request packet loginReq, err := lc.parseLoginRequest(packet.Data) if err != nil { log.Printf("Failed to parse login request from %s: %v", lc.sessionID, err) lc.sendLoginDenied() return } lc.version = loginReq.Version // Check if client version is supported if !lc.server.GetConfig().IsVersionSupported(lc.version) { log.Printf("Unsupported client version %d from %s", lc.version, lc.sessionID) lc.sendLoginDeniedBadVersion() return } // Authenticate with database account, err := lc.server.database.AuthenticateAccount(loginReq.Username, loginReq.Password) if err != nil { log.Printf("Authentication failed for %s: %v", loginReq.Username, err) lc.sendLoginDenied() return } if account == nil { log.Printf("Invalid credentials for %s", loginReq.Username) lc.sendLoginDenied() return } // Check for existing session lc.server.clientMutex.RLock() for _, existingClient := range lc.server.clients { if existingClient.account != nil && existingClient.account.ID == account.ID && existingClient != lc { log.Printf("Account %s already logged in, disconnecting previous session", account.Username) existingClient.Disconnect() break } } lc.server.clientMutex.RUnlock() // Update account info account.LastLogin = time.Now() account.IPAddress = lc.connection.GetClientAddr().IP.String() account.ClientVersion = lc.version lc.server.database.UpdateAccountLogin(account) lc.account = account lc.authenticated = true lc.server.IncrementSuccessfulLogins() log.Printf("User %s successfully authenticated", account.Username) lc.sendLoginAccepted() } // handleWorldListRequest sends the list of available world servers func (lc *LoginClient) handleWorldListRequest(packet *udp.ApplicationPacket) { if !lc.authenticated { lc.Disconnect() return } lc.sendWorldList() lc.needsWorldList = false // Load and send character list if !lc.sentCharacterList { characters, err := lc.server.database.LoadCharacters(lc.account.ID, lc.version) if err != nil { log.Printf("Failed to load characters for account %d: %v", lc.account.ID, err) } else { lc.account.Characters = characters lc.sentCharacterList = true } lc.sendCharacterList() } } // handleCharacterListRequest handles explicit character list requests func (lc *LoginClient) handleCharacterListRequest(packet *udp.ApplicationPacket) { if !lc.authenticated { lc.Disconnect() return } lc.sendCharacterList() } // IsStale returns true if the client connection should be cleaned up func (lc *LoginClient) IsStale() bool { return time.Since(lc.lastActivity) > 5*time.Minute } // Disconnect closes the client connection and cleans up func (lc *LoginClient) Disconnect() { if lc.connection != nil { lc.connection.Close() } // Clean up any pending requests lc.createRequest = nil log.Printf("Client %s disconnected", lc.sessionID) lc.server.RemoveClient(lc.sessionID) } // GetAccount returns the authenticated account func (lc *LoginClient) GetAccount() *Account { return lc.account } // GetVersion returns the client version func (lc *LoginClient) GetVersion() uint16 { return lc.version } // GetSessionID returns the session identifier func (lc *LoginClient) GetSessionID() string { return lc.sessionID }