fix groups

This commit is contained in:
Sky Johnson 2025-08-08 12:55:30 -05:00
parent 27e720e703
commit b08de58336
8 changed files with 1169 additions and 334 deletions

View File

@ -617,50 +617,3 @@ func BenchmarkMemoryAllocation(b *testing.B) {
} }
}) })
} }
// BenchmarkComparisonWithOldSystem provides comparison benchmarks
func BenchmarkComparisonWithOldSystem(b *testing.B) {
ml := NewMasterList()
const numGroups = 1000
// Setup
b.StopTimer()
groups := make([]*Group, numGroups)
for i := 0; i < numGroups; i++ {
groups[i] = createTestGroup(b, int32(i+1), rand.Intn(6)+1)
ml.AddGroup(groups[i])
}
// Cleanup all groups when benchmark is done
defer func() {
for _, group := range groups {
if group != nil {
group.Disband()
}
}
}()
b.StartTimer()
b.Run("ModernizedGroupLookup", func(b *testing.B) {
// Modern generic-based lookup
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(numGroups) + 1)
_ = ml.GetGroup(id)
}
})
})
b.Run("ModernizedGroupFiltering", func(b *testing.B) {
// Modern filter-based operations
for i := 0; i < b.N; i++ {
_ = ml.GetActiveGroups()
}
})
b.Run("ModernizedGroupStatistics", func(b *testing.B) {
// Modern statistics computation
for i := 0; i < b.N; i++ {
_ = ml.GetGroupStatistics()
}
})
}

View File

