566 lines
16 KiB
Go
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
|
|
} |