eq2go/cmd/login_server/client.go

231 lines
5.8 KiB
Go

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
}