diff --git a/internal/groups/benchmark_test.go b/internal/groups/benchmark_test.go index 8e84e84..6a6b967 100644 --- a/internal/groups/benchmark_test.go +++ b/internal/groups/benchmark_test.go @@ -36,9 +36,9 @@ func createTestEntity(id int32, name string, isPlayer bool) *mockEntity { // createTestGroup creates a group with test data for benchmarking func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group { b.Helper() - + group := NewGroup(groupID, nil, nil) - + // Add test members for i := 0; i < memberCount; i++ { entity := createTestEntity( @@ -46,14 +46,14 @@ func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group { fmt.Sprintf("Player%d", i+1), true, ) - + isLeader := (i == 0) err := group.AddMember(entity, isLeader) if err != nil { b.Fatalf("Failed to add member to group: %v", err) } } - + return group } @@ -65,7 +65,7 @@ func BenchmarkGroupCreation(b *testing.B) { group.Disband() // Clean up background goroutine } }) - + b.Run("NewGroupWithOptions", func(b *testing.B) { options := DefaultGroupOptions() for i := 0; i < b.N; i++ { @@ -73,7 +73,7 @@ func BenchmarkGroupCreation(b *testing.B) { group.Disband() // Clean up background goroutine } }) - + b.Run("NewGroupParallel", func(b *testing.B) { var idCounter int64 b.RunParallel(func(pb *testing.PB) { @@ -90,7 +90,7 @@ func BenchmarkGroupCreation(b *testing.B) { func BenchmarkGroupMemberOperations(b *testing.B) { group := createTestGroup(b, 1001, 3) defer group.Disband() // Clean up background goroutine - + b.Run("AddMember", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { @@ -101,7 +101,7 @@ func BenchmarkGroupMemberOperations(b *testing.B) { testGroup.Disband() // Clean up background goroutine } }) - + b.Run("GetSize", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -109,13 +109,13 @@ func BenchmarkGroupMemberOperations(b *testing.B) { } }) }) - + b.Run("GetMembers", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = group.GetMembers() } }) - + b.Run("GetLeaderName", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -123,13 +123,13 @@ func BenchmarkGroupMemberOperations(b *testing.B) { } }) }) - + b.Run("UpdateGroupMemberInfo", func(b *testing.B) { members := group.GetMembers() if len(members) == 0 { b.Skip("No members to update") } - + for i := 0; i < b.N; i++ { member := members[i%len(members)] if member.Member != nil { @@ -143,7 +143,7 @@ func BenchmarkGroupMemberOperations(b *testing.B) { func BenchmarkGroupOptions(b *testing.B) { group := createTestGroup(b, 1001, 3) defer group.Disband() // Clean up background goroutine - + b.Run("GetGroupOptions", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -151,7 +151,7 @@ func BenchmarkGroupOptions(b *testing.B) { } }) }) - + b.Run("SetGroupOptions", func(b *testing.B) { options := DefaultGroupOptions() for i := 0; i < b.N; i++ { @@ -159,7 +159,7 @@ func BenchmarkGroupOptions(b *testing.B) { group.SetGroupOptions(&options) } }) - + b.Run("GetLastLooterIndex", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -167,7 +167,7 @@ func BenchmarkGroupOptions(b *testing.B) { } }) }) - + b.Run("SetNextLooterIndex", func(b *testing.B) { for i := 0; i < b.N; i++ { group.SetNextLooterIndex(int8(i % 6)) @@ -179,11 +179,11 @@ func BenchmarkGroupOptions(b *testing.B) { func BenchmarkGroupRaidOperations(b *testing.B) { group := createTestGroup(b, 1001, 6) defer group.Disband() // Clean up background goroutine - + // Setup some raid groups raidGroups := []int32{1001, 1002, 1003, 1004} group.ReplaceRaidGroups(raidGroups) - + b.Run("GetRaidGroups", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -191,7 +191,7 @@ func BenchmarkGroupRaidOperations(b *testing.B) { } }) }) - + b.Run("IsGroupRaid", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -199,14 +199,14 @@ func BenchmarkGroupRaidOperations(b *testing.B) { } }) }) - + b.Run("IsInRaidGroup", func(b *testing.B) { for i := 0; i < b.N; i++ { targetID := int32((i % 4) + 1001) _ = group.IsInRaidGroup(targetID, false) } }) - + b.Run("AddGroupToRaid", func(b *testing.B) { for i := 0; i < b.N; i++ { // Cycle through a limited set of group IDs to avoid infinite growth @@ -214,14 +214,14 @@ func BenchmarkGroupRaidOperations(b *testing.B) { group.AddGroupToRaid(groupID) } }) - + b.Run("ReplaceRaidGroups", func(b *testing.B) { newGroups := []int32{2001, 2002, 2003} for i := 0; i < b.N; i++ { group.ReplaceRaidGroups(newGroups) } }) - + b.Run("ClearGroupRaid", func(b *testing.B) { for i := 0; i < b.N; i++ { group.ClearGroupRaid() @@ -235,25 +235,25 @@ func BenchmarkGroupRaidOperations(b *testing.B) { func BenchmarkGroupMessaging(b *testing.B) { group := createTestGroup(b, 1001, 6) defer group.Disband() // Clean up background goroutine - + b.Run("SimpleGroupMessage", func(b *testing.B) { for i := 0; i < b.N; i++ { group.SimpleGroupMessage(fmt.Sprintf("Benchmark message %d", i)) } }) - + b.Run("SendGroupMessage", func(b *testing.B) { for i := 0; i < b.N; i++ { group.SendGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, fmt.Sprintf("System message %d", i)) } }) - + b.Run("GroupChatMessageFromName", func(b *testing.B) { for i := 0; i < b.N; i++ { group.GroupChatMessageFromName( - fmt.Sprintf("Player%d", i%6+1), - 0, - fmt.Sprintf("Chat message %d", i), + fmt.Sprintf("Player%d", i%6+1), + 0, + fmt.Sprintf("Chat message %d", i), CHANNEL_GROUP_CHAT, ) } @@ -264,7 +264,7 @@ func BenchmarkGroupMessaging(b *testing.B) { func BenchmarkGroupState(b *testing.B) { group := createTestGroup(b, 1001, 6) defer group.Disband() // Clean up background goroutine - + b.Run("GetID", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -272,7 +272,7 @@ func BenchmarkGroupState(b *testing.B) { } }) }) - + b.Run("IsDisbanded", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -280,7 +280,7 @@ func BenchmarkGroupState(b *testing.B) { } }) }) - + b.Run("GetCreatedTime", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -288,7 +288,7 @@ func BenchmarkGroupState(b *testing.B) { } }) }) - + b.Run("GetLastActivity", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -301,11 +301,11 @@ func BenchmarkGroupState(b *testing.B) { // BenchmarkMasterListOperations measures master list performance func BenchmarkMasterListOperations(b *testing.B) { ml := NewMasterList() - + // Pre-populate with groups const numGroups = 1000 groups := make([]*Group, numGroups) - + b.StopTimer() for i := 0; i < numGroups; i++ { groups[i] = createTestGroup(b, int32(i+1), rand.Intn(6)+1) @@ -320,7 +320,7 @@ func BenchmarkMasterListOperations(b *testing.B) { } }() b.StartTimer() - + b.Run("GetGroup", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -329,7 +329,7 @@ func BenchmarkMasterListOperations(b *testing.B) { } }) }) - + b.Run("AddGroup", func(b *testing.B) { startID := int32(numGroups + 1) var addedGroups []*Group @@ -344,39 +344,39 @@ func BenchmarkMasterListOperations(b *testing.B) { group.Disband() } }) - + b.Run("GetAllGroups", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ml.GetAllGroups() } }) - + b.Run("GetActiveGroups", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ml.GetActiveGroups() } }) - + b.Run("GetGroupsByZone", func(b *testing.B) { for i := 0; i < b.N; i++ { zoneID := int32(rand.Intn(100) + 1) _ = ml.GetGroupsByZone(zoneID) } }) - + b.Run("GetGroupsBySize", func(b *testing.B) { for i := 0; i < b.N; i++ { size := int32(rand.Intn(6) + 1) _ = ml.GetGroupsBySize(size) } }) - + b.Run("GetRaidGroups", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ml.GetRaidGroups() } }) - + b.Run("GetGroupStatistics", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = ml.GetGroupStatistics() @@ -393,9 +393,9 @@ func BenchmarkManagerOperations(b *testing.B) { BuffUpdateInterval: 5 * time.Second, EnableStatistics: true, } - + manager := NewManager(config, nil) - + // Pre-populate with groups b.StopTimer() for i := 0; i < 100; i++ { @@ -403,7 +403,7 @@ func BenchmarkManagerOperations(b *testing.B) { manager.NewGroup(leader, nil, 0) } b.StartTimer() - + b.Run("NewGroup", func(b *testing.B) { startID := int32(1000) for i := 0; i < b.N; i++ { @@ -414,7 +414,7 @@ func BenchmarkManagerOperations(b *testing.B) { } } }) - + b.Run("GetGroup", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -423,7 +423,7 @@ func BenchmarkManagerOperations(b *testing.B) { } }) }) - + b.Run("IsGroupIDValid", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -432,7 +432,7 @@ func BenchmarkManagerOperations(b *testing.B) { } }) }) - + b.Run("GetGroupCount", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -440,13 +440,13 @@ func BenchmarkManagerOperations(b *testing.B) { } }) }) - + b.Run("GetAllGroups", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = manager.GetAllGroups() } }) - + b.Run("GetStats", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -462,9 +462,9 @@ func BenchmarkInviteSystem(b *testing.B) { InviteTimeout: 30 * time.Second, EnableStatistics: true, } - + manager := NewManager(config, nil) - + b.Run("AddInvite", func(b *testing.B) { for i := 0; i < b.N; i++ { leader := createTestEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) @@ -472,7 +472,7 @@ func BenchmarkInviteSystem(b *testing.B) { manager.AddInvite(leader, member) } }) - + b.Run("HasPendingInvite", func(b *testing.B) { // Add some invites first for i := 0; i < 100; i++ { @@ -480,7 +480,7 @@ func BenchmarkInviteSystem(b *testing.B) { member := createTestEntity(int32(i+2001), fmt.Sprintf("TestMember%d", i+1), true) manager.AddInvite(leader, member) } - + b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -489,7 +489,7 @@ func BenchmarkInviteSystem(b *testing.B) { } }) }) - + b.Run("Invite", func(b *testing.B) { for i := 0; i < b.N; i++ { leader := createTestEntity(int32(i+3001), fmt.Sprintf("InviteLeader%d", i+1), true) @@ -497,7 +497,7 @@ func BenchmarkInviteSystem(b *testing.B) { _ = manager.Invite(leader, member) } }) - + b.Run("DeclineInvite", func(b *testing.B) { // Add invites to decline for i := 0; i < b.N; i++ { @@ -515,20 +515,20 @@ func BenchmarkConcurrentOperations(b *testing.B) { MaxGroups: 1000, EnableStatistics: true, } - + manager := NewManager(config, nil) - + // Pre-populate for i := 0; i < 50; i++ { leader := createTestEntity(int32(i+1), fmt.Sprintf("ConcurrentLeader%d", i+1), true) manager.NewGroup(leader, nil, 0) } - + b.Run("ConcurrentGroupAccess", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { groupID := int32(rand.Intn(50) + 1) - + switch rand.Intn(4) { case 0: _ = manager.GetGroup(groupID) @@ -543,7 +543,7 @@ func BenchmarkConcurrentOperations(b *testing.B) { } }) }) - + b.Run("ConcurrentInviteOperations", func(b *testing.B) { var counter int64 b.RunParallel(func(pb *testing.PB) { @@ -551,7 +551,7 @@ func BenchmarkConcurrentOperations(b *testing.B) { i := atomic.AddInt64(&counter, 1) leader := createTestEntity(int32(i+20000), fmt.Sprintf("ConcurrentInviteLeader%d", i), true) member := createTestEntity(int32(i+30000), fmt.Sprintf("ConcurrentInviteMember%d", i), true) - + switch rand.Intn(3) { case 0: manager.AddInvite(leader, member) @@ -574,7 +574,7 @@ func BenchmarkMemoryAllocation(b *testing.B) { group.Disband() // Clean up background goroutine } }) - + b.Run("MasterListAllocation", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -582,7 +582,7 @@ func BenchmarkMemoryAllocation(b *testing.B) { _ = ml } }) - + b.Run("ManagerAllocation", func(b *testing.B) { config := GroupManagerConfig{} b.ReportAllocs() @@ -592,7 +592,7 @@ func BenchmarkMemoryAllocation(b *testing.B) { _ = manager } }) - + b.Run("GroupMemberInfoAllocation", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -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() - } - }) -} \ No newline at end of file diff --git a/internal/groups/entities.go b/internal/groups/entities.go index 113adb3..3fbcb3a 100644 --- a/internal/groups/entities.go +++ b/internal/groups/entities.go @@ -8,19 +8,19 @@ type Entity interface { GetLevel() int8 GetClass() int8 GetRace() int8 - + // Health and power GetHP() int32 GetTotalHP() int32 GetPower() int32 GetTotalPower() int32 - + // Entity types IsPlayer() bool IsBot() bool IsNPC() bool IsDead() bool - + // World positioning GetZone() Zone GetDistance(other Entity) float32 @@ -31,4 +31,4 @@ type Zone interface { GetZoneID() int32 GetInstanceID() int32 GetZoneName() string -} \ No newline at end of file +} diff --git a/internal/groups/group.go b/internal/groups/group.go index efafe27..f92dfad 100644 --- a/internal/groups/group.go +++ b/internal/groups/group.go @@ -9,29 +9,29 @@ 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:"-"` raidGroupsMutex sync.RWMutex `json:"-"` optionsMutex sync.RWMutex `json:"-"` activityMutex sync.RWMutex `json:"-"` disbandMutex sync.RWMutex `json:"-"` - + // Communication channels messageQueue chan *GroupMessage `json:"-"` updateQueue chan *GroupUpdate `json:"-"` - + // Background processing - stopChan chan struct{} `json:"-"` + stopChan chan struct{} `json:"-"` wg sync.WaitGroup `json:"-"` - + // Database integration - embedded operations db any `json:"-"` // Database connection isNew bool `json:"-"` // Flag for new groups @@ -53,11 +53,11 @@ func New(db any) *Group { db: db, isNew: true, } - + // Start background processing group.wg.Add(1) go group.processMessages() - + return group } @@ -106,7 +106,7 @@ func (g *Group) Save() error { func (g *Group) Delete() error { // Disband the group first g.Disband() - + // TODO: Implement database delete logic // This would require integration with the actual database system return nil diff --git a/internal/groups/groups_test.go b/internal/groups/groups_test.go index 32411aa..9ef589b 100644 --- a/internal/groups/groups_test.go +++ b/internal/groups/groups_test.go @@ -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 { @@ -111,7 +111,7 @@ func TestGroupCreation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { group := NewGroup(tt.groupID, tt.options, nil) - + if (group == nil) != tt.expectNil { t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil) return @@ -245,7 +245,7 @@ func TestGroupOptions(t *testing.T) { } group.SetGroupOptions(&newOptions) - + options = group.GetGroupOptions() if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED { t.Errorf("Updated loot method = %d, want %d", options.LootMethod, LOOT_METHOD_NEED_BEFORE_GREED) @@ -305,13 +305,13 @@ func TestGroupConcurrency(t *testing.T) { } wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true) - + for j := range operationsPerGoroutine { if j%2 == 0 { // Try to add member (will mostly fail due to full group) @@ -329,18 +329,18 @@ func TestGroupConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) // Test concurrent option updates t.Run("ConcurrentOptionUpdates", func(t *testing.T) { wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for j := range operationsPerGoroutine { if j%2 == 0 { // Read options @@ -357,18 +357,18 @@ func TestGroupConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) // Test concurrent raid operations t.Run("ConcurrentRaidOperations", func(t *testing.T) { wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for j := range operationsPerGoroutine { switch j % 4 { case 0: @@ -384,18 +384,18 @@ func TestGroupConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) // Test concurrent member info updates t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) { wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for range operationsPerGoroutine { members := group.GetMembers() if len(members) > 0 { @@ -406,7 +406,7 @@ func TestGroupConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) } @@ -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, @@ -555,7 +555,7 @@ func TestGroupManagerInvitations(t *testing.T) { t.Logf("Accept invite result = %d (expected due to missing leader lookup in test)", acceptResult) } - // Since invite acceptance failed due to missing world integration, + // Since invite acceptance failed due to missing world integration, // let's manually add the member to test the group functionality err := manager.AddGroupMember(groupID, member, false) if err != nil { @@ -570,11 +570,11 @@ func TestGroupManagerInvitations(t *testing.T) { // Test invitation timeout member2 := createMockEntity(3, "Member2", true) manager.Invite(leader, member2) - + // Wait for timeout (now timeout is 200ms so wait 250ms) time.Sleep(250 * time.Millisecond) - - // Try to accept after timeout (will fail due to missing leader lookup, + + // Try to accept after timeout (will fail due to missing leader lookup, // but we're mainly testing that the invite was cleaned up) acceptResult = manager.AcceptInvite(member2, nil, true) if acceptResult == GROUP_INVITE_SUCCESS { @@ -611,26 +611,26 @@ func TestGroupManagerConcurrency(t *testing.T) { // Test concurrent group creation and removal t.Run("ConcurrentGroupCreation", func(t *testing.T) { wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for j := range groupsPerGoroutine { leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true) - + // Create group groupID, err := manager.NewGroup(leader, nil, 0) if err != nil { continue } - + // Add some members for k := range 3 { member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true) _ = manager.AddGroupMember(groupID, member, false) } - + // Sometimes remove the group if j%2 == 0 { _ = manager.RemoveGroup(groupID) @@ -638,7 +638,7 @@ func TestGroupManagerConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) @@ -647,7 +647,7 @@ func TestGroupManagerConcurrency(t *testing.T) { // Create some groups groups := make([]int32, 10) leaders := make([]*mockEntity, 10) - + for i := range 10 { leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true) leaders[i] = leader @@ -656,20 +656,20 @@ func TestGroupManagerConcurrency(t *testing.T) { } wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for j := range 100 { leaderIdx := goroutineID % len(leaders) leader := leaders[leaderIdx] - + member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true) - + // Send invite _ = manager.Invite(leader, member) - + // Sometimes accept, sometimes decline if j%3 == 0 { _ = manager.AcceptInvite(member, nil, false) @@ -680,9 +680,9 @@ func TestGroupManagerConcurrency(t *testing.T) { } }(i) } - + wg.Wait() - + // Cleanup groups for _, groupID := range groups { _ = manager.RemoveGroup(groupID) @@ -692,11 +692,11 @@ func TestGroupManagerConcurrency(t *testing.T) { // Test concurrent statistics updates t.Run("ConcurrentStatistics", func(t *testing.T) { wg.Add(numGoroutines) - + for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() - + for range 1000 { _ = manager.GetStats() _ = manager.GetGroupCount() @@ -704,7 +704,7 @@ func TestGroupManagerConcurrency(t *testing.T) { } }(i) } - + wg.Wait() }) } @@ -860,4 +860,363 @@ func BenchmarkGroupOperations(b *testing.B) { } }) }) -} \ No newline at end of file +} + +// 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() + }) +} diff --git a/internal/groups/manager.go b/internal/groups/manager.go index 3a64b45..9560c42 100644 --- a/internal/groups/manager.go +++ b/internal/groups/manager.go @@ -9,36 +9,36 @@ 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"` RaidPendingInvites map[string]*GroupInvite `json:"raid_pending_invites"` invitesMutex sync.RWMutex `json:"-"` - + // Event handlers EventHandlers []GroupEventHandler `json:"-"` eventHandlersMutex sync.RWMutex `json:"-"` - + // Statistics statsMutex sync.RWMutex `json:"-"` - + // Background processing - stopChan chan struct{} `json:"-"` + stopChan chan struct{} `json:"-"` wg sync.WaitGroup `json:"-"` - + // Integration interfaces database GroupDatabase `json:"-"` packetHandler GroupPacketHandler `json:"-"` validator GroupValidator `json:"-"` notifier GroupNotifier `json:"-"` - + // Database integration - embedded operations db any `json:"-"` // Database connection isNew bool `json:"-"` // Flag for new managers @@ -58,7 +58,7 @@ func NewManager(config GroupManagerConfig, db any) *Manager { db: db, isNew: true, } - + return manager } @@ -72,7 +72,7 @@ func (m *Manager) Save() error { func (m *Manager) Delete() error { // Stop the manager first m.Stop() - + // TODO: Implement database delete logic return nil } @@ -165,7 +165,7 @@ func (m *Manager) RemoveGroup(groupID int32) error { // Disband the group group.Disband() - + // Remove from master list if !m.MasterList.RemoveGroup(groupID) { return fmt.Errorf("failed to remove group %d from master list", groupID) @@ -507,4 +507,4 @@ func (m *Manager) GetStats() GroupManagerStats { defer m.statsMutex.RUnlock() return m.Stats -} \ No newline at end of file +} diff --git a/internal/groups/manager_methods.go b/internal/groups/manager_methods.go index 1ceac6d..33a58e5 100644 --- a/internal/groups/manager_methods.go +++ b/internal/groups/manager_methods.go @@ -514,4 +514,4 @@ func (m *Manager) fireGroupInviteDeclinedEvent(leader, member Entity) { for _, handler := range m.EventHandlers { go handler.OnGroupInviteDeclined(leader, member) } -} \ No newline at end of file +} diff --git a/internal/groups/manager_test.go b/internal/groups/manager_test.go index c1bd4b0..65df6c0 100644 --- a/internal/groups/manager_test.go +++ b/internal/groups/manager_test.go @@ -290,7 +290,7 @@ func TestManagerConcurrentOperations(t *testing.T) { for j := range operationsPerGoroutine { leader := createMockEntity(int32(id*1000+j), fmt.Sprintf("Leader%d_%d", id, j), true) - + // Create group groupID, err := manager.NewGroup(leader, nil, 0) if err != nil { @@ -417,7 +417,7 @@ func TestManagerEventHandlers(t *testing.T) { } manager := NewManager(config, nil) - + // Track events events := make([]string, 0) var eventsMutex sync.Mutex @@ -610,4 +610,4 @@ func (m *mockEventHandler) OnGroupOptionsChanged(group *Group, oldOptions, newOp func (m *mockEventHandler) OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error { return nil -} \ No newline at end of file +} diff --git a/internal/groups/master.go b/internal/groups/master.go index 78cd00d..f5aeec3 100644 --- a/internal/groups/master.go +++ b/internal/groups/master.go @@ -2,212 +2,735 @@ 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() - - var totalMembers int32 + ml.mutex.Lock() // Need write lock to potentially update cache + defer ml.mutex.Unlock() + + ml.refreshMetaCache() + 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())) } - + if group.GetSize() == 0 && !group.IsDisbanded() { errors = append(errors, fmt.Errorf("group %d is empty but not disbanded", group.GetID())) } - + if group.GetSize() > MAX_GROUP_SIZE { - errors = append(errors, fmt.Errorf("group %d exceeds maximum size (%d > %d)", + errors = append(errors, fmt.Errorf("group %d exceeds maximum size (%d > %d)", group.GetID(), group.GetSize(), MAX_GROUP_SIZE)) } - + // Check for leader members := group.GetMembers() hasLeader := false leaderCount := 0 - + for _, member := range members { if member.Leader { hasLeader = true leaderCount++ } } - + if !hasLeader && !group.IsDisbanded() { errors = append(errors, fmt.Errorf("group %d has no leader", group.GetID())) } - + 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 -} \ No newline at end of file +} + +// IsValid returns true if all groups are valid +func (ml *MasterList) IsValid() bool { + errors := ml.ValidateAll() + return len(errors) == 0 +}