eq2go/internal/chat/chat.go
2025-08-23 18:01:26 -05:00

566 lines
16 KiB
Go

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<<raceID)) != 0
}
// CanJoinChannelByClass checks class restrictions (C++ API compatibility)
func (c *Channel) CanJoinChannelByClass(classID int32) bool {
return c.ClassRestriction == NoClassRestriction || (c.ClassRestriction&(1<<classID)) != 0
}
// IsInChannel checks if character is in channel (C++ API compatibility)
func (c *Channel) IsInChannel(characterID int32) bool {
for _, id := range c.members {
if id == characterID {
return true
}
}
return false
}
// JoinChannel adds character to channel (C++ API compatibility)
func (c *Channel) JoinChannel(characterID int32) error {
if c.IsInChannel(characterID) {
return fmt.Errorf("character %d already in channel %s", characterID, c.Name)
}
c.members = append(c.members, characterID)
return nil
}
// LeaveChannel removes character from channel (C++ API compatibility)
func (c *Channel) LeaveChannel(characterID int32) error {
for i, id := range c.members {
if id == characterID {
c.members = append(c.members[:i], c.members[i+1:]...)
return nil
}
}
return fmt.Errorf("character %d not in channel %s", characterID, c.Name)
}
// GetMembers returns copy of member list
func (c *Channel) GetMembers() []int32 {
members := make([]int32, len(c.members))
copy(members, c.members)
return members
}
// IsEmpty returns true if channel has no members
func (c *Channel) IsEmpty() bool {
return len(c.members) == 0
}
// Manager provides centralized chat management with packet support
type Manager struct {
channels map[int32]*Channel
byName map[string]*Channel // Name -> 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
}