417 lines
9.7 KiB
Go
417 lines
9.7 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 interface{} // 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)
|
|
} |