package chat import ( "context" "fmt" "strings" "sync" "time" ) // NewChatManager creates a new chat manager instance func NewChatManager(database ChannelDatabase, clientManager ClientManager, playerManager PlayerManager, languageProcessor LanguageProcessor) *ChatManager { return &ChatManager{ channels: make(map[string]*Channel), database: database, clientManager: clientManager, playerManager: playerManager, languageProcessor: languageProcessor, } } // Initialize loads world channels from database and prepares the chat system func (cm *ChatManager) Initialize(ctx context.Context) error { cm.mu.Lock() defer cm.mu.Unlock() // Load world channels from database worldChannels, err := cm.database.LoadWorldChannels(ctx) if err != nil { return fmt.Errorf("failed to load world channels: %w", err) } // Create world channels for _, channelData := range worldChannels { channel := &Channel{ name: channelData.Name, password: channelData.Password, channelType: ChannelTypeWorld, levelRestriction: channelData.LevelRestriction, raceRestriction: channelData.RaceRestriction, classRestriction: channelData.ClassRestriction, members: make([]int32, 0), created: time.Now(), } cm.channels[strings.ToLower(channelData.Name)] = channel } return nil } // AddChannel adds a new channel to the manager (used for world channels loaded from database) func (cm *ChatManager) AddChannel(channel *Channel) { cm.mu.Lock() defer cm.mu.Unlock() cm.channels[strings.ToLower(channel.name)] = channel } // GetNumChannels returns the total number of channels func (cm *ChatManager) GetNumChannels() int { cm.mu.RLock() defer cm.mu.RUnlock() return len(cm.channels) } // GetWorldChannelList returns filtered list of world channels for a client func (cm *ChatManager) GetWorldChannelList(characterID int32) ([]ChannelInfo, error) { cm.mu.RLock() defer cm.mu.RUnlock() playerInfo, err := cm.playerManager.GetPlayerInfo(characterID) if err != nil { return nil, fmt.Errorf("failed to get player info: %w", err) } var channelList []ChannelInfo for _, channel := range cm.channels { if channel.channelType == ChannelTypeWorld { // Check if player can join based on restrictions if cm.canJoinChannel(playerInfo.Level, playerInfo.Race, playerInfo.Class, channel.levelRestriction, channel.raceRestriction, channel.classRestriction) { channelInfo := ChannelInfo{ Name: channel.name, HasPassword: channel.password != "", MemberCount: len(channel.members), LevelRestriction: channel.levelRestriction, RaceRestriction: channel.raceRestriction, ClassRestriction: channel.classRestriction, ChannelType: channel.channelType, } channelList = append(channelList, channelInfo) } } } return channelList, nil } // ChannelExists checks if a channel with the given name exists func (cm *ChatManager) ChannelExists(channelName string) bool { cm.mu.RLock() defer cm.mu.RUnlock() _, exists := cm.channels[strings.ToLower(channelName)] return exists } // HasPassword checks if a channel has a password func (cm *ChatManager) HasPassword(channelName string) bool { cm.mu.RLock() defer cm.mu.RUnlock() if channel, exists := cm.channels[strings.ToLower(channelName)]; exists { return channel.password != "" } return false } // PasswordMatches checks if the provided password matches the channel password func (cm *ChatManager) PasswordMatches(channelName, password string) bool { cm.mu.RLock() defer cm.mu.RUnlock() if channel, exists := cm.channels[strings.ToLower(channelName)]; exists { return channel.password == password } return false } // CreateChannel creates a new custom channel func (cm *ChatManager) CreateChannel(channelName string, password ...string) error { if len(channelName) > MaxChannelNameLength { return fmt.Errorf("channel name too long: %d > %d", len(channelName), MaxChannelNameLength) } cm.mu.Lock() defer cm.mu.Unlock() // Check if channel already exists if _, exists := cm.channels[strings.ToLower(channelName)]; exists { return fmt.Errorf("channel %s already exists", channelName) } // Create new custom channel channel := &Channel{ name: channelName, channelType: ChannelTypeCustom, members: make([]int32, 0), created: time.Now(), } // Set password if provided if len(password) > 0 && password[0] != "" { if len(password[0]) > MaxChannelPasswordLength { return fmt.Errorf("channel password too long: %d > %d", len(password[0]), MaxChannelPasswordLength) } channel.password = password[0] } cm.channels[strings.ToLower(channelName)] = channel return nil } // IsInChannel checks if a character is in the specified channel func (cm *ChatManager) IsInChannel(characterID int32, channelName string) bool { cm.mu.RLock() defer cm.mu.RUnlock() if channel, exists := cm.channels[strings.ToLower(channelName)]; exists { return channel.isInChannel(characterID) } return false } // JoinChannel adds a character to a channel func (cm *ChatManager) JoinChannel(characterID int32, channelName string, password ...string) error { cm.mu.Lock() defer cm.mu.Unlock() channel, exists := cm.channels[strings.ToLower(channelName)] if !exists { return fmt.Errorf("channel %s does not exist", channelName) } // Check password if channel has one if channel.password != "" { if len(password) == 0 || password[0] != channel.password { return fmt.Errorf("invalid password for channel %s", channelName) } } // Get player info for validation playerInfo, err := cm.playerManager.GetPlayerInfo(characterID) if err != nil { return fmt.Errorf("failed to get player info: %w", err) } // Validate restrictions if !cm.canJoinChannel(playerInfo.Level, playerInfo.Race, playerInfo.Class, channel.levelRestriction, channel.raceRestriction, channel.classRestriction) { return fmt.Errorf("player does not meet channel requirements") } // Add to channel if err := channel.joinChannel(characterID); err != nil { return err } // Notify all channel members of the join cm.notifyChannelUpdate(channelName, ChatChannelOtherJoin, playerInfo.CharacterName, characterID) return nil } // LeaveChannel removes a character from a channel func (cm *ChatManager) LeaveChannel(characterID int32, channelName string) error { cm.mu.Lock() defer cm.mu.Unlock() channel, exists := cm.channels[strings.ToLower(channelName)] if !exists { return fmt.Errorf("channel %s does not exist", channelName) } // Get player info for notification playerInfo, err := cm.playerManager.GetPlayerInfo(characterID) if err != nil { return fmt.Errorf("failed to get player info: %w", err) } // Remove from channel if err := channel.leaveChannel(characterID); err != nil { return err } // Delete custom channels with no members if channel.channelType == ChannelTypeCustom && len(channel.members) == 0 { delete(cm.channels, strings.ToLower(channelName)) } // Notify all remaining channel members of the leave cm.notifyChannelUpdate(channelName, ChatChannelOtherLeave, playerInfo.CharacterName, characterID) return nil } // LeaveAllChannels removes a character from all channels they're in func (cm *ChatManager) LeaveAllChannels(characterID int32) error { cm.mu.Lock() defer cm.mu.Unlock() playerInfo, err := cm.playerManager.GetPlayerInfo(characterID) if err != nil { return fmt.Errorf("failed to get player info: %w", err) } // Find all channels the player is in and remove them var channelsToDelete []string for channelName, channel := range cm.channels { if channel.isInChannel(characterID) { channel.leaveChannel(characterID) // Mark custom channels with no members for deletion if channel.channelType == ChannelTypeCustom && len(channel.members) == 0 { channelsToDelete = append(channelsToDelete, channelName) } else { // Notify remaining members cm.notifyChannelUpdate(channel.name, ChatChannelOtherLeave, playerInfo.CharacterName, characterID) } } } // Delete empty custom channels for _, channelName := range channelsToDelete { delete(cm.channels, channelName) } return nil } // TellChannel sends a message to all members of a channel func (cm *ChatManager) TellChannel(senderID int32, channelName, message string, customName ...string) error { cm.mu.RLock() defer cm.mu.RUnlock() channel, exists := cm.channels[strings.ToLower(channelName)] if !exists { return fmt.Errorf("channel %s does not exist", channelName) } // Check if sender is in channel (unless it's a system message) if senderID != 0 && !channel.isInChannel(senderID) { return fmt.Errorf("sender is not in channel %s", channelName) } // Get sender info var senderName string var languageID int32 if senderID != 0 { playerInfo, err := cm.playerManager.GetPlayerInfo(senderID) if err != nil { return fmt.Errorf("failed to get sender info: %w", err) } senderName = playerInfo.CharacterName // Get sender's default language if cm.languageProcessor != nil { languageID = cm.languageProcessor.GetDefaultLanguage(senderID) } } // Use custom name if provided (for system messages) if len(customName) > 0 && customName[0] != "" { senderName = customName[0] } // Create message chatMessage := ChannelMessage{ SenderID: senderID, SenderName: senderName, Message: message, LanguageID: languageID, ChannelName: channelName, Timestamp: time.Now(), } // Send to all channel members return cm.deliverChannelMessage(channel, chatMessage) } // SendChannelUserList sends the list of users in a channel to a client func (cm *ChatManager) SendChannelUserList(requesterID int32, channelName string) error { cm.mu.RLock() defer cm.mu.RUnlock() channel, exists := cm.channels[strings.ToLower(channelName)] if !exists { return fmt.Errorf("channel %s does not exist", channelName) } // Check if requester is in channel if !channel.isInChannel(requesterID) { return fmt.Errorf("requester is not in channel %s", channelName) } // Build member list var members []ChannelMember for _, memberID := range channel.members { if playerInfo, err := cm.playerManager.GetPlayerInfo(memberID); err == nil { member := ChannelMember{ CharacterID: memberID, CharacterName: playerInfo.CharacterName, Level: playerInfo.Level, Race: playerInfo.Race, Class: playerInfo.Class, JoinedAt: time.Now(), // TODO: Track actual join time } members = append(members, member) } } // Send user list to requester return cm.clientManager.SendChannelUserList(requesterID, channelName, members) } // GetChannel returns a channel by name (for internal use) func (cm *ChatManager) GetChannel(channelName string) *Channel { cm.mu.RLock() defer cm.mu.RUnlock() return cm.channels[strings.ToLower(channelName)] } // GetStatistics returns chat system statistics func (cm *ChatManager) GetStatistics() ChatStatistics { cm.mu.RLock() defer cm.mu.RUnlock() stats := ChatStatistics{ TotalChannels: len(cm.channels), } for _, channel := range cm.channels { switch channel.channelType { case ChannelTypeWorld: stats.WorldChannels++ case ChannelTypeCustom: stats.CustomChannels++ } stats.TotalMembers += len(channel.members) if len(channel.members) > 0 { stats.ActiveChannels++ } } return stats } // Helper methods // canJoinChannel checks if a player meets channel requirements func (cm *ChatManager) canJoinChannel(playerLevel, playerRace, playerClass, levelReq, raceReq, classReq int32) bool { // Check level restriction if levelReq > NoLevelRestriction && playerLevel < levelReq { return false } // Check race restriction (bitmask) if raceReq > NoRaceRestriction && (raceReq&(1< NoClassRestriction && (classReq&(1<