@ -9,13 +9,13 @@ import (
// Group represents a player group with embedded database operations // Group represents a player group with embedded database operations
type Group struct { type Group struct {
// Core fields // Core fields
GroupID int32 `json:"group_id" db:"group_id"` GroupID int32 `json:"group_id" db:"group_id"`
Options GroupOptions `json:"options"` Options GroupOptions `json:"options"`
Members []*GroupMemberInfo `json:"members"` Members []*GroupMemberInfo `json:"members"`
RaidGroups []int32 `json:"raid_groups"` RaidGroups []int32 `json:"raid_groups"`
CreatedTime time.Time `json:"created_time" db:"created_time"` CreatedTime time.Time `json:"created_time" db:"created_time"`
LastActivity time.Time `json:"last_activity" db:"last_activity"` LastActivity time.Time `json:"last_activity" db:"last_activity"`
Disbanded bool `json:"disbanded" db:"disbanded"` Disbanded bool `json:"disbanded" db:"disbanded"`
// Internal fields // Internal fields
membersMutex sync.RWMutex `json:"-"` membersMutex sync.RWMutex `json:"-"`
@ -29,7 +29,7 @@ type Group struct {
updateQueue chan *GroupUpdate `json:"-"` updateQueue chan *GroupUpdate `json:"-"`
// Background processing // Background processing
stopChan chan struct{} `json:"-"` stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"` wg sync.WaitGroup `json:"-"`
// Database integration - embedded operations // Database integration - embedded operations

View File

@ -9,46 +9,46 @@ import (
// Mock entity implementation for testing // Mock entity implementation for testing
type mockEntity struct { type mockEntity struct {
id int32 id int32
name string name string
level int8 level int8
class int8 class int8
race int8 race int8
hp int32 hp int32
maxHP int32 maxHP int32
power int32 power int32
maxPower int32 maxPower int32
isPlayer bool isPlayer bool
isNPC bool isNPC bool
isBot bool isBot bool
isDead bool isDead bool
zone *mockZone zone *mockZone
groupID int32 groupID int32
groupInfo *GroupMemberInfo groupInfo *GroupMemberInfo
} }
func (m *mockEntity) GetID() int32 { return m.id } func (m *mockEntity) GetID() int32 { return m.id }
func (m *mockEntity) GetName() string { return m.name } func (m *mockEntity) GetName() string { return m.name }
func (m *mockEntity) GetLevel() int8 { return m.level } func (m *mockEntity) GetLevel() int8 { return m.level }
func (m *mockEntity) GetClass() int8 { return m.class } func (m *mockEntity) GetClass() int8 { return m.class }
func (m *mockEntity) GetRace() int8 { return m.race } func (m *mockEntity) GetRace() int8 { return m.race }
func (m *mockEntity) GetHP() int32 { return m.hp } func (m *mockEntity) GetHP() int32 { return m.hp }
func (m *mockEntity) GetTotalHP() int32 { return m.maxHP } func (m *mockEntity) GetTotalHP() int32 { return m.maxHP }
func (m *mockEntity) GetPower() int32 { return m.power } func (m *mockEntity) GetPower() int32 { return m.power }
func (m *mockEntity) GetTotalPower() int32 { return m.maxPower } func (m *mockEntity) GetTotalPower() int32 { return m.maxPower }
func (m *mockEntity) IsPlayer() bool { return m.isPlayer } func (m *mockEntity) IsPlayer() bool { return m.isPlayer }
func (m *mockEntity) IsNPC() bool { return m.isNPC } func (m *mockEntity) IsNPC() bool { return m.isNPC }
func (m *mockEntity) IsBot() bool { return m.isBot } func (m *mockEntity) IsBot() bool { return m.isBot }
func (m *mockEntity) IsDead() bool { return m.isDead } func (m *mockEntity) IsDead() bool { return m.isDead }
func (m *mockEntity) GetZone() Zone { return m.zone } func (m *mockEntity) GetZone() Zone { return m.zone }
func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 } func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 }
// GroupAware implementation // GroupAware implementation
func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo } func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo }
func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info } func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info }
func (m *mockEntity) GetGroupID() int32 { return m.groupID } func (m *mockEntity) GetGroupID() int32 { return m.groupID }
func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID } func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID }
func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 } func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 }
// Mock zone implementation // Mock zone implementation
type mockZone struct { type mockZone struct {
@ -521,8 +521,8 @@ func TestGroupManagerInvitations(t *testing.T) {
MaxGroups: 1000, MaxGroups: 1000,
MaxRaidGroups: 4, MaxRaidGroups: 4,
InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing
UpdateInterval: 0, // Disable background updates for testing UpdateInterval: 0, // Disable background updates for testing
BuffUpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing
EnableCrossServer: true, EnableCrossServer: true,
EnableRaids: true, EnableRaids: true,
EnableQuestSharing: true, EnableQuestSharing: true,
@ -861,3 +861,362 @@ func BenchmarkGroupOperations(b *testing.B) {
}) })
}) })
} }
// TestMasterListCreation tests master list creation
func TestMasterListCreation(t *testing.T) {
masterList := NewMasterList()
if masterList == nil {
t.Fatal("NewMasterList returned nil")
}
if masterList.GetGroupCount() != 0 {
t.Errorf("Expected count 0, got %d", masterList.GetGroupCount())
}
if !masterList.IsEmpty() {
t.Error("New master list should be empty")
}
}
// TestMasterListBasicOperations tests basic operations
func TestMasterListBasicOperations(t *testing.T) {
masterList := NewMasterList()
// Create test groups
group1 := NewGroup(1001, nil, nil)
group2 := NewGroup(1002, nil, nil)
// Add members to create different characteristics
leader1 := createMockEntity(1, "Leader1", true)
member1 := createMockEntity(2, "Member1", true)
group1.AddMember(leader1, true)
group1.AddMember(member1, false)
leader2 := createMockEntity(3, "Leader2", true)
group2.AddMember(leader2, true)
// Test adding
if !masterList.AddGroup(group1) {
t.Error("Should successfully add group1")
}
if !masterList.AddGroup(group2) {
t.Error("Should successfully add group2")
}
// Test duplicate add (should fail)
if masterList.AddGroup(group1) {
t.Error("Should not add duplicate group")
}
if masterList.GetGroupCount() != 2 {
t.Errorf("Expected count 2, got %d", masterList.GetGroupCount())
}
// Test retrieving
retrieved := masterList.GetGroup(1001)
if retrieved == nil {
t.Error("Should retrieve added group")
}
if retrieved.GetID() != 1001 {
t.Errorf("Expected ID 1001, got %d", retrieved.GetID())
}
// Test safe retrieval
retrieved, exists := masterList.GetGroupSafe(1001)
if !exists || retrieved == nil {
t.Error("GetGroupSafe should return group and true")
}
_, exists = masterList.GetGroupSafe(9999)
if exists {
t.Error("GetGroupSafe should return false for non-existent ID")
}
// Test HasGroup
if !masterList.HasGroup(1001) {
t.Error("HasGroup should return true for existing ID")
}
if masterList.HasGroup(9999) {
t.Error("HasGroup should return false for non-existent ID")
}
// Test removing
if !masterList.RemoveGroup(1001) {
t.Error("Should successfully remove group")
}
if masterList.GetGroupCount() != 1 {
t.Errorf("Expected count 1, got %d", masterList.GetGroupCount())
}
if masterList.HasGroup(1001) {
t.Error("Group should be removed")
}
// Test clear
masterList.Clear()
if masterList.GetGroupCount() != 0 {
t.Errorf("Expected count 0 after clear, got %d", masterList.GetGroupCount())
}
// Cleanup
group1.Disband()
group2.Disband()
}
// TestMasterListFeatures tests domain-specific features
func TestMasterListFeatures(t *testing.T) {
masterList := NewMasterList()
// Create groups with different characteristics
group1 := NewGroup(101, nil, nil)
group2 := NewGroup(102, nil, nil)
group3 := NewGroup(103, nil, nil)
group4 := NewGroup(104, nil, nil)
// Group 1: 2 members in zone 220
leader1 := createMockEntity(1, "Leader1", true)
member1 := createMockEntity(2, "Member1", true)
leader1.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"}
member1.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"}
group1.AddMember(leader1, true)
group1.AddMember(member1, false)
// Group 2: 1 member (solo) in zone 221
leader2 := createMockEntity(3, "Leader2", true)
leader2.zone = &mockZone{zoneID: 221, instanceID: 1, zoneName: "antonica"}
group2.AddMember(leader2, true)
// Group 3: Full group (6 members) in zone 220, make it a raid
leader3 := createMockEntity(4, "Leader3", true)
leader3.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"}
group3.AddMember(leader3, true)
for i := 1; i < MAX_GROUP_SIZE; i++ {
member := createMockEntity(int32(10+i), fmt.Sprintf("RaidMember%d", i), true)
member.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"}
group3.AddMember(member, false)
}
group3.ReplaceRaidGroups([]int32{103, 104})
// Group 4: Disbanded group
leader4 := createMockEntity(20, "Leader4", true)
group4.AddMember(leader4, true)
group4.Disband()
// Add groups to master list
masterList.AddGroup(group1)
masterList.AddGroup(group2)
masterList.AddGroup(group3)
masterList.AddGroup(group4)
// Test GetGroupByMember
found := masterList.GetGroupByMember("member1")
if found == nil || found.GetID() != 101 {
t.Error("GetGroupByMember should find group containing Member1")
}
found = masterList.GetGroupByMember("LEADER2")
if found == nil || found.GetID() != 102 {
t.Error("GetGroupByMember should find group containing Leader2 (case insensitive)")
}
found = masterList.GetGroupByMember("NonExistent")
if found != nil {
t.Error("GetGroupByMember should return nil for non-existent member")
}
// Test GetGroupByLeader
found = masterList.GetGroupByLeader("leader1")
if found == nil || found.GetID() != 101 {
t.Error("GetGroupByLeader should find group led by Leader1")
}
found = masterList.GetGroupByLeader("LEADER3")
if found == nil || found.GetID() != 103 {
t.Error("GetGroupByLeader should find group led by Leader3 (case insensitive)")
}
// Test GetGroupsBySize
soloGroups := masterList.GetGroupsBySize(1)
if len(soloGroups) != 1 {
t.Errorf("GetGroupsBySize(1) returned %v results, want 1", len(soloGroups))
}
twoMemberGroups := masterList.GetGroupsBySize(2)
if len(twoMemberGroups) != 1 {
t.Errorf("GetGroupsBySize(2) returned %v results, want 1", len(twoMemberGroups))
}
fullGroups := masterList.GetGroupsBySize(MAX_GROUP_SIZE)
if len(fullGroups) != 1 {
t.Errorf("GetGroupsBySize(%d) returned %v results, want 1", MAX_GROUP_SIZE, len(fullGroups))
}
// Test GetGroupsByZone
zone220Groups := masterList.GetGroupsByZone(220)
if len(zone220Groups) != 2 { // group1 and group3
t.Errorf("GetGroupsByZone(220) returned %v results, want 2", len(zone220Groups))
}
zone221Groups := masterList.GetGroupsByZone(221)
if len(zone221Groups) != 1 { // group2
t.Errorf("GetGroupsByZone(221) returned %v results, want 1", len(zone221Groups))
}
// Test GetActiveGroups
activeGroups := masterList.GetActiveGroups()
if len(activeGroups) != 3 { // group1, group2, group3 (group4 is disbanded)
t.Errorf("GetActiveGroups() returned %v results, want 3", len(activeGroups))
}
// Test GetRaidGroups
raidGroups := masterList.GetRaidGroups()
if len(raidGroups) != 1 { // group3
t.Errorf("GetRaidGroups() returned %v results, want 1", len(raidGroups))
}
// Test GetSoloGroups
soloGroups = masterList.GetSoloGroups()
if len(soloGroups) != 1 { // group2
t.Errorf("GetSoloGroups() returned %v results, want 1", len(soloGroups))
}
// Test GetFullGroups
fullGroups = masterList.GetFullGroups()
if len(fullGroups) != 1 { // group3
t.Errorf("GetFullGroups() returned %v results, want 1", len(fullGroups))
}
// Test GetZones
zones := masterList.GetZones()
if len(zones) < 2 {
t.Errorf("GetZones() returned %v zones, want at least 2", len(zones))
}
// Test GetSizes
sizes := masterList.GetSizes()
if len(sizes) < 3 {
t.Errorf("GetSizes() returned %v sizes, want at least 3", len(sizes))
}
// Test GetTotalMembers
totalMembers := masterList.GetTotalMembers()
expectedTotal := int32(2 + 1 + MAX_GROUP_SIZE) // group1 + group2 + group3 (group4 is disbanded)
if totalMembers != expectedTotal {
t.Errorf("GetTotalMembers() returned %v, want %v", totalMembers, expectedTotal)
}
// Test UpdateGroup
group1.AddMember(createMockEntity(30, "NewMember", true), false)
err := masterList.UpdateGroup(group1)
if err != nil {
t.Errorf("UpdateGroup failed: %v", err)
}
// Test updating non-existent group
nonExistentGroup := NewGroup(9999, nil, nil)
err = masterList.UpdateGroup(nonExistentGroup)
if err == nil {
t.Error("UpdateGroup should fail for non-existent group")
}
nonExistentGroup.Disband()
// Test GetGroupStatistics
stats := masterList.GetGroupStatistics()
if stats.TotalGroups != 4 {
t.Errorf("Statistics TotalGroups = %v, want 4", stats.TotalGroups)
}
if stats.ActiveGroups != 3 {
t.Errorf("Statistics ActiveGroups = %v, want 3", stats.ActiveGroups)
}
if stats.RaidGroups != 1 {
t.Errorf("Statistics RaidGroups = %v, want 1", stats.RaidGroups)
}
if stats.SoloGroups != 1 {
t.Errorf("Statistics SoloGroups = %v, want 1", stats.SoloGroups)
}
if stats.FullGroups != 1 {
t.Errorf("Statistics FullGroups = %v, want 1", stats.FullGroups)
}
// Test Cleanup
removedCount := masterList.Cleanup()
if removedCount != 1 { // Should remove group4
t.Errorf("Cleanup() removed %v groups, want 1", removedCount)
}
if masterList.GetGroupCount() != 3 {
t.Errorf("Group count after cleanup = %v, want 3", masterList.GetGroupCount())
}
// Cleanup
group1.Disband()
group2.Disband()
group3.Disband()
}
// TestMasterListConcurrency tests concurrent access
func TestMasterListConcurrency(t *testing.T) {
masterList := NewMasterList()
// Add initial groups
for i := 1; i <= 50; i++ {
group := NewGroup(int32(i+100), nil, nil)
leader := createMockEntity(int32(i), fmt.Sprintf("Leader%d", i), true)
group.AddMember(leader, true)
masterList.AddGroup(group)
}
// Test concurrent access
done := make(chan bool, 10)
// Concurrent readers
for i := 0; i < 5; i++ {
go func() {
defer func() { done <- true }()
for j := 0; j < 100; j++ {
masterList.GetGroup(int32(j%50 + 101))
masterList.GetActiveGroups()
masterList.GetGroupByMember(fmt.Sprintf("leader%d", j%50+1))
masterList.GetGroupsBySize(1)
masterList.GetZones()
}
}()
}
// Concurrent writers
for i := 0; i < 5; i++ {
go func(workerID int) {
defer func() { done <- true }()
for j := 0; j < 10; j++ {
groupID := int32(workerID*1000 + j + 1000)
group := NewGroup(groupID, nil, nil)
leader := createMockEntity(int32(workerID*1000+j), fmt.Sprintf("Worker%d-Leader%d", workerID, j), true)
group.AddMember(leader, true)
masterList.AddGroup(group) // Some may fail due to concurrent additions
}
}(i)
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
// Verify final state - should have at least 50 initial groups
finalCount := masterList.GetGroupCount()
if finalCount < 50 {
t.Errorf("Expected at least 50 groups after concurrent operations, got %d", finalCount)
}
if finalCount > 100 {
t.Errorf("Expected at most 100 groups after concurrent operations, got %d", finalCount)
}
// Cleanup
masterList.ForEach(func(id int32, group *Group) {
group.Disband()
})
}

View File

@ -9,13 +9,13 @@ import (
// Manager provides group management with embedded database operations // Manager provides group management with embedded database operations
type Manager struct { type Manager struct {
// Core fields with embedded database operations // Core fields with embedded database operations
MasterList *MasterList `json:"master_list"` MasterList *MasterList `json:"master_list"`
Config GroupManagerConfig `json:"config"` Config GroupManagerConfig `json:"config"`
Stats GroupManagerStats `json:"stats"` Stats GroupManagerStats `json:"stats"`
// Group ID generation // Group ID generation
nextGroupID int32 `json:"-" db:"next_group_id"` nextGroupID int32 `json:"-" db:"next_group_id"`
nextGroupIDMutex sync.Mutex `json:"-"` nextGroupIDMutex sync.Mutex `json:"-"`
// Pending invitations // Pending invitations
PendingInvites map[string]*GroupInvite `json:"pending_invites"` PendingInvites map[string]*GroupInvite `json:"pending_invites"`
@ -30,7 +30,7 @@ type Manager struct {
statsMutex sync.RWMutex `json:"-"` statsMutex sync.RWMutex `json:"-"`
// Background processing // Background processing
stopChan chan struct{} `json:"-"` stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"` wg sync.WaitGroup `json:"-"`
// Integration interfaces // Integration interfaces

View File

@ -2,179 +2,671 @@ package groups
import ( import (
"fmt" "fmt"
"maps"
"eq2emu/internal/common" "strings"
"sync"
"time"
) )
// MasterList manages all groups using generic MasterList pattern // MasterList manages groups with optimized lookups for:
// - Fast ID-based lookups (O(1))
// - Fast member-based lookups (indexed)
// - Fast zone-based filtering (indexed)
// - Fast size-based filtering (indexed)
// - Raid group management and lookups
// - Leader-based searching
// - Activity tracking and cleanup
type MasterList struct { type MasterList struct {
*common.MasterList[int32, *Group] // Core storage
groups map[int32]*Group // ID -> Group
mutex sync.RWMutex
// Indices for O(1) lookups
byMember map[string]*Group // Member name -> group containing that member
byLeader map[string]*Group // Leader name -> group
byZone map[int32][]*Group // Zone ID -> groups with members in that zone
bySize map[int32][]*Group // Size -> groups of that size
activeGroups map[int32]*Group // Active (non-disbanded) groups
raidGroups map[int32]*Group // Groups that are part of raids
soloGroups map[int32]*Group // Single-member groups
fullGroups map[int32]*Group // Full groups (size = MAX_GROUP_SIZE)
// Activity tracking
byLastActivity map[time.Time][]*Group // Last activity time -> groups
// Cached metadata and slices
totalMembers int32 // Total active members across all groups
zones []int32 // Unique zones with group members
sizes []int32 // Unique group sizes
zoneStats map[int32]int // Zone ID -> group count
sizeStats map[int32]int // Size -> group count
allGroupsSlice []*Group // Cached slice of all groups
activeGroupsSlice []*Group // Cached slice of active groups
metaStale bool // Whether metadata cache needs refresh
} }
// NewMasterList creates a new master list for groups // NewMasterList creates a new group master list
func NewMasterList() *MasterList { func NewMasterList() *MasterList {
return &MasterList{ return &MasterList{
MasterList: common.NewMasterList[int32, *Group](), groups: make(map[int32]*Group),
byMember: make(map[string]*Group),
byLeader: make(map[string]*Group),
byZone: make(map[int32][]*Group),
bySize: make(map[int32][]*Group),
activeGroups: make(map[int32]*Group),
raidGroups: make(map[int32]*Group),
soloGroups: make(map[int32]*Group),
fullGroups: make(map[int32]*Group),
byLastActivity: make(map[time.Time][]*Group),
zoneStats: make(map[int32]int),
sizeStats: make(map[int32]int),
allGroupsSlice: make([]*Group, 0),
activeGroupsSlice: make([]*Group, 0),
metaStale: true,
} }
} }
// AddGroup adds a group to the master list // refreshMetaCache updates the cached metadata
func (ml *MasterList) refreshMetaCache() {
if !ml.metaStale {
return
}
// Clear and rebuild zone and size stats
ml.zoneStats = make(map[int32]int)
ml.sizeStats = make(map[int32]int)
zoneSet := make(map[int32]struct{})
sizeSet := make(map[int32]struct{})
ml.totalMembers = 0
// Collect unique values and stats
for _, group := range ml.activeGroups {
size := group.GetSize()
ml.sizeStats[size]++
sizeSet[size] = struct{}{}
ml.totalMembers += size
// Collect zones from group members
members := group.GetMembers()
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
ml.zoneStats[zoneID]++
zoneSet[zoneID] = struct{}{}
}
}
// Clear and rebuild cached slices
ml.zones = ml.zones[:0]
for zoneID := range zoneSet {
ml.zones = append(ml.zones, zoneID)
}
ml.sizes = ml.sizes[:0]
for size := range sizeSet {
ml.sizes = append(ml.sizes, size)
}
// Rebuild all groups slice
ml.allGroupsSlice = ml.allGroupsSlice[:0]
if cap(ml.allGroupsSlice) < len(ml.groups) {
ml.allGroupsSlice = make([]*Group, 0, len(ml.groups))
}
for _, group := range ml.groups {
ml.allGroupsSlice = append(ml.allGroupsSlice, group)
}
// Rebuild active groups slice
ml.activeGroupsSlice = ml.activeGroupsSlice[:0]
if cap(ml.activeGroupsSlice) < len(ml.activeGroups) {
ml.activeGroupsSlice = make([]*Group, 0, len(ml.activeGroups))
}
for _, group := range ml.activeGroups {
ml.activeGroupsSlice = append(ml.activeGroupsSlice, group)
}
ml.metaStale = false
}
// updateGroupIndices updates all indices for a group
func (ml *MasterList) updateGroupIndices(group *Group, add bool) {
groupID := group.GetID()
size := group.GetSize()
leaderName := group.GetLeaderName()
isRaid := group.IsGroupRaid()
isDisbanded := group.IsDisbanded()
members := group.GetMembers()
if add {
// Add to size index
ml.bySize[size] = append(ml.bySize[size], group)
// Add to leader index
if leaderName != "" {
ml.byLeader[strings.ToLower(leaderName)] = group
}
// Add to member index
for _, member := range members {
if member.Name != "" {
ml.byMember[strings.ToLower(member.Name)] = group
}
}
// Add to zone index
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
ml.byZone[zoneID] = append(ml.byZone[zoneID], group)
}
// Add to specialized indices
if !isDisbanded {
ml.activeGroups[groupID] = group
}
if isRaid {
ml.raidGroups[groupID] = group
}
if size == 1 {
ml.soloGroups[groupID] = group
}
if size == MAX_GROUP_SIZE {
ml.fullGroups[groupID] = group
}
// Add to activity index
activity := group.GetLastActivity()
ml.byLastActivity[activity] = append(ml.byLastActivity[activity], group)
} else {
// Remove from size index
sizeGroups := ml.bySize[size]
for i, g := range sizeGroups {
if g.GetID() == groupID {
ml.bySize[size] = append(sizeGroups[:i], sizeGroups[i+1:]...)
break
}
}
// Remove from leader index
if leaderName != "" {
delete(ml.byLeader, strings.ToLower(leaderName))
}
// Remove from member index
for _, member := range members {
if member.Name != "" {
delete(ml.byMember, strings.ToLower(member.Name))
}
}
// Remove from zone index
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
zoneGroups := ml.byZone[zoneID]
for i, g := range zoneGroups {
if g.GetID() == groupID {
ml.byZone[zoneID] = append(zoneGroups[:i], zoneGroups[i+1:]...)
break
}
}
}
// Remove from specialized indices
delete(ml.activeGroups, groupID)
delete(ml.raidGroups, groupID)
delete(ml.soloGroups, groupID)
delete(ml.fullGroups, groupID)
// Remove from activity index
activity := group.GetLastActivity()
activityGroups := ml.byLastActivity[activity]
for i, g := range activityGroups {
if g.GetID() == groupID {
ml.byLastActivity[activity] = append(activityGroups[:i], activityGroups[i+1:]...)
break
}
}
}
}
// AddGroup adds a group with full indexing
func (ml *MasterList) AddGroup(group *Group) bool { func (ml *MasterList) AddGroup(group *Group) bool {
return ml.MasterList.Add(group) if group == nil {
return false
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
if _, exists := ml.groups[group.GetID()]; exists {
return false
}
// Add to core storage
ml.groups[group.GetID()] = group
// Update all indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
return true
} }
// GetGroup retrieves a group by ID // GetGroup retrieves by ID (O(1))
func (ml *MasterList) GetGroup(groupID int32) *Group { func (ml *MasterList) GetGroup(groupID int32) *Group {
return ml.MasterList.Get(groupID) ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.groups[groupID]
} }
// RemoveGroup removes a group by ID // GetGroupSafe retrieves a group by ID with existence check
func (ml *MasterList) GetGroupSafe(groupID int32) (*Group, bool) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
group, exists := ml.groups[groupID]
return group, exists
}
// HasGroup checks if a group exists by ID
func (ml *MasterList) HasGroup(groupID int32) bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
_, exists := ml.groups[groupID]
return exists
}
// RemoveGroup removes a group and updates all indices
func (ml *MasterList) RemoveGroup(groupID int32) bool { func (ml *MasterList) RemoveGroup(groupID int32) bool {
return ml.MasterList.Remove(groupID) ml.mutex.Lock()
defer ml.mutex.Unlock()
group, exists := ml.groups[groupID]
if !exists {
return false
}
// Remove from core storage
delete(ml.groups, groupID)
// Update all indices
ml.updateGroupIndices(group, false)
// Invalidate metadata cache
ml.metaStale = true
return true
} }
// GetAllGroups returns all groups // GetAllGroups returns all groups as a slice
func (ml *MasterList) GetAllGroups() []*Group { func (ml *MasterList) GetAllGroups() []*Group {
return ml.MasterList.GetAllSlice() ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]*Group, len(ml.allGroupsSlice))
copy(result, ml.allGroupsSlice)
return result
}
// GetAllGroupsMap returns a copy of all groups map
func (ml *MasterList) GetAllGroupsMap() map[int32]*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Group, len(ml.groups))
maps.Copy(result, ml.groups)
return result
}
// GetGroupCount returns the total number of groups
func (ml *MasterList) GetGroupCount() int32 {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return int32(len(ml.groups))
}
// Size returns the total number of groups
func (ml *MasterList) Size() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.groups)
}
// IsEmpty returns true if the master list is empty
func (ml *MasterList) IsEmpty() bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.groups) == 0
}
// Clear removes all groups from the list
func (ml *MasterList) Clear() {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Clear all maps
ml.groups = make(map[int32]*Group)
ml.byMember = make(map[string]*Group)
ml.byLeader = make(map[string]*Group)
ml.byZone = make(map[int32][]*Group)
ml.bySize = make(map[int32][]*Group)
ml.activeGroups = make(map[int32]*Group)
ml.raidGroups = make(map[int32]*Group)
ml.soloGroups = make(map[int32]*Group)
ml.fullGroups = make(map[int32]*Group)
ml.byLastActivity = make(map[time.Time][]*Group)
// Clear cached metadata
ml.zones = ml.zones[:0]
ml.sizes = ml.sizes[:0]
ml.allGroupsSlice = ml.allGroupsSlice[:0]
ml.activeGroupsSlice = ml.activeGroupsSlice[:0]
ml.zoneStats = make(map[int32]int)
ml.sizeStats = make(map[int32]int)
ml.totalMembers = 0
ml.metaStale = true
} }
// GetGroupsByFilter returns groups matching the filter function // GetGroupsByFilter returns groups matching the filter function
func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group { func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group {
return ml.MasterList.Filter(filter) ml.mutex.RLock()
} defer ml.mutex.RUnlock()
// GetActiveGroups returns all non-disbanded groups var result []*Group
func (ml *MasterList) GetActiveGroups() []*Group { for _, group := range ml.groups {
return ml.GetGroupsByFilter(func(group *Group) bool { if filter(group) {
return !group.IsDisbanded() result = append(result, group)
})
}
// GetGroupsByZone returns groups with members in the specified zone
func (ml *MasterList) GetGroupsByZone(zoneID int32) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool {
members := group.GetMembers()
for _, member := range members {
if member.ZoneID == zoneID {
return true
}
} }
return false }
}) return result
} }
// GetGroupsBySize returns groups of the specified size // GetGroupByMember returns the group containing the specified member (O(1))
func (ml *MasterList) GetGroupByMember(memberName string) *Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byMember[strings.ToLower(memberName)]
}
// GetGroupByLeader returns the group led by the specified leader (O(1))
func (ml *MasterList) GetGroupByLeader(leaderName string) *Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byLeader[strings.ToLower(leaderName)]
}
// GetActiveGroups returns all non-disbanded groups (O(1))
func (ml *MasterList) GetActiveGroups() []*Group {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]*Group, len(ml.activeGroupsSlice))
copy(result, ml.activeGroupsSlice)
return result
}
// GetGroupsByZone returns groups with members in the specified zone (O(1))
func (ml *MasterList) GetGroupsByZone(zoneID int32) []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byZone[zoneID]
}
// GetGroupsBySize returns groups of the specified size (O(1))
func (ml *MasterList) GetGroupsBySize(size int32) []*Group { func (ml *MasterList) GetGroupsBySize(size int32) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool { ml.mutex.RLock()
return group.GetSize() == size defer ml.mutex.RUnlock()
}) return ml.bySize[size]
} }
// GetRaidGroups returns all groups that are part of raids // GetRaidGroups returns all groups that are part of raids (O(1))
func (ml *MasterList) GetRaidGroups() []*Group { func (ml *MasterList) GetRaidGroups() []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool { ml.mutex.RLock()
return group.IsGroupRaid() defer ml.mutex.RUnlock()
})
result := make([]*Group, 0, len(ml.raidGroups))
for _, group := range ml.raidGroups {
result = append(result, group)
}
return result
} }
// GetSoloGroups returns all groups with only one member // GetSoloGroups returns all groups with only one member (O(1))
func (ml *MasterList) GetSoloGroups() []*Group { func (ml *MasterList) GetSoloGroups() []*Group {
return ml.GetGroupsBySize(1) ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Group, 0, len(ml.soloGroups))
for _, group := range ml.soloGroups {
result = append(result, group)
}
return result
} }
// GetFullGroups returns all groups at maximum capacity // GetFullGroups returns all groups at maximum capacity (O(1))
func (ml *MasterList) GetFullGroups() []*Group { func (ml *MasterList) GetFullGroups() []*Group {
return ml.GetGroupsBySize(MAX_GROUP_SIZE) ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Group, 0, len(ml.fullGroups))
for _, group := range ml.fullGroups {
result = append(result, group)
}
return result
} }
// GetGroupsByLeader returns groups led by entities with the specified name // GetGroupsByLeader returns groups led by entities with the specified name
func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group { func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool { group := ml.GetGroupByLeader(leaderName)
return group.GetLeaderName() == leaderName if group != nil {
}) return []*Group{group}
}
return []*Group{}
} }
// GetGroupsByMember returns groups containing a member with the specified name // GetGroupsByMember returns groups containing a member with the specified name
func (ml *MasterList) GetGroupsByMember(memberName string) []*Group { func (ml *MasterList) GetGroupsByMember(memberName string) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool { group := ml.GetGroupByMember(memberName)
members := group.GetMembers() if group != nil {
for _, member := range members { return []*Group{group}
if member.Name == memberName { }
return true return []*Group{}
}
}
return false
})
} }
// GetGroupStatistics returns statistics about the groups in the master list // GetZones returns all unique zones with group members using cached results
func (ml *MasterList) GetZones() []int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]int32, len(ml.zones))
copy(result, ml.zones)
return result
}
// GetSizes returns all unique group sizes using cached results
func (ml *MasterList) GetSizes() []int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]int32, len(ml.sizes))
copy(result, ml.sizes)
return result
}
// GetTotalMembers returns the total number of active members across all groups
func (ml *MasterList) GetTotalMembers() int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
return ml.totalMembers
}
// GetGroupStatistics returns statistics about the groups in the master list using cached data
func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats { func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats {
allGroups := ml.GetAllGroups() ml.mutex.Lock() // Need write lock to potentially update cache
activeGroups := ml.GetActiveGroups() defer ml.mutex.Unlock()
raidGroups := ml.GetRaidGroups()
ml.refreshMetaCache()
var totalMembers int32
var totalRaidMembers int32 var totalRaidMembers int32
for _, group := range ml.raidGroups {
for _, group := range activeGroups { totalRaidMembers += group.GetSize()
totalMembers += group.GetSize()
if group.IsGroupRaid() {
totalRaidMembers += group.GetSize()
}
} }
var averageGroupSize float64 var averageGroupSize float64
if len(activeGroups) > 0 { if len(ml.activeGroups) > 0 {
averageGroupSize = float64(totalMembers) / float64(len(activeGroups)) averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups))
} }
return &GroupMasterListStats{ return &GroupMasterListStats{
TotalGroups: int32(len(allGroups)), TotalGroups: int32(len(ml.groups)),
ActiveGroups: int32(len(activeGroups)), ActiveGroups: int32(len(ml.activeGroups)),
RaidGroups: int32(len(raidGroups)), RaidGroups: int32(len(ml.raidGroups)),
TotalMembers: totalMembers, TotalMembers: ml.totalMembers,
TotalRaidMembers: totalRaidMembers, TotalRaidMembers: totalRaidMembers,
AverageGroupSize: averageGroupSize, AverageGroupSize: averageGroupSize,
SoloGroups: int32(len(ml.GetSoloGroups())), SoloGroups: int32(len(ml.soloGroups)),
FullGroups: int32(len(ml.GetFullGroups())), FullGroups: int32(len(ml.fullGroups)),
} }
} }
// GroupMasterListStats holds statistics about the groups master list // GroupMasterListStats holds statistics about the groups master list
type GroupMasterListStats struct { type GroupMasterListStats struct {
TotalGroups int32 `json:"total_groups"` TotalGroups int32 `json:"total_groups"`
ActiveGroups int32 `json:"active_groups"` ActiveGroups int32 `json:"active_groups"`
RaidGroups int32 `json:"raid_groups"` RaidGroups int32 `json:"raid_groups"`
TotalMembers int32 `json:"total_members"` TotalMembers int32 `json:"total_members"`
TotalRaidMembers int32 `json:"total_raid_members"` TotalRaidMembers int32 `json:"total_raid_members"`
AverageGroupSize float64 `json:"average_group_size"` AverageGroupSize float64 `json:"average_group_size"`
SoloGroups int32 `json:"solo_groups"` SoloGroups int32 `json:"solo_groups"`
FullGroups int32 `json:"full_groups"` FullGroups int32 `json:"full_groups"`
}
// RefreshGroupIndices refreshes indices for a group (used when group state changes)
func (ml *MasterList) RefreshGroupIndices(group *Group) {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Remove from old indices
ml.updateGroupIndices(group, false)
// Add to new indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
}
// UpdateGroup updates an existing group and refreshes indices
func (ml *MasterList) UpdateGroup(group *Group) error {
if group == nil {
return fmt.Errorf("group cannot be nil")
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
old, exists := ml.groups[group.GetID()]
if !exists {
return fmt.Errorf("group %d not found", group.GetID())
}
// Remove old group from indices (but not core storage yet)
ml.updateGroupIndices(old, false)
// Update core storage
ml.groups[group.GetID()] = group
// Add new group to indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
return nil
}
// ForEach executes a function for each group
func (ml *MasterList) ForEach(fn func(int32, *Group)) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
for id, group := range ml.groups {
fn(id, group)
}
} }
// Cleanup removes disbanded groups from the master list // Cleanup removes disbanded groups from the master list
func (ml *MasterList) Cleanup() int32 { func (ml *MasterList) Cleanup() int32 {
disbandedGroups := ml.GetGroupsByFilter(func(group *Group) bool { ml.mutex.Lock()
return group.IsDisbanded() defer ml.mutex.Unlock()
})
removed := int32(0) removed := int32(0)
for _, group := range disbandedGroups { for id, group := range ml.groups {
if ml.RemoveGroup(group.GetID()) { if group.IsDisbanded() {
// Remove from core storage
delete(ml.groups, id)
// Remove from indices (group is already disbanded, so activeGroups won't include it)
ml.updateGroupIndices(group, false)
removed++ removed++
} }
} }
if removed > 0 {
// Invalidate metadata cache
ml.metaStale = true
}
return removed return removed
} }
// ValidateAll validates all groups in the master list // ValidateAll validates all groups in the master list
func (ml *MasterList) ValidateAll() []error { func (ml *MasterList) ValidateAll() []error {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var errors []error var errors []error
allGroups := ml.GetAllGroups() for id, group := range ml.groups {
for _, group := range allGroups { if group == nil {
errors = append(errors, fmt.Errorf("group ID %d is nil", id))
continue
}
// Check for basic validity // Check for basic validity
if group.GetID() != id {
errors = append(errors, fmt.Errorf("group ID mismatch: map key %d != group ID %d", id, group.GetID()))
}
if group.GetID() <= 0 { if group.GetID() <= 0 {
errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID())) errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID()))
} }
@ -207,7 +699,38 @@ func (ml *MasterList) ValidateAll() []error {
if leaderCount > 1 { if leaderCount > 1 {
errors = append(errors, fmt.Errorf("group %d has multiple leaders (%d)", group.GetID(), leaderCount)) errors = append(errors, fmt.Errorf("group %d has multiple leaders (%d)", group.GetID(), leaderCount))
} }
// Validate index consistency
if !group.IsDisbanded() {
if _, exists := ml.activeGroups[id]; !exists {
errors = append(errors, fmt.Errorf("active group %d not found in activeGroups index", id))
}
}
if group.IsGroupRaid() {
if _, exists := ml.raidGroups[id]; !exists {
errors = append(errors, fmt.Errorf("raid group %d not found in raidGroups index", id))
}
}
if group.GetSize() == 1 {
if _, exists := ml.soloGroups[id]; !exists {
errors = append(errors, fmt.Errorf("solo group %d not found in soloGroups index", id))
}
}
if group.GetSize() == MAX_GROUP_SIZE {
if _, exists := ml.fullGroups[id]; !exists {
errors = append(errors, fmt.Errorf("full group %d not found in fullGroups index", id))
}
}
} }
return errors return errors
} }
// IsValid returns true if all groups are valid
func (ml *MasterList) IsValid() bool {
errors := ml.ValidateAll()
return len(errors) == 0
}