eq2go/internal/world/client_list.go
2025-08-07 11:21:56 -05:00

418 lines
9.5 KiB
Go

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)
}