eq2go/internal/chat/chat.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
}