package world import ( "fmt" "strings" "sync" "time" "eq2emu/internal/commands" "eq2emu/internal/entity" "eq2emu/internal/packets" "eq2emu/internal/spawn" ) // Client represents a connected player client type Client struct { // Account information AccountID int32 AccountName string AdminLevel int // Character information CharacterID int32 CharacterName string Player *entity.Entity // Connection information Connection any // TODO: Will be *udp.Connection IPAddress string ConnectedTime time.Time LastActivity time.Time ClientVersion int32 // EQ2 client version // Zone information CurrentZone *ZoneServer ZoneID int32 // State flags IsConnected bool IsLinkdead bool IsAFK bool IsAnonymous bool IsLFG bool // Chat state LastTellFrom string IgnoreList map[string]bool // Group/Guild GroupID int32 GuildID int32 // Pending operations PendingZone *ZoneChangeDetails mutex sync.RWMutex } // ZoneChangeDetails holds information about a zone change type ZoneChangeDetails struct { ZoneID int32 InstanceID int32 X float32 Y float32 Z float32 Heading float32 } // ClientList manages all connected clients type ClientList struct { clients map[int32]*Client // CharacterID -> Client clientsByName map[string]*Client // Lowercase name -> Client clientsByAcct map[int32][]*Client // AccountID -> Clients mutex sync.RWMutex } // NewClientList creates a new client list func NewClientList() *ClientList { return &ClientList{ clients: make(map[int32]*Client), clientsByName: make(map[string]*Client), clientsByAcct: make(map[int32][]*Client), } } // Add adds a client to the list func (cl *ClientList) Add(client *Client) error { cl.mutex.Lock() defer cl.mutex.Unlock() if _, exists := cl.clients[client.CharacterID]; exists { return fmt.Errorf("client with character ID %d already exists", client.CharacterID) } // Add to maps cl.clients[client.CharacterID] = client cl.clientsByName[strings.ToLower(client.CharacterName)] = client // Add to account map cl.clientsByAcct[client.AccountID] = append(cl.clientsByAcct[client.AccountID], client) client.ConnectedTime = time.Now() client.LastActivity = time.Now() client.IsConnected = true return nil } // Remove removes a client from the list func (cl *ClientList) Remove(characterID int32) { cl.mutex.Lock() defer cl.mutex.Unlock() client, exists := cl.clients[characterID] if !exists { return } // Remove from maps delete(cl.clients, characterID) delete(cl.clientsByName, strings.ToLower(client.CharacterName)) // Remove from account map if clients, ok := cl.clientsByAcct[client.AccountID]; ok { newClients := make([]*Client, 0, len(clients)-1) for _, c := range clients { if c.CharacterID != characterID { newClients = append(newClients, c) } } if len(newClients) > 0 { cl.clientsByAcct[client.AccountID] = newClients } else { delete(cl.clientsByAcct, client.AccountID) } } } // GetByCharacterID returns a client by character ID func (cl *ClientList) GetByCharacterID(characterID int32) *Client { cl.mutex.RLock() defer cl.mutex.RUnlock() return cl.clients[characterID] } // GetByCharacterName returns a client by character name func (cl *ClientList) GetByCharacterName(name string) *Client { cl.mutex.RLock() defer cl.mutex.RUnlock() return cl.clientsByName[strings.ToLower(name)] } // GetByAccountID returns all clients for an account func (cl *ClientList) GetByAccountID(accountID int32) []*Client { cl.mutex.RLock() defer cl.mutex.RUnlock() clients := cl.clientsByAcct[accountID] result := make([]*Client, len(clients)) copy(result, clients) return result } // Count returns the total number of connected clients func (cl *ClientList) Count() int32 { cl.mutex.RLock() defer cl.mutex.RUnlock() return int32(len(cl.clients)) } // GetAll returns all connected clients func (cl *ClientList) GetAll() []*Client { cl.mutex.RLock() defer cl.mutex.RUnlock() result := make([]*Client, 0, len(cl.clients)) for _, client := range cl.clients { result = append(result, client) } return result } // ProcessAll processes all clients func (cl *ClientList) ProcessAll() { clients := cl.GetAll() now := time.Now() for _, client := range clients { client.Process(now) } } // DisconnectAll disconnects all clients func (cl *ClientList) DisconnectAll(reason string) { clients := cl.GetAll() for _, client := range clients { client.DisconnectWithReason(reason) } } // BroadcastMessage sends a message to all clients func (cl *ClientList) BroadcastMessage(message string) { clients := cl.GetAll() for _, client := range clients { client.SendSimpleMessage(message) } } // Process handles client processing func (c *Client) Process(now time.Time) { c.mutex.Lock() defer c.mutex.Unlock() if !c.IsConnected { return } // Check for linkdead timeout if now.Sub(c.LastActivity) > 5*time.Minute { if !c.IsLinkdead { c.IsLinkdead = true fmt.Printf("Client %s has gone linkdead\n", c.CharacterName) } // Disconnect after 10 minutes if now.Sub(c.LastActivity) > 10*time.Minute { c.DisconnectWithReason("Linkdead timeout") } } // Process pending zone change if c.PendingZone != nil { // TODO: Implement zone change c.PendingZone = nil } } // DisconnectWithReason disconnects the client with a reason func (c *Client) DisconnectWithReason(reason string) { c.mutex.Lock() defer c.mutex.Unlock() if !c.IsConnected { return } fmt.Printf("Disconnecting client %s: %s\n", c.CharacterName, reason) // Remove from current zone if c.CurrentZone != nil { c.CurrentZone.RemoveClient(c.CharacterID) c.CurrentZone = nil } // TODO: Save character data // TODO: Close connection c.IsConnected = false } // SendSimpleMessage sends a simple message to the client func (c *Client) SendSimpleMessage(message string) { // TODO: Implement when UDP connection is available fmt.Printf("[%s] %s\n", c.CharacterName, message) } // Zone changes the client's zone func (c *Client) Zone(details *ZoneChangeDetails) { c.mutex.Lock() defer c.mutex.Unlock() c.PendingZone = details } // UpdateActivity updates the client's last activity time func (c *Client) UpdateActivity() { c.mutex.Lock() defer c.mutex.Unlock() c.LastActivity = time.Now() if c.IsLinkdead { c.IsLinkdead = false fmt.Printf("Client %s is no longer linkdead\n", c.CharacterName) } } // Command interface implementations for Client // GetPlayer implements commands.ClientInterface func (c *Client) GetPlayer() *entity.Entity { c.mutex.RLock() defer c.mutex.RUnlock() return c.Player } // GetAccountID implements commands.ClientInterface func (c *Client) GetAccountID() int32 { return c.AccountID } // GetCharacterID implements commands.ClientInterface func (c *Client) GetCharacterID() int32 { return c.CharacterID } // GetAdminLevel implements commands.ClientInterface func (c *Client) GetAdminLevel() int { return c.AdminLevel } // GetName implements commands.ClientInterface func (c *Client) GetName() string { return c.CharacterName } // IsInZone implements commands.ClientInterface func (c *Client) IsInZone() bool { c.mutex.RLock() defer c.mutex.RUnlock() return c.CurrentZone != nil } // GetZone implements commands.ClientInterface func (c *Client) GetZone() commands.ZoneInterface { c.mutex.RLock() defer c.mutex.RUnlock() if c.CurrentZone != nil { return &ZoneAdapter{zone: c.CurrentZone} } return nil } // SendMessage implements commands.ClientInterface (channel version) func (c *Client) SendMessage(channel int, color int, message string) { // TODO: Implement channel-based messaging when packets are available fmt.Printf("[%s][Ch:%d] %s\n", c.CharacterName, channel, message) } // SendPopupMessage implements commands.ClientInterface func (c *Client) SendPopupMessage(message string) { // TODO: Implement popup messaging when packets are available c.SendMessage(0, 0, fmt.Sprintf("[POPUP] %s", message)) } // Disconnect implements commands.ClientInterface func (c *Client) Disconnect() { c.DisconnectWithReason("Disconnected by command") } // ZoneAdapter adapts ZoneServer to commands.ZoneInterface type ZoneAdapter struct { zone *ZoneServer } func (za *ZoneAdapter) GetID() int32 { return za.zone.ID } func (za *ZoneAdapter) GetName() string { return za.zone.Name } func (za *ZoneAdapter) GetDescription() string { return za.zone.Description } func (za *ZoneAdapter) GetPlayers() []*entity.Entity { // TODO: Implement when entity package is fully integrated return nil } func (za *ZoneAdapter) Shutdown() { za.zone.Shutdown() } func (za *ZoneAdapter) SendZoneMessage(channel int, color int, message string) { // TODO: Implement zone-wide messaging } func (za *ZoneAdapter) GetSpawnByName(name string) *spawn.Spawn { // TODO: Implement spawn lookup return nil } func (za *ZoneAdapter) GetSpawnByID(id int32) *spawn.Spawn { // TODO: Implement spawn lookup return nil } // GetClientVersion returns the client version func (c *Client) GetClientVersion() int32 { c.mutex.RLock() defer c.mutex.RUnlock() return c.ClientVersion } // SetClientVersion sets the client version func (c *Client) SetClientVersion(version int32) { c.mutex.Lock() defer c.mutex.Unlock() c.ClientVersion = version } // ProcessPacket processes an incoming packet for this client func (c *Client) ProcessPacket(world *World, rawData []byte, clientOpcode uint16) error { // Create packet context ctx := world.CreatePacketContext(c) // Process the packet through the global packet processor return packets.ProcessGlobalPacket(ctx, rawData, clientOpcode) }