456 lines
13 KiB
Go
456 lines
13 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"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<<playerRace)) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check class restriction (bitmask)
|
|
if classReq > NoClassRestriction && (classReq&(1<<playerClass)) == 0 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// notifyChannelUpdate sends channel update notifications to all members
|
|
func (cm *ChatManager) notifyChannelUpdate(channelName string, action int, characterName string, excludeCharacterID int32) {
|
|
if channel, exists := cm.channels[strings.ToLower(channelName)]; exists {
|
|
for _, memberID := range channel.members {
|
|
if memberID != excludeCharacterID {
|
|
cm.clientManager.SendChannelUpdate(memberID, channelName, action, characterName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// deliverChannelMessage processes and delivers a message to all channel members
|
|
func (cm *ChatManager) deliverChannelMessage(channel *Channel, message ChannelMessage) error {
|
|
for _, memberID := range channel.members {
|
|
// Process message for language if language processor is available
|
|
processedMessage := message
|
|
if cm.languageProcessor != nil && message.SenderID != 0 {
|
|
if processedText, err := cm.languageProcessor.ProcessMessage(
|
|
message.SenderID, memberID, message.Message, message.LanguageID); err == nil {
|
|
processedMessage.Message = processedText
|
|
}
|
|
}
|
|
|
|
// Send message to member
|
|
if err := cm.clientManager.SendChannelMessage(memberID, processedMessage); err != nil {
|
|
// Log error but continue sending to other members
|
|
// TODO: Add proper logging
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|