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
type Group struct {
// Core fields
GroupID int32 `json:"group_id" db:"group_id"`
Options GroupOptions `json:"options"`
Members []*GroupMemberInfo `json:"members"`
RaidGroups []int32 `json:"raid_groups"`
CreatedTime time.Time `json:"created_time" db:"created_time"`
LastActivity time.Time `json:"last_activity" db:"last_activity"`
Disbanded bool `json:"disbanded" db:"disbanded"`
GroupID int32 `json:"group_id" db:"group_id"`
Options GroupOptions `json:"options"`
Members []*GroupMemberInfo `json:"members"`
RaidGroups []int32 `json:"raid_groups"`
CreatedTime time.Time `json:"created_time" db:"created_time"`
LastActivity time.Time `json:"last_activity" db:"last_activity"`
Disbanded bool `json:"disbanded" db:"disbanded"`
// Internal fields
membersMutex sync.RWMutex `json:"-"`
@ -29,7 +29,7 @@ type Group struct {
updateQueue chan *GroupUpdate `json:"-"`
// Background processing
stopChan chan struct{} `json:"-"`
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
// Database integration - embedded operations

View File

@ -9,46 +9,46 @@ import (
// Mock entity implementation for testing
type mockEntity struct {
id int32
name string
level int8
class int8
race int8
hp int32
maxHP int32
power int32
maxPower int32
isPlayer bool
isNPC bool
isBot bool
isDead bool
zone *mockZone
groupID int32
groupInfo *GroupMemberInfo
id int32
name string
level int8
class int8
race int8
hp int32
maxHP int32
power int32
maxPower int32
isPlayer bool
isNPC bool
isBot bool
isDead bool
zone *mockZone
groupID int32
groupInfo *GroupMemberInfo
}
func (m *mockEntity) GetID() int32 { return m.id }
func (m *mockEntity) GetName() string { return m.name }
func (m *mockEntity) GetLevel() int8 { return m.level }
func (m *mockEntity) GetClass() int8 { return m.class }
func (m *mockEntity) GetRace() int8 { return m.race }
func (m *mockEntity) GetHP() int32 { return m.hp }
func (m *mockEntity) GetTotalHP() int32 { return m.maxHP }
func (m *mockEntity) GetPower() int32 { return m.power }
func (m *mockEntity) GetTotalPower() int32 { return m.maxPower }
func (m *mockEntity) IsPlayer() bool { return m.isPlayer }
func (m *mockEntity) IsNPC() bool { return m.isNPC }
func (m *mockEntity) IsBot() bool { return m.isBot }
func (m *mockEntity) IsDead() bool { return m.isDead }
func (m *mockEntity) GetZone() Zone { return m.zone }
func (m *mockEntity) GetID() int32 { return m.id }
func (m *mockEntity) GetName() string { return m.name }
func (m *mockEntity) GetLevel() int8 { return m.level }
func (m *mockEntity) GetClass() int8 { return m.class }
func (m *mockEntity) GetRace() int8 { return m.race }
func (m *mockEntity) GetHP() int32 { return m.hp }
func (m *mockEntity) GetTotalHP() int32 { return m.maxHP }
func (m *mockEntity) GetPower() int32 { return m.power }
func (m *mockEntity) GetTotalPower() int32 { return m.maxPower }
func (m *mockEntity) IsPlayer() bool { return m.isPlayer }
func (m *mockEntity) IsNPC() bool { return m.isNPC }
func (m *mockEntity) IsBot() bool { return m.isBot }
func (m *mockEntity) IsDead() bool { return m.isDead }
func (m *mockEntity) GetZone() Zone { return m.zone }
func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 }
// 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) GetGroupID() int32 { return m.groupID }
func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID }
func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 }
func (m *mockEntity) GetGroupID() int32 { return m.groupID }
func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID }
func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 }
// Mock zone implementation
type mockZone struct {
@ -521,8 +521,8 @@ func TestGroupManagerInvitations(t *testing.T) {
MaxGroups: 1000,
MaxRaidGroups: 4,
InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing
UpdateInterval: 0, // Disable background updates for testing
BuffUpdateInterval: 0, // Disable background updates for testing
UpdateInterval: 0, // Disable background updates for testing
BuffUpdateInterval: 0, // Disable background updates for testing
EnableCrossServer: true,
EnableRaids: 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
type Manager struct {
// Core fields with embedded database operations
MasterList *MasterList `json:"master_list"`
Config GroupManagerConfig `json:"config"`
Stats GroupManagerStats `json:"stats"`
MasterList *MasterList `json:"master_list"`
Config GroupManagerConfig `json:"config"`
Stats GroupManagerStats `json:"stats"`
// Group ID generation
nextGroupID int32 `json:"-" db:"next_group_id"`
nextGroupIDMutex sync.Mutex `json:"-"`
nextGroupID int32 `json:"-" db:"next_group_id"`
nextGroupIDMutex sync.Mutex `json:"-"`
// Pending invitations
PendingInvites map[string]*GroupInvite `json:"pending_invites"`
@ -30,7 +30,7 @@ type Manager struct {
statsMutex sync.RWMutex `json:"-"`
// Background processing
stopChan chan struct{} `json:"-"`
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
// Integration interfaces

View File

@ -2,179 +2,671 @@ package groups
import (
"fmt"
"eq2emu/internal/common"
"maps"
"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 {
*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 {
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 {
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 {
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 {
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 {
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
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
func (ml *MasterList) GetActiveGroups() []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool {
return !group.IsDisbanded()
})
}
// 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
}
var result []*Group
for _, group := range ml.groups {
if filter(group) {
result = append(result, group)
}
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 {
return ml.GetGroupsByFilter(func(group *Group) bool {
return group.GetSize() == size
})
ml.mutex.RLock()
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 {
return ml.GetGroupsByFilter(func(group *Group) bool {
return group.IsGroupRaid()
})
ml.mutex.RLock()
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 {
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 {
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
func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool {
return group.GetLeaderName() == leaderName
})
group := ml.GetGroupByLeader(leaderName)
if group != nil {
return []*Group{group}
}
return []*Group{}
}
// GetGroupsByMember returns groups containing a member with the specified name
func (ml *MasterList) GetGroupsByMember(memberName string) []*Group {
return ml.GetGroupsByFilter(func(group *Group) bool {
members := group.GetMembers()
for _, member := range members {
if member.Name == memberName {
return true
}
}
return false
})
group := ml.GetGroupByMember(memberName)
if group != nil {
return []*Group{group}
}
return []*Group{}
}
// 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 {
allGroups := ml.GetAllGroups()
activeGroups := ml.GetActiveGroups()
raidGroups := ml.GetRaidGroups()
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
var totalMembers int32
var totalRaidMembers int32
for _, group := range activeGroups {
totalMembers += group.GetSize()
if group.IsGroupRaid() {
totalRaidMembers += group.GetSize()
}
for _, group := range ml.raidGroups {
totalRaidMembers += group.GetSize()
}
var averageGroupSize float64
if len(activeGroups) > 0 {
averageGroupSize = float64(totalMembers) / float64(len(activeGroups))
if len(ml.activeGroups) > 0 {
averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups))
}
return &GroupMasterListStats{
TotalGroups: int32(len(allGroups)),
ActiveGroups: int32(len(activeGroups)),
RaidGroups: int32(len(raidGroups)),
TotalMembers: totalMembers,
TotalRaidMembers: totalRaidMembers,
AverageGroupSize: averageGroupSize,
SoloGroups: int32(len(ml.GetSoloGroups())),
FullGroups: int32(len(ml.GetFullGroups())),
TotalGroups: int32(len(ml.groups)),
ActiveGroups: int32(len(ml.activeGroups)),
RaidGroups: int32(len(ml.raidGroups)),
TotalMembers: ml.totalMembers,
TotalRaidMembers: totalRaidMembers,
AverageGroupSize: averageGroupSize,
SoloGroups: int32(len(ml.soloGroups)),
FullGroups: int32(len(ml.fullGroups)),
}
}
// GroupMasterListStats holds statistics about the groups master list
type GroupMasterListStats struct {
TotalGroups int32 `json:"total_groups"`
ActiveGroups int32 `json:"active_groups"`
RaidGroups int32 `json:"raid_groups"`
TotalMembers int32 `json:"total_members"`
TotalRaidMembers int32 `json:"total_raid_members"`
AverageGroupSize float64 `json:"average_group_size"`
SoloGroups int32 `json:"solo_groups"`
FullGroups int32 `json:"full_groups"`
TotalGroups int32 `json:"total_groups"`
ActiveGroups int32 `json:"active_groups"`
RaidGroups int32 `json:"raid_groups"`
TotalMembers int32 `json:"total_members"`
TotalRaidMembers int32 `json:"total_raid_members"`
AverageGroupSize float64 `json:"average_group_size"`
SoloGroups int32 `json:"solo_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
func (ml *MasterList) Cleanup() int32 {
disbandedGroups := ml.GetGroupsByFilter(func(group *Group) bool {
return group.IsDisbanded()
})
ml.mutex.Lock()
defer ml.mutex.Unlock()
removed := int32(0)
for _, group := range disbandedGroups {
if ml.RemoveGroup(group.GetID()) {
for id, group := range ml.groups {
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++
}
}
if removed > 0 {
// Invalidate metadata cache
ml.metaStale = true
}
return removed
}
// ValidateAll validates all groups in the master list
func (ml *MasterList) ValidateAll() []error {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var errors []error
allGroups := ml.GetAllGroups()
for _, group := range allGroups {
for id, group := range ml.groups {
if group == nil {
errors = append(errors, fmt.Errorf("group ID %d is nil", id))
continue
}
// 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 {
errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID()))
}
@ -207,7 +699,38 @@ func (ml *MasterList) ValidateAll() []error {
if leaderCount > 1 {
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
}
// IsValid returns true if all groups are valid
func (ml *MasterList) IsValid() bool {
errors := ml.ValidateAll()
return len(errors) == 0
}