515 lines
12 KiB
Go
515 lines
12 KiB
Go
package guilds
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"eq2emu/internal/database"
|
|
)
|
|
|
|
// MasterList provides a bespoke, performance-optimized guild collection with specialized indices
|
|
type MasterList struct {
|
|
// Core storage
|
|
guilds map[int32]*Guild
|
|
mutex sync.RWMutex
|
|
|
|
// Specialized indices for O(1) lookups
|
|
byName map[string]*Guild // name -> guild (case-insensitive)
|
|
byLevel map[int8][]*Guild // level -> guilds
|
|
recruiting []*Guild // guilds with recruiters
|
|
|
|
// Cached metadata with invalidation
|
|
levels []int8
|
|
recruitingMap map[int32]bool // guildID -> isRecruiting
|
|
metaStale bool
|
|
}
|
|
|
|
// NewMasterList creates a new bespoke master list optimized for guild operations
|
|
func NewMasterList() *MasterList {
|
|
return &MasterList{
|
|
guilds: make(map[int32]*Guild),
|
|
byName: make(map[string]*Guild),
|
|
byLevel: make(map[int8][]*Guild),
|
|
recruiting: make([]*Guild, 0),
|
|
recruitingMap: make(map[int32]bool),
|
|
}
|
|
}
|
|
|
|
// Add adds a guild to the master list with O(1) core operations
|
|
func (m *MasterList) Add(guild *Guild) bool {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
guildID := guild.GetID()
|
|
|
|
// Check existence
|
|
if _, exists := m.guilds[guildID]; exists {
|
|
return false
|
|
}
|
|
|
|
// Add to core storage
|
|
m.guilds[guildID] = guild
|
|
|
|
// Update specialized indices
|
|
name := strings.ToLower(guild.GetName())
|
|
m.byName[name] = guild
|
|
|
|
level := guild.GetLevel()
|
|
m.byLevel[level] = append(m.byLevel[level], guild)
|
|
|
|
// Update recruiting index
|
|
isRecruiting := guild.GetNumRecruiters() > 0
|
|
m.recruitingMap[guildID] = isRecruiting
|
|
if isRecruiting {
|
|
m.recruiting = append(m.recruiting, guild)
|
|
}
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// Get retrieves a guild by ID with O(1) performance
|
|
func (m *MasterList) Get(guildID int32) *Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return m.guilds[guildID]
|
|
}
|
|
|
|
// GetByName retrieves a guild by name with O(1) performance
|
|
func (m *MasterList) GetByName(name string) *Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return m.byName[strings.ToLower(name)]
|
|
}
|
|
|
|
// GetByLevel returns all guilds of a specific level with O(1) performance
|
|
func (m *MasterList) GetByLevel(level int8) []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
guilds := m.byLevel[level]
|
|
if guilds == nil {
|
|
return nil
|
|
}
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*Guild, len(guilds))
|
|
copy(result, guilds)
|
|
return result
|
|
}
|
|
|
|
// GetRecruiting returns all guilds that are recruiting with O(1) performance
|
|
func (m *MasterList) GetRecruiting() []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]*Guild, len(m.recruiting))
|
|
copy(result, m.recruiting)
|
|
return result
|
|
}
|
|
|
|
// GetByLevelRange returns guilds within a level range using optimized range queries
|
|
func (m *MasterList) GetByLevelRange(minLevel, maxLevel int8) []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
var result []*Guild
|
|
for level := minLevel; level <= maxLevel; level++ {
|
|
if guilds := m.byLevel[level]; guilds != nil {
|
|
result = append(result, guilds...)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetRecruitingByLevel returns recruiting guilds of a specific level using set intersection
|
|
func (m *MasterList) GetRecruitingByLevel(level int8) []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
levelGuilds := m.byLevel[level]
|
|
if levelGuilds == nil {
|
|
return nil
|
|
}
|
|
|
|
var result []*Guild
|
|
for _, guild := range levelGuilds {
|
|
if m.recruitingMap[guild.GetID()] {
|
|
result = append(result, guild)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Remove removes a guild from the master list
|
|
func (m *MasterList) Remove(guildID int32) bool {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
guild := m.guilds[guildID]
|
|
if guild == nil {
|
|
return false
|
|
}
|
|
|
|
// Remove from core storage
|
|
delete(m.guilds, guildID)
|
|
|
|
// Remove from name index
|
|
name := strings.ToLower(guild.GetName())
|
|
delete(m.byName, name)
|
|
|
|
// Remove from level index
|
|
level := guild.GetLevel()
|
|
if levelGuilds := m.byLevel[level]; levelGuilds != nil {
|
|
for i, g := range levelGuilds {
|
|
if g.GetID() == guildID {
|
|
m.byLevel[level] = append(levelGuilds[:i], levelGuilds[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
// Clean up empty level arrays
|
|
if len(m.byLevel[level]) == 0 {
|
|
delete(m.byLevel, level)
|
|
}
|
|
}
|
|
|
|
// Remove from recruiting index
|
|
delete(m.recruitingMap, guildID)
|
|
for i, g := range m.recruiting {
|
|
if g.GetID() == guildID {
|
|
m.recruiting = append(m.recruiting[:i], m.recruiting[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// Update updates a guild's indices when its properties change
|
|
func (m *MasterList) Update(guild *Guild) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
guildID := guild.GetID()
|
|
oldGuild := m.guilds[guildID]
|
|
if oldGuild == nil {
|
|
return
|
|
}
|
|
|
|
// Update core storage
|
|
m.guilds[guildID] = guild
|
|
|
|
// Update name index if changed
|
|
oldName := strings.ToLower(oldGuild.GetName())
|
|
newName := strings.ToLower(guild.GetName())
|
|
if oldName != newName {
|
|
delete(m.byName, oldName)
|
|
m.byName[newName] = guild
|
|
}
|
|
|
|
// Update level index if changed
|
|
oldLevel := oldGuild.GetLevel()
|
|
newLevel := guild.GetLevel()
|
|
if oldLevel != newLevel {
|
|
// Remove from old level
|
|
if levelGuilds := m.byLevel[oldLevel]; levelGuilds != nil {
|
|
for i, g := range levelGuilds {
|
|
if g.GetID() == guildID {
|
|
m.byLevel[oldLevel] = append(levelGuilds[:i], levelGuilds[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(m.byLevel[oldLevel]) == 0 {
|
|
delete(m.byLevel, oldLevel)
|
|
}
|
|
}
|
|
|
|
// Add to new level
|
|
m.byLevel[newLevel] = append(m.byLevel[newLevel], guild)
|
|
}
|
|
|
|
// Update recruiting index if changed
|
|
oldRecruiting := m.recruitingMap[guildID]
|
|
newRecruiting := guild.GetNumRecruiters() > 0
|
|
if oldRecruiting != newRecruiting {
|
|
m.recruitingMap[guildID] = newRecruiting
|
|
|
|
if oldRecruiting && !newRecruiting {
|
|
// Remove from recruiting list
|
|
for i, g := range m.recruiting {
|
|
if g.GetID() == guildID {
|
|
m.recruiting = append(m.recruiting[:i], m.recruiting[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
} else if !oldRecruiting && newRecruiting {
|
|
// Add to recruiting list
|
|
m.recruiting = append(m.recruiting, guild)
|
|
}
|
|
}
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
}
|
|
|
|
// GetAll returns all guilds
|
|
func (m *MasterList) GetAll() []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
result := make([]*Guild, 0, len(m.guilds))
|
|
for _, guild := range m.guilds {
|
|
result = append(result, guild)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Count returns the total number of guilds
|
|
func (m *MasterList) Count() int {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return len(m.guilds)
|
|
}
|
|
|
|
// GetLevels returns all guild levels with lazy caching
|
|
func (m *MasterList) GetLevels() []int8 {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
if !m.metaStale && m.levels != nil {
|
|
return m.levels
|
|
}
|
|
|
|
// Rebuild level cache
|
|
levelSet := make(map[int8]bool)
|
|
for level := range m.byLevel {
|
|
levelSet[level] = true
|
|
}
|
|
|
|
m.levels = make([]int8, 0, len(levelSet))
|
|
for level := range levelSet {
|
|
m.levels = append(m.levels, level)
|
|
}
|
|
|
|
sort.Slice(m.levels, func(i, j int) bool {
|
|
return m.levels[i] < m.levels[j]
|
|
})
|
|
|
|
m.metaStale = false
|
|
return m.levels
|
|
}
|
|
|
|
// GetStatistics returns detailed statistics about the guild system
|
|
func (m *MasterList) GetStatistics() GuildStatistics {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
stats := GuildStatistics{
|
|
TotalGuilds: len(m.guilds),
|
|
}
|
|
|
|
totalMembers := 0
|
|
activeGuilds := 0
|
|
totalRecruiters := len(m.recruiting)
|
|
uniqueAccounts := make(map[int32]bool)
|
|
highestLevel := int8(1)
|
|
|
|
for _, guild := range m.guilds {
|
|
members := guild.GetAllMembers()
|
|
memberCount := len(members)
|
|
|
|
totalMembers += memberCount
|
|
|
|
if memberCount > 0 {
|
|
activeGuilds++
|
|
}
|
|
|
|
// Track unique accounts
|
|
for _, member := range members {
|
|
uniqueAccounts[member.AccountID] = true
|
|
}
|
|
|
|
// Find highest level
|
|
if guild.GetLevel() > highestLevel {
|
|
highestLevel = guild.GetLevel()
|
|
}
|
|
}
|
|
|
|
stats.TotalMembers = totalMembers
|
|
stats.ActiveGuilds = activeGuilds
|
|
stats.TotalRecruiters = totalRecruiters
|
|
stats.UniqueAccounts = len(uniqueAccounts)
|
|
stats.HighestGuildLevel = highestLevel
|
|
|
|
if len(m.guilds) > 0 {
|
|
stats.AverageGuildSize = float64(totalMembers) / float64(len(m.guilds))
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// Search performs efficient guild searches using specialized indices
|
|
func (m *MasterList) Search(criteria GuildSearchCriteria) []*Guild {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
// Start with all guilds or use specialized indices for optimization
|
|
var candidates []*Guild
|
|
|
|
// Use level range optimization if specified
|
|
if criteria.MinLevel > 0 || criteria.MaxLevel > 0 {
|
|
minLevel := criteria.MinLevel
|
|
maxLevel := criteria.MaxLevel
|
|
|
|
if minLevel == 0 {
|
|
minLevel = 1
|
|
}
|
|
if maxLevel == 0 {
|
|
maxLevel = 100 // reasonable max
|
|
}
|
|
|
|
for level := minLevel; level <= maxLevel; level++ {
|
|
if guilds := m.byLevel[level]; guilds != nil {
|
|
candidates = append(candidates, guilds...)
|
|
}
|
|
}
|
|
} else if criteria.RecruitingOnly {
|
|
// Use recruiting index for optimization
|
|
candidates = make([]*Guild, len(m.recruiting))
|
|
copy(candidates, m.recruiting)
|
|
} else {
|
|
// Use all guilds
|
|
candidates = make([]*Guild, 0, len(m.guilds))
|
|
for _, guild := range m.guilds {
|
|
candidates = append(candidates, guild)
|
|
}
|
|
}
|
|
|
|
// Apply remaining filters
|
|
var results []*Guild
|
|
for _, guild := range candidates {
|
|
if m.matchesCriteria(guild, criteria) {
|
|
results = append(results, guild)
|
|
}
|
|
}
|
|
|
|
// Sort by name
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].GetName() < results[j].GetName()
|
|
})
|
|
|
|
return results
|
|
}
|
|
|
|
// LoadFromDatabase loads all guilds from database into the master list
|
|
func (m *MasterList) LoadFromDatabase(db *database.Database) error {
|
|
// Clear existing data
|
|
m.mutex.Lock()
|
|
m.guilds = make(map[int32]*Guild)
|
|
m.byName = make(map[string]*Guild)
|
|
m.byLevel = make(map[int8][]*Guild)
|
|
m.recruiting = make([]*Guild, 0)
|
|
m.recruitingMap = make(map[int32]bool)
|
|
m.metaStale = true
|
|
m.mutex.Unlock()
|
|
|
|
// Load guild IDs first using MySQL-compatible approach
|
|
var guildIDs []int32
|
|
|
|
rows, err := db.Query(`SELECT id FROM guilds ORDER BY id`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var id int32
|
|
if err := rows.Scan(&id); err != nil {
|
|
return err
|
|
}
|
|
guildIDs = append(guildIDs, id)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load each guild and add to master list
|
|
for _, guildID := range guildIDs {
|
|
guild, err := Load(db, guildID)
|
|
if err != nil {
|
|
continue // Skip failed loads
|
|
}
|
|
|
|
m.Add(guild)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper method for criteria matching
|
|
func (m *MasterList) matchesCriteria(guild *Guild, criteria GuildSearchCriteria) bool {
|
|
// Name pattern matching
|
|
if criteria.NamePattern != "" {
|
|
if !strings.Contains(strings.ToLower(guild.GetName()), strings.ToLower(criteria.NamePattern)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Member count range
|
|
memberCount := len(guild.GetAllMembers())
|
|
if criteria.MinMembers > 0 && memberCount < criteria.MinMembers {
|
|
return false
|
|
}
|
|
if criteria.MaxMembers > 0 && memberCount > criteria.MaxMembers {
|
|
return false
|
|
}
|
|
|
|
// Play style
|
|
if criteria.PlayStyle > 0 && guild.GetRecruitingPlayStyle() != criteria.PlayStyle {
|
|
return false
|
|
}
|
|
|
|
// Required flags
|
|
for _, flag := range criteria.RequiredFlags {
|
|
if guild.GetRecruitingFlag(flag) == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Required description tags
|
|
for _, tag := range criteria.RequiredDescTags {
|
|
found := false
|
|
for i := int8(0); i < 4; i++ {
|
|
if guild.GetRecruitingDescTag(i) == tag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Excluded description tags
|
|
for _, tag := range criteria.ExcludedDescTags {
|
|
for i := int8(0); i < 4; i++ {
|
|
if guild.GetRecruitingDescTag(i) == tag {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|