package chat import ( "fmt" "strings" "sync" "time" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Channel represents a chat channel with membership management type Channel struct { ID int32 `json:"id"` Name string `json:"name"` Password string `json:"-"` ChannelType int `json:"type"` LevelRestriction int32 `json:"level_restriction"` RaceRestriction int32 `json:"race_restriction"` ClassRestriction int32 `json:"class_restriction"` DiscordEnabled bool `json:"discord_enabled"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` members []int32 `json:"-"` // Character IDs in channel } // GetID returns the channel ID func (c *Channel) GetID() int32 { return c.ID } // GetName returns the channel name (C++ API compatibility) func (c *Channel) GetName() string { return c.Name } // GetType returns the channel type (C++ API compatibility) func (c *Channel) GetType() int { return c.ChannelType } // GetNumClients returns the number of clients in the channel (C++ API compatibility) func (c *Channel) GetNumClients() int { return len(c.members) } // HasPassword returns true if channel has a password (C++ API compatibility) func (c *Channel) HasPassword() bool { return c.Password != "" } // PasswordMatches checks if password matches (C++ API compatibility) func (c *Channel) PasswordMatches(password string) bool { return c.Password == password } // CanJoinChannelByLevel checks level requirements (C++ API compatibility) func (c *Channel) CanJoinChannelByLevel(level int32) bool { return level >= c.LevelRestriction } // CanJoinChannelByRace checks race restrictions (C++ API compatibility) func (c *Channel) CanJoinChannelByRace(raceID int32) bool { return c.RaceRestriction == NoRaceRestriction || (c.RaceRestriction&(1< Channel for fast lookups mutex sync.RWMutex db *database.Database // Statistics stats struct { ChannelsLoaded int32 PacketsSent int32 PacketErrors int32 MessagesRouted int32 ChannelsCreated int32 } } // NewManager creates a new chat manager func NewManager(db *database.Database) *Manager { return &Manager{ channels: make(map[int32]*Channel), byName: make(map[string]*Channel), db: db, } } // LoadChannels loads all channels from database (C++ API compatibility) func (m *Manager) LoadChannels() error { m.mutex.Lock() defer m.mutex.Unlock() query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels ORDER BY id` rows, err := m.db.Query(query) if err != nil { return fmt.Errorf("failed to query channels: %w", err) } defer rows.Close() // Clear existing data m.channels = make(map[int32]*Channel) m.byName = make(map[string]*Channel) count := int32(0) for rows.Next() { channel := &Channel{members: make([]int32, 0)} err := rows.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType, &channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction, &channel.DiscordEnabled, &channel.Created, &channel.Updated) if err != nil { return fmt.Errorf("failed to scan channel: %w", err) } m.channels[channel.ID] = channel m.byName[strings.ToLower(channel.Name)] = channel count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating channel rows: %w", err) } m.stats.ChannelsLoaded = count return nil } // AddChannel adds a channel to the manager func (m *Manager) AddChannel(channel *Channel) { m.mutex.Lock() defer m.mutex.Unlock() m.channels[channel.ID] = channel m.byName[strings.ToLower(channel.Name)] = channel } // GetChannel gets channel by ID func (m *Manager) GetChannel(id int32) *Channel { m.mutex.RLock() defer m.mutex.RUnlock() return m.channels[id] } // GetChannelByName gets channel by name (case-insensitive) func (m *Manager) GetChannelByName(name string) *Channel { m.mutex.RLock() defer m.mutex.RUnlock() return m.byName[strings.ToLower(name)] } // GetNumChannels returns total channel count (C++ API compatibility) func (m *Manager) GetNumChannels() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.channels) } // ChannelExists checks if channel exists by name (C++ API compatibility) func (m *Manager) ChannelExists(channelName string) bool { return m.GetChannelByName(channelName) != nil } // HasPassword checks if channel has password (C++ API compatibility) func (m *Manager) HasPassword(channelName string) bool { channel := m.GetChannelByName(channelName) return channel != nil && channel.HasPassword() } // PasswordMatches checks channel password (C++ API compatibility) func (m *Manager) PasswordMatches(channelName, password string) bool { channel := m.GetChannelByName(channelName) return channel != nil && channel.PasswordMatches(password) } // CreateChannel creates a new channel (C++ API compatibility) func (m *Manager) CreateChannel(channelName string, password ...string) bool { m.mutex.Lock() defer m.mutex.Unlock() // Check if already exists if m.byName[strings.ToLower(channelName)] != nil { return false } // Create new channel channel := &Channel{ ID: int32(len(m.channels) + 1), // Simple ID assignment Name: channelName, ChannelType: ChannelTypeCustom, Created: time.Now(), Updated: time.Now(), members: make([]int32, 0), } if len(password) > 0 { channel.Password = password[0] } m.channels[channel.ID] = channel m.byName[strings.ToLower(channelName)] = channel m.stats.ChannelsCreated++ return true } // IsInChannel checks if client is in channel (C++ API compatibility) func (m *Manager) IsInChannel(characterID int32, channelName string) bool { channel := m.GetChannelByName(channelName) return channel != nil && channel.IsInChannel(characterID) } // JoinChannel adds client to channel (C++ API compatibility) func (m *Manager) JoinChannel(characterID int32, channelName string) bool { channel := m.GetChannelByName(channelName) if channel == nil { return false } return channel.JoinChannel(characterID) == nil } // LeaveChannel removes client from channel (C++ API compatibility) func (m *Manager) LeaveChannel(characterID int32, channelName string) bool { channel := m.GetChannelByName(channelName) if channel == nil { return false } return channel.LeaveChannel(characterID) == nil } // LeaveAllChannels removes client from all channels (C++ API compatibility) func (m *Manager) LeaveAllChannels(characterID int32) bool { m.mutex.Lock() defer m.mutex.Unlock() success := true for _, channel := range m.channels { if channel.IsInChannel(characterID) { if err := channel.LeaveChannel(characterID); err != nil { success = false } } } return success } // GetWorldChannelList builds world channel list packet (C++ API compatibility) func (m *Manager) GetWorldChannelList(characterID int32, clientVersion uint32, level, race, class int32) ([]byte, error) { packet, exists := packets.GetPacket("AvailWorldChannels") if !exists { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to get AvailWorldChannels packet structure: packet not found") } // Get world channels that player can join m.mutex.RLock() var channelNames []string for _, channel := range m.channels { if channel.GetType() == ChannelTypeWorld && channel.CanJoinChannelByLevel(level) && channel.CanJoinChannelByRace(race) && channel.CanJoinChannelByClass(class) { channelNames = append(channelNames, channel.GetName()) } } m.mutex.RUnlock() // Build packet data channelArray := make([]map[string]interface{}, len(channelNames)) for i, name := range channelNames { channelArray[i] = map[string]interface{}{ "channel_name": name, "unknown": uint8(0), // For version 562+ } } data := map[string]interface{}{ "num_channels": uint32(len(channelNames)), "channel_array": channelArray, } builder := packets.NewPacketBuilder(packet, clientVersion, 0) packetData, err := builder.Build(data) if err != nil { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to build world channel list packet: %v", err) } m.stats.PacketsSent++ return packetData, nil } // SendChannelUpdate builds channel update packet (C++ API compatibility) func (m *Manager) SendChannelUpdate(characterID int32, clientVersion uint32, channelName, playerName string, action int) ([]byte, error) { packet, exists := packets.GetPacket("ChatChannelUpdate") if !exists { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to get ChatChannelUpdate packet structure: packet not found") } data := map[string]interface{}{ "action": uint8(action), "channel_name": channelName, "player_name": playerName, } builder := packets.NewPacketBuilder(packet, clientVersion, 0) packetData, err := builder.Build(data) if err != nil { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to build channel update packet: %v", err) } m.stats.PacketsSent++ return packetData, nil } // TellChannel sends message to channel (C++ API compatibility) func (m *Manager) TellChannel(senderID int32, channelName, message string, clientVersion uint32) ([]byte, error) { channel := m.GetChannelByName(channelName) if channel == nil { return nil, fmt.Errorf("channel %s does not exist", channelName) } if !channel.IsInChannel(senderID) { return nil, fmt.Errorf("character %d not in channel %s", senderID, channelName) } packet, exists := packets.GetPacket("HearChat") if !exists { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to get HearChat packet structure: packet not found") } data := map[string]interface{}{ "understood": uint8(1), "from_spawn_id": uint32(senderID), "to_spawn_id": uint32(0), "from": "Character", // Would be filled with actual character name "to": "", "channel": uint8(3), // Channel type for custom channels "language": uint8(0), // Common language "message": message, "channel_name": channelName, "show_bubble": uint8(0), "time": uint32(time.Now().Unix()), "unknown": uint16(0), "unknown2": make([]uint8, 6), "unknown4": uint8(0), "unknown5": uint8(0), } builder := packets.NewPacketBuilder(packet, clientVersion, 0) packetData, err := builder.Build(data) if err != nil { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to build chat message packet: %v", err) } m.stats.PacketsSent++ m.stats.MessagesRouted++ return packetData, nil } // SendChannelUserList builds who list packet (C++ API compatibility) func (m *Manager) SendChannelUserList(characterID int32, channelName string, clientVersion uint32) ([]byte, error) { channel := m.GetChannelByName(channelName) if channel == nil { return nil, fmt.Errorf("channel %s does not exist", channelName) } // For now, return empty user list since we'd need player manager integration // This follows the C++ pattern but without full implementation m.stats.PacketsSent++ return []byte{}, nil } // GetWorldChannels returns all world channels func (m *Manager) GetWorldChannels() []*Channel { m.mutex.RLock() defer m.mutex.RUnlock() var worldChannels []*Channel for _, channel := range m.channels { if channel.GetType() == ChannelTypeWorld { worldChannels = append(worldChannels, channel) } } return worldChannels } // GetCustomChannels returns all custom channels func (m *Manager) GetCustomChannels() []*Channel { m.mutex.RLock() defer m.mutex.RUnlock() var customChannels []*Channel for _, channel := range m.channels { if channel.GetType() == ChannelTypeCustom { customChannels = append(customChannels, channel) } } return customChannels } // GetStatistics returns current statistics func (m *Manager) GetStatistics() map[string]interface{} { m.mutex.RLock() defer m.mutex.RUnlock() return map[string]interface{}{ "channels_loaded": m.stats.ChannelsLoaded, "packets_sent": m.stats.PacketsSent, "packet_errors": m.stats.PacketErrors, "messages_routed": m.stats.MessagesRouted, "channels_created": m.stats.ChannelsCreated, "total_channels": int32(len(m.channels)), "world_channels": int32(len(m.GetWorldChannels())), "custom_channels": int32(len(m.GetCustomChannels())), } } // CleanupEmptyChannels removes empty custom channels func (m *Manager) CleanupEmptyChannels() int { m.mutex.Lock() defer m.mutex.Unlock() removed := 0 for id, channel := range m.channels { if channel.GetType() == ChannelTypeCustom && channel.IsEmpty() { delete(m.channels, id) delete(m.byName, strings.ToLower(channel.Name)) removed++ } } return removed } // Global manager instance var globalManager *Manager // InitializeManager initializes the global chat manager func InitializeManager(db *database.Database) error { globalManager = NewManager(db) return globalManager.LoadChannels() } // GetManager returns the global chat manager func GetManager() *Manager { return globalManager } // Global functions for C++ API compatibility func GetNumChannels() int { if globalManager != nil { return globalManager.GetNumChannels() } return 0 } func ChannelExists(channelName string) bool { if globalManager != nil { return globalManager.ChannelExists(channelName) } return false } func HasPassword(channelName string) bool { if globalManager != nil { return globalManager.HasPassword(channelName) } return false } func PasswordMatches(channelName, password string) bool { if globalManager != nil { return globalManager.PasswordMatches(channelName, password) } return false } func CreateChannel(channelName string, password ...string) bool { if globalManager != nil { return globalManager.CreateChannel(channelName, password...) } return false } func IsInChannel(characterID int32, channelName string) bool { if globalManager != nil { return globalManager.IsInChannel(characterID, channelName) } return false } func JoinChannel(characterID int32, channelName string) bool { if globalManager != nil { return globalManager.JoinChannel(characterID, channelName) } return false } func LeaveChannel(characterID int32, channelName string) bool { if globalManager != nil { return globalManager.LeaveChannel(characterID, channelName) } return false } func LeaveAllChannels(characterID int32) bool { if globalManager != nil { return globalManager.LeaveAllChannels(characterID) } return false }