eq2go/internal/chat/master.go

293 lines
7.6 KiB
Go

package chat
import (
"fmt"
"strings"
"eq2emu/internal/common"
"eq2emu/internal/database"
)
// MasterList manages a collection of channels using the generic MasterList base
type MasterList struct {
*common.MasterList[int32, *Channel]
}
// NewMasterList creates a new channel master list
func NewMasterList() *MasterList {
return &MasterList{
MasterList: common.NewMasterList[int32, *Channel](),
}
}
// AddChannel adds a channel to the master list
func (ml *MasterList) AddChannel(channel *Channel) bool {
return ml.Add(channel)
}
// GetChannel retrieves a channel by ID
func (ml *MasterList) GetChannel(id int32) *Channel {
return ml.Get(id)
}
// GetChannelSafe retrieves a channel by ID with existence check
func (ml *MasterList) GetChannelSafe(id int32) (*Channel, bool) {
return ml.GetSafe(id)
}
// HasChannel checks if a channel exists by ID
func (ml *MasterList) HasChannel(id int32) bool {
return ml.Exists(id)
}
// RemoveChannel removes a channel by ID
func (ml *MasterList) RemoveChannel(id int32) bool {
return ml.Remove(id)
}
// GetAllChannels returns all channels as a map
func (ml *MasterList) GetAllChannels() map[int32]*Channel {
return ml.GetAll()
}
// GetAllChannelsList returns all channels as a slice
func (ml *MasterList) GetAllChannelsList() []*Channel {
return ml.GetAllSlice()
}
// GetChannelCount returns the number of channels
func (ml *MasterList) GetChannelCount() int {
return ml.Size()
}
// ClearChannels removes all channels from the list
func (ml *MasterList) ClearChannels() {
ml.Clear()
}
// FindChannelsByName finds channels containing the given name substring
func (ml *MasterList) FindChannelsByName(nameSubstring string) []*Channel {
return ml.Filter(func(channel *Channel) bool {
return contains(channel.GetName(), nameSubstring)
})
}
// FindChannelsByType finds channels of a specific type
func (ml *MasterList) FindChannelsByType(channelType int) []*Channel {
return ml.Filter(func(channel *Channel) bool {
return channel.GetType() == channelType
})
}
// GetWorldChannels returns all world channels
func (ml *MasterList) GetWorldChannels() []*Channel {
return ml.FindChannelsByType(ChannelTypeWorld)
}
// GetCustomChannels returns all custom channels
func (ml *MasterList) GetCustomChannels() []*Channel {
return ml.FindChannelsByType(ChannelTypeCustom)
}
// GetActiveChannels returns channels that have members
func (ml *MasterList) GetActiveChannels() []*Channel {
return ml.Filter(func(channel *Channel) bool {
return !channel.IsEmpty()
})
}
// GetEmptyChannels returns channels that have no members
func (ml *MasterList) GetEmptyChannels() []*Channel {
return ml.Filter(func(channel *Channel) bool {
return channel.IsEmpty()
})
}
// GetChannelByName retrieves a channel by name (case-insensitive)
func (ml *MasterList) GetChannelByName(name string) *Channel {
name = strings.ToLower(name)
var foundChannel *Channel
ml.ForEach(func(id int32, channel *Channel) {
if strings.ToLower(channel.GetName()) == name {
foundChannel = channel
}
})
return foundChannel
}
// HasChannelByName checks if a channel exists by name (case-insensitive)
func (ml *MasterList) HasChannelByName(name string) bool {
return ml.GetChannelByName(name) != nil
}
// GetCompatibleChannels returns channels compatible with player restrictions
func (ml *MasterList) GetCompatibleChannels(level, race, class int32) []*Channel {
return ml.Filter(func(channel *Channel) bool {
return channel.CanJoinChannelByLevel(level) &&
channel.CanJoinChannelByRace(race) &&
channel.CanJoinChannelByClass(class)
})
}
// ValidateChannels checks all channels for consistency
func (ml *MasterList) ValidateChannels() []string {
var issues []string
ml.ForEach(func(id int32, channel *Channel) {
if channel == nil {
issues = append(issues, fmt.Sprintf("Channel ID %d is nil", id))
return
}
if channel.GetID() != id {
issues = append(issues, fmt.Sprintf("Channel ID mismatch: map key %d != channel ID %d", id, channel.GetID()))
}
if len(channel.GetName()) == 0 {
issues = append(issues, fmt.Sprintf("Channel ID %d has empty name", id))
}
if channel.GetType() < ChannelTypeNone || channel.GetType() > ChannelTypeCustom {
issues = append(issues, fmt.Sprintf("Channel ID %d has invalid type: %d", id, channel.GetType()))
}
if len(channel.GetName()) > MaxChannelNameLength {
issues = append(issues, fmt.Sprintf("Channel ID %d name too long: %d > %d", id, len(channel.GetName()), MaxChannelNameLength))
}
if len(channel.Password) > MaxChannelPasswordLength {
issues = append(issues, fmt.Sprintf("Channel ID %d password too long: %d > %d", id, len(channel.Password), MaxChannelPasswordLength))
}
})
return issues
}
// IsValid returns true if all channels are valid
func (ml *MasterList) IsValid() bool {
issues := ml.ValidateChannels()
return len(issues) == 0
}
// GetStatistics returns statistics about the channel collection
func (ml *MasterList) GetStatistics() map[string]any {
stats := make(map[string]any)
stats["total_channels"] = ml.Size()
if ml.IsEmpty() {
return stats
}
// Count by channel type
typeCounts := make(map[int]int)
var totalMembers int
var activeChannels int
var minID, maxID int32
first := true
ml.ForEach(func(id int32, channel *Channel) {
typeCounts[channel.GetType()]++
totalMembers += channel.GetNumClients()
if !channel.IsEmpty() {
activeChannels++
}
if first {
minID = id
maxID = id
first = false
} else {
if id < minID {
minID = id
}
if id > maxID {
maxID = id
}
}
})
stats["channels_by_type"] = typeCounts
stats["world_channels"] = typeCounts[ChannelTypeWorld]
stats["custom_channels"] = typeCounts[ChannelTypeCustom]
stats["total_members"] = totalMembers
stats["active_channels"] = activeChannels
stats["min_id"] = minID
stats["max_id"] = maxID
stats["id_range"] = maxID - minID
return stats
}
// LoadAllChannels loads all channels from the database into the master list
func (ml *MasterList) LoadAllChannels(db *database.Database) error {
if db == nil {
return fmt.Errorf("database connection is nil")
}
// Clear existing channels
ml.Clear()
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 := db.Query(query)
if err != nil {
return fmt.Errorf("failed to query channels: %w", err)
}
defer rows.Close()
count := 0
for rows.Next() {
channel := &Channel{
db: db,
isNew: false,
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)
}
if !ml.AddChannel(channel) {
return fmt.Errorf("failed to add channel %d to master list", channel.ID)
}
count++
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating channel rows: %w", err)
}
return nil
}
// LoadAllChannelsFromDatabase is a convenience function that creates a master list and loads all channels
func LoadAllChannelsFromDatabase(db *database.Database) (*MasterList, error) {
masterList := NewMasterList()
err := masterList.LoadAllChannels(db)
if err != nil {
return nil, err
}
return masterList, nil
}
// contains checks if a string contains a substring (case-sensitive)
func contains(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
for i := 0; i <= len(str)-len(substr); i++ {
if str[i:i+len(substr)] == substr {
return true
}
}
return false
}