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

@ -36,9 +36,9 @@ func createTestEntity(id int32, name string, isPlayer bool) *mockEntity {
// createTestGroup creates a group with test data for benchmarking // createTestGroup creates a group with test data for benchmarking
func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group { func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group {
b.Helper() b.Helper()
group := NewGroup(groupID, nil, nil) group := NewGroup(groupID, nil, nil)
// Add test members // Add test members
for i := 0; i < memberCount; i++ { for i := 0; i < memberCount; i++ {
entity := createTestEntity( entity := createTestEntity(
@ -46,14 +46,14 @@ func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group {
fmt.Sprintf("Player%d", i+1), fmt.Sprintf("Player%d", i+1),
true, true,
) )
isLeader := (i == 0) isLeader := (i == 0)
err := group.AddMember(entity, isLeader) err := group.AddMember(entity, isLeader)
if err != nil { if err != nil {
b.Fatalf("Failed to add member to group: %v", err) b.Fatalf("Failed to add member to group: %v", err)
} }
} }
return group return group
} }
@ -65,7 +65,7 @@ func BenchmarkGroupCreation(b *testing.B) {
group.Disband() // Clean up background goroutine group.Disband() // Clean up background goroutine
} }
}) })
b.Run("NewGroupWithOptions", func(b *testing.B) { b.Run("NewGroupWithOptions", func(b *testing.B) {
options := DefaultGroupOptions() options := DefaultGroupOptions()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -73,7 +73,7 @@ func BenchmarkGroupCreation(b *testing.B) {
group.Disband() // Clean up background goroutine group.Disband() // Clean up background goroutine
} }
}) })
b.Run("NewGroupParallel", func(b *testing.B) { b.Run("NewGroupParallel", func(b *testing.B) {
var idCounter int64 var idCounter int64
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
@ -90,7 +90,7 @@ func BenchmarkGroupCreation(b *testing.B) {
func BenchmarkGroupMemberOperations(b *testing.B) { func BenchmarkGroupMemberOperations(b *testing.B) {
group := createTestGroup(b, 1001, 3) group := createTestGroup(b, 1001, 3)
defer group.Disband() // Clean up background goroutine defer group.Disband() // Clean up background goroutine
b.Run("AddMember", func(b *testing.B) { b.Run("AddMember", func(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -101,7 +101,7 @@ func BenchmarkGroupMemberOperations(b *testing.B) {
testGroup.Disband() // Clean up background goroutine testGroup.Disband() // Clean up background goroutine
} }
}) })
b.Run("GetSize", func(b *testing.B) { b.Run("GetSize", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -109,13 +109,13 @@ func BenchmarkGroupMemberOperations(b *testing.B) {
} }
}) })
}) })
b.Run("GetMembers", func(b *testing.B) { b.Run("GetMembers", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = group.GetMembers() _ = group.GetMembers()
} }
}) })
b.Run("GetLeaderName", func(b *testing.B) { b.Run("GetLeaderName", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -123,13 +123,13 @@ func BenchmarkGroupMemberOperations(b *testing.B) {
} }
}) })
}) })
b.Run("UpdateGroupMemberInfo", func(b *testing.B) { b.Run("UpdateGroupMemberInfo", func(b *testing.B) {
members := group.GetMembers() members := group.GetMembers()
if len(members) == 0 { if len(members) == 0 {
b.Skip("No members to update") b.Skip("No members to update")
} }
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
member := members[i%len(members)] member := members[i%len(members)]
if member.Member != nil { if member.Member != nil {
@ -143,7 +143,7 @@ func BenchmarkGroupMemberOperations(b *testing.B) {
func BenchmarkGroupOptions(b *testing.B) { func BenchmarkGroupOptions(b *testing.B) {
group := createTestGroup(b, 1001, 3) group := createTestGroup(b, 1001, 3)
defer group.Disband() // Clean up background goroutine defer group.Disband() // Clean up background goroutine
b.Run("GetGroupOptions", func(b *testing.B) { b.Run("GetGroupOptions", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -151,7 +151,7 @@ func BenchmarkGroupOptions(b *testing.B) {
} }
}) })
}) })
b.Run("SetGroupOptions", func(b *testing.B) { b.Run("SetGroupOptions", func(b *testing.B) {
options := DefaultGroupOptions() options := DefaultGroupOptions()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -159,7 +159,7 @@ func BenchmarkGroupOptions(b *testing.B) {
group.SetGroupOptions(&options) group.SetGroupOptions(&options)
} }
}) })
b.Run("GetLastLooterIndex", func(b *testing.B) { b.Run("GetLastLooterIndex", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -167,7 +167,7 @@ func BenchmarkGroupOptions(b *testing.B) {
} }
}) })
}) })
b.Run("SetNextLooterIndex", func(b *testing.B) { b.Run("SetNextLooterIndex", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.SetNextLooterIndex(int8(i % 6)) group.SetNextLooterIndex(int8(i % 6))
@ -179,11 +179,11 @@ func BenchmarkGroupOptions(b *testing.B) {
func BenchmarkGroupRaidOperations(b *testing.B) { func BenchmarkGroupRaidOperations(b *testing.B) {
group := createTestGroup(b, 1001, 6) group := createTestGroup(b, 1001, 6)
defer group.Disband() // Clean up background goroutine defer group.Disband() // Clean up background goroutine
// Setup some raid groups // Setup some raid groups
raidGroups := []int32{1001, 1002, 1003, 1004} raidGroups := []int32{1001, 1002, 1003, 1004}
group.ReplaceRaidGroups(raidGroups) group.ReplaceRaidGroups(raidGroups)
b.Run("GetRaidGroups", func(b *testing.B) { b.Run("GetRaidGroups", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -191,7 +191,7 @@ func BenchmarkGroupRaidOperations(b *testing.B) {
} }
}) })
}) })
b.Run("IsGroupRaid", func(b *testing.B) { b.Run("IsGroupRaid", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -199,14 +199,14 @@ func BenchmarkGroupRaidOperations(b *testing.B) {
} }
}) })
}) })
b.Run("IsInRaidGroup", func(b *testing.B) { b.Run("IsInRaidGroup", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
targetID := int32((i % 4) + 1001) targetID := int32((i % 4) + 1001)
_ = group.IsInRaidGroup(targetID, false) _ = group.IsInRaidGroup(targetID, false)
} }
}) })
b.Run("AddGroupToRaid", func(b *testing.B) { b.Run("AddGroupToRaid", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// Cycle through a limited set of group IDs to avoid infinite growth // Cycle through a limited set of group IDs to avoid infinite growth
@ -214,14 +214,14 @@ func BenchmarkGroupRaidOperations(b *testing.B) {
group.AddGroupToRaid(groupID) group.AddGroupToRaid(groupID)
} }
}) })
b.Run("ReplaceRaidGroups", func(b *testing.B) { b.Run("ReplaceRaidGroups", func(b *testing.B) {
newGroups := []int32{2001, 2002, 2003} newGroups := []int32{2001, 2002, 2003}
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.ReplaceRaidGroups(newGroups) group.ReplaceRaidGroups(newGroups)
} }
}) })
b.Run("ClearGroupRaid", func(b *testing.B) { b.Run("ClearGroupRaid", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.ClearGroupRaid() group.ClearGroupRaid()
@ -235,25 +235,25 @@ func BenchmarkGroupRaidOperations(b *testing.B) {
func BenchmarkGroupMessaging(b *testing.B) { func BenchmarkGroupMessaging(b *testing.B) {
group := createTestGroup(b, 1001, 6) group := createTestGroup(b, 1001, 6)
defer group.Disband() // Clean up background goroutine defer group.Disband() // Clean up background goroutine
b.Run("SimpleGroupMessage", func(b *testing.B) { b.Run("SimpleGroupMessage", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.SimpleGroupMessage(fmt.Sprintf("Benchmark message %d", i)) group.SimpleGroupMessage(fmt.Sprintf("Benchmark message %d", i))
} }
}) })
b.Run("SendGroupMessage", func(b *testing.B) { b.Run("SendGroupMessage", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.SendGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, fmt.Sprintf("System message %d", i)) group.SendGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, fmt.Sprintf("System message %d", i))
} }
}) })
b.Run("GroupChatMessageFromName", func(b *testing.B) { b.Run("GroupChatMessageFromName", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
group.GroupChatMessageFromName( group.GroupChatMessageFromName(
fmt.Sprintf("Player%d", i%6+1), fmt.Sprintf("Player%d", i%6+1),
0, 0,
fmt.Sprintf("Chat message %d", i), fmt.Sprintf("Chat message %d", i),
CHANNEL_GROUP_CHAT, CHANNEL_GROUP_CHAT,
) )
} }
@ -264,7 +264,7 @@ func BenchmarkGroupMessaging(b *testing.B) {
func BenchmarkGroupState(b *testing.B) { func BenchmarkGroupState(b *testing.B) {
group := createTestGroup(b, 1001, 6) group := createTestGroup(b, 1001, 6)
defer group.Disband() // Clean up background goroutine defer group.Disband() // Clean up background goroutine
b.Run("GetID", func(b *testing.B) { b.Run("GetID", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -272,7 +272,7 @@ func BenchmarkGroupState(b *testing.B) {
} }
}) })
}) })
b.Run("IsDisbanded", func(b *testing.B) { b.Run("IsDisbanded", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -280,7 +280,7 @@ func BenchmarkGroupState(b *testing.B) {
} }
}) })
}) })
b.Run("GetCreatedTime", func(b *testing.B) { b.Run("GetCreatedTime", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -288,7 +288,7 @@ func BenchmarkGroupState(b *testing.B) {
} }
}) })
}) })
b.Run("GetLastActivity", func(b *testing.B) { b.Run("GetLastActivity", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -301,11 +301,11 @@ func BenchmarkGroupState(b *testing.B) {
// BenchmarkMasterListOperations measures master list performance // BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) { func BenchmarkMasterListOperations(b *testing.B) {
ml := NewMasterList() ml := NewMasterList()
// Pre-populate with groups // Pre-populate with groups
const numGroups = 1000 const numGroups = 1000
groups := make([]*Group, numGroups) groups := make([]*Group, numGroups)
b.StopTimer() b.StopTimer()
for i := 0; i < numGroups; i++ { for i := 0; i < numGroups; i++ {
groups[i] = createTestGroup(b, int32(i+1), rand.Intn(6)+1) groups[i] = createTestGroup(b, int32(i+1), rand.Intn(6)+1)
@ -320,7 +320,7 @@ func BenchmarkMasterListOperations(b *testing.B) {
} }
}() }()
b.StartTimer() b.StartTimer()
b.Run("GetGroup", func(b *testing.B) { b.Run("GetGroup", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -329,7 +329,7 @@ func BenchmarkMasterListOperations(b *testing.B) {
} }
}) })
}) })
b.Run("AddGroup", func(b *testing.B) { b.Run("AddGroup", func(b *testing.B) {
startID := int32(numGroups + 1) startID := int32(numGroups + 1)
var addedGroups []*Group var addedGroups []*Group
@ -344,39 +344,39 @@ func BenchmarkMasterListOperations(b *testing.B) {
group.Disband() group.Disband()
} }
}) })
b.Run("GetAllGroups", func(b *testing.B) { b.Run("GetAllGroups", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = ml.GetAllGroups() _ = ml.GetAllGroups()
} }
}) })
b.Run("GetActiveGroups", func(b *testing.B) { b.Run("GetActiveGroups", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = ml.GetActiveGroups() _ = ml.GetActiveGroups()
} }
}) })
b.Run("GetGroupsByZone", func(b *testing.B) { b.Run("GetGroupsByZone", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
zoneID := int32(rand.Intn(100) + 1) zoneID := int32(rand.Intn(100) + 1)
_ = ml.GetGroupsByZone(zoneID) _ = ml.GetGroupsByZone(zoneID)
} }
}) })
b.Run("GetGroupsBySize", func(b *testing.B) { b.Run("GetGroupsBySize", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
size := int32(rand.Intn(6) + 1) size := int32(rand.Intn(6) + 1)
_ = ml.GetGroupsBySize(size) _ = ml.GetGroupsBySize(size)
} }
}) })
b.Run("GetRaidGroups", func(b *testing.B) { b.Run("GetRaidGroups", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = ml.GetRaidGroups() _ = ml.GetRaidGroups()
} }
}) })
b.Run("GetGroupStatistics", func(b *testing.B) { b.Run("GetGroupStatistics", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = ml.GetGroupStatistics() _ = ml.GetGroupStatistics()
@ -393,9 +393,9 @@ func BenchmarkManagerOperations(b *testing.B) {
BuffUpdateInterval: 5 * time.Second, BuffUpdateInterval: 5 * time.Second,
EnableStatistics: true, EnableStatistics: true,
} }
manager := NewManager(config, nil) manager := NewManager(config, nil)
// Pre-populate with groups // Pre-populate with groups
b.StopTimer() b.StopTimer()
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
@ -403,7 +403,7 @@ func BenchmarkManagerOperations(b *testing.B) {
manager.NewGroup(leader, nil, 0) manager.NewGroup(leader, nil, 0)
} }
b.StartTimer() b.StartTimer()
b.Run("NewGroup", func(b *testing.B) { b.Run("NewGroup", func(b *testing.B) {
startID := int32(1000) startID := int32(1000)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -414,7 +414,7 @@ func BenchmarkManagerOperations(b *testing.B) {
} }
} }
}) })
b.Run("GetGroup", func(b *testing.B) { b.Run("GetGroup", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -423,7 +423,7 @@ func BenchmarkManagerOperations(b *testing.B) {
} }
}) })
}) })
b.Run("IsGroupIDValid", func(b *testing.B) { b.Run("IsGroupIDValid", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -432,7 +432,7 @@ func BenchmarkManagerOperations(b *testing.B) {
} }
}) })
}) })
b.Run("GetGroupCount", func(b *testing.B) { b.Run("GetGroupCount", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -440,13 +440,13 @@ func BenchmarkManagerOperations(b *testing.B) {
} }
}) })
}) })
b.Run("GetAllGroups", func(b *testing.B) { b.Run("GetAllGroups", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = manager.GetAllGroups() _ = manager.GetAllGroups()
} }
}) })
b.Run("GetStats", func(b *testing.B) { b.Run("GetStats", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -462,9 +462,9 @@ func BenchmarkInviteSystem(b *testing.B) {
InviteTimeout: 30 * time.Second, InviteTimeout: 30 * time.Second,
EnableStatistics: true, EnableStatistics: true,
} }
manager := NewManager(config, nil) manager := NewManager(config, nil)
b.Run("AddInvite", func(b *testing.B) { b.Run("AddInvite", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
leader := createTestEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) 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) manager.AddInvite(leader, member)
} }
}) })
b.Run("HasPendingInvite", func(b *testing.B) { b.Run("HasPendingInvite", func(b *testing.B) {
// Add some invites first // Add some invites first
for i := 0; i < 100; i++ { 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) member := createTestEntity(int32(i+2001), fmt.Sprintf("TestMember%d", i+1), true)
manager.AddInvite(leader, member) manager.AddInvite(leader, member)
} }
b.ResetTimer() b.ResetTimer()
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
@ -489,7 +489,7 @@ func BenchmarkInviteSystem(b *testing.B) {
} }
}) })
}) })
b.Run("Invite", func(b *testing.B) { b.Run("Invite", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
leader := createTestEntity(int32(i+3001), fmt.Sprintf("InviteLeader%d", i+1), true) 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) _ = manager.Invite(leader, member)
} }
}) })
b.Run("DeclineInvite", func(b *testing.B) { b.Run("DeclineInvite", func(b *testing.B) {
// Add invites to decline // Add invites to decline
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -515,20 +515,20 @@ func BenchmarkConcurrentOperations(b *testing.B) {
MaxGroups: 1000, MaxGroups: 1000,
EnableStatistics: true, EnableStatistics: true,
} }
manager := NewManager(config, nil) manager := NewManager(config, nil)
// Pre-populate // Pre-populate
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
leader := createTestEntity(int32(i+1), fmt.Sprintf("ConcurrentLeader%d", i+1), true) leader := createTestEntity(int32(i+1), fmt.Sprintf("ConcurrentLeader%d", i+1), true)
manager.NewGroup(leader, nil, 0) manager.NewGroup(leader, nil, 0)
} }
b.Run("ConcurrentGroupAccess", func(b *testing.B) { b.Run("ConcurrentGroupAccess", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
groupID := int32(rand.Intn(50) + 1) groupID := int32(rand.Intn(50) + 1)
switch rand.Intn(4) { switch rand.Intn(4) {
case 0: case 0:
_ = manager.GetGroup(groupID) _ = manager.GetGroup(groupID)
@ -543,7 +543,7 @@ func BenchmarkConcurrentOperations(b *testing.B) {
} }
}) })
}) })
b.Run("ConcurrentInviteOperations", func(b *testing.B) { b.Run("ConcurrentInviteOperations", func(b *testing.B) {
var counter int64 var counter int64
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
@ -551,7 +551,7 @@ func BenchmarkConcurrentOperations(b *testing.B) {
i := atomic.AddInt64(&counter, 1) i := atomic.AddInt64(&counter, 1)
leader := createTestEntity(int32(i+20000), fmt.Sprintf("ConcurrentInviteLeader%d", i), true) leader := createTestEntity(int32(i+20000), fmt.Sprintf("ConcurrentInviteLeader%d", i), true)
member := createTestEntity(int32(i+30000), fmt.Sprintf("ConcurrentInviteMember%d", i), true) member := createTestEntity(int32(i+30000), fmt.Sprintf("ConcurrentInviteMember%d", i), true)
switch rand.Intn(3) { switch rand.Intn(3) {
case 0: case 0:
manager.AddInvite(leader, member) manager.AddInvite(leader, member)
@ -574,7 +574,7 @@ func BenchmarkMemoryAllocation(b *testing.B) {
group.Disband() // Clean up background goroutine group.Disband() // Clean up background goroutine
} }
}) })
b.Run("MasterListAllocation", func(b *testing.B) { b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -582,7 +582,7 @@ func BenchmarkMemoryAllocation(b *testing.B) {
_ = ml _ = ml
} }
}) })
b.Run("ManagerAllocation", func(b *testing.B) { b.Run("ManagerAllocation", func(b *testing.B) {
config := GroupManagerConfig{} config := GroupManagerConfig{}
b.ReportAllocs() b.ReportAllocs()
@ -592,7 +592,7 @@ func BenchmarkMemoryAllocation(b *testing.B) {
_ = manager _ = manager
} }
}) })
b.Run("GroupMemberInfoAllocation", func(b *testing.B) { b.Run("GroupMemberInfoAllocation", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { 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()
}
})
}

View File

@ -8,19 +8,19 @@ type Entity interface {
GetLevel() int8 GetLevel() int8
GetClass() int8 GetClass() int8
GetRace() int8 GetRace() int8
// Health and power // Health and power
GetHP() int32 GetHP() int32
GetTotalHP() int32 GetTotalHP() int32
GetPower() int32 GetPower() int32
GetTotalPower() int32 GetTotalPower() int32
// Entity types // Entity types
IsPlayer() bool IsPlayer() bool
IsBot() bool IsBot() bool
IsNPC() bool IsNPC() bool
IsDead() bool IsDead() bool
// World positioning // World positioning
GetZone() Zone GetZone() Zone
GetDistance(other Entity) float32 GetDistance(other Entity) float32
@ -31,4 +31,4 @@ type Zone interface {
GetZoneID() int32 GetZoneID() int32
GetInstanceID() int32 GetInstanceID() int32
GetZoneName() string GetZoneName() string
} }

View File

@ -9,29 +9,29 @@ import (
// Group represents a player group with embedded database operations // Group represents a player group with embedded database operations
type Group struct { type Group struct {
// Core fields // Core fields
GroupID int32 `json:"group_id" db:"group_id"` GroupID int32 `json:"group_id" db:"group_id"`
Options GroupOptions `json:"options"` Options GroupOptions `json:"options"`
Members []*GroupMemberInfo `json:"members"` Members []*GroupMemberInfo `json:"members"`
RaidGroups []int32 `json:"raid_groups"` RaidGroups []int32 `json:"raid_groups"`
CreatedTime time.Time `json:"created_time" db:"created_time"` CreatedTime time.Time `json:"created_time" db:"created_time"`
LastActivity time.Time `json:"last_activity" db:"last_activity"` LastActivity time.Time `json:"last_activity" db:"last_activity"`
Disbanded bool `json:"disbanded" db:"disbanded"` Disbanded bool `json:"disbanded" db:"disbanded"`
// Internal fields // Internal fields
membersMutex sync.RWMutex `json:"-"` membersMutex sync.RWMutex `json:"-"`
raidGroupsMutex sync.RWMutex `json:"-"` raidGroupsMutex sync.RWMutex `json:"-"`
optionsMutex sync.RWMutex `json:"-"` optionsMutex sync.RWMutex `json:"-"`
activityMutex sync.RWMutex `json:"-"` activityMutex sync.RWMutex `json:"-"`
disbandMutex sync.RWMutex `json:"-"` disbandMutex sync.RWMutex `json:"-"`
// Communication channels // Communication channels
messageQueue chan *GroupMessage `json:"-"` messageQueue chan *GroupMessage `json:"-"`
updateQueue chan *GroupUpdate `json:"-"` updateQueue chan *GroupUpdate `json:"-"`
// Background processing // Background processing
stopChan chan struct{} `json:"-"` stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"` wg sync.WaitGroup `json:"-"`
// Database integration - embedded operations // Database integration - embedded operations
db any `json:"-"` // Database connection db any `json:"-"` // Database connection
isNew bool `json:"-"` // Flag for new groups isNew bool `json:"-"` // Flag for new groups
@ -53,11 +53,11 @@ func New(db any) *Group {
db: db, db: db,
isNew: true, isNew: true,
} }
// Start background processing // Start background processing
group.wg.Add(1) group.wg.Add(1)
go group.processMessages() go group.processMessages()
return group return group
} }
@ -106,7 +106,7 @@ func (g *Group) Save() error {
func (g *Group) Delete() error { func (g *Group) Delete() error {
// Disband the group first // Disband the group first
g.Disband() g.Disband()
// TODO: Implement database delete logic // TODO: Implement database delete logic
// This would require integration with the actual database system // This would require integration with the actual database system
return nil return nil

View File

@ -9,46 +9,46 @@ import (
// Mock entity implementation for testing // Mock entity implementation for testing
type mockEntity struct { type mockEntity struct {
id int32 id int32
name string name string
level int8 level int8
class int8 class int8
race int8 race int8
hp int32 hp int32
maxHP int32 maxHP int32
power int32 power int32
maxPower int32 maxPower int32
isPlayer bool isPlayer bool
isNPC bool isNPC bool
isBot bool isBot bool
isDead bool isDead bool
zone *mockZone zone *mockZone
groupID int32 groupID int32
groupInfo *GroupMemberInfo groupInfo *GroupMemberInfo
} }
func (m *mockEntity) GetID() int32 { return m.id } func (m *mockEntity) GetID() int32 { return m.id }
func (m *mockEntity) GetName() string { return m.name } func (m *mockEntity) GetName() string { return m.name }
func (m *mockEntity) GetLevel() int8 { return m.level } func (m *mockEntity) GetLevel() int8 { return m.level }
func (m *mockEntity) GetClass() int8 { return m.class } func (m *mockEntity) GetClass() int8 { return m.class }
func (m *mockEntity) GetRace() int8 { return m.race } func (m *mockEntity) GetRace() int8 { return m.race }
func (m *mockEntity) GetHP() int32 { return m.hp } func (m *mockEntity) GetHP() int32 { return m.hp }
func (m *mockEntity) GetTotalHP() int32 { return m.maxHP } func (m *mockEntity) GetTotalHP() int32 { return m.maxHP }
func (m *mockEntity) GetPower() int32 { return m.power } func (m *mockEntity) GetPower() int32 { return m.power }
func (m *mockEntity) GetTotalPower() int32 { return m.maxPower } func (m *mockEntity) GetTotalPower() int32 { return m.maxPower }
func (m *mockEntity) IsPlayer() bool { return m.isPlayer } func (m *mockEntity) IsPlayer() bool { return m.isPlayer }
func (m *mockEntity) IsNPC() bool { return m.isNPC } func (m *mockEntity) IsNPC() bool { return m.isNPC }
func (m *mockEntity) IsBot() bool { return m.isBot } func (m *mockEntity) IsBot() bool { return m.isBot }
func (m *mockEntity) IsDead() bool { return m.isDead } func (m *mockEntity) IsDead() bool { return m.isDead }
func (m *mockEntity) GetZone() Zone { return m.zone } func (m *mockEntity) GetZone() Zone { return m.zone }
func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 } func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 }
// GroupAware implementation // GroupAware implementation
func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo } func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo }
func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info } func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info }
func (m *mockEntity) GetGroupID() int32 { return m.groupID } func (m *mockEntity) GetGroupID() int32 { return m.groupID }
func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID } func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID }
func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 } func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 }
// Mock zone implementation // Mock zone implementation
type mockZone struct { type mockZone struct {
@ -111,7 +111,7 @@ func TestGroupCreation(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
group := NewGroup(tt.groupID, tt.options, nil) group := NewGroup(tt.groupID, tt.options, nil)
if (group == nil) != tt.expectNil { if (group == nil) != tt.expectNil {
t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil) t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil)
return return
@ -245,7 +245,7 @@ func TestGroupOptions(t *testing.T) {
} }
group.SetGroupOptions(&newOptions) group.SetGroupOptions(&newOptions)
options = group.GetGroupOptions() options = group.GetGroupOptions()
if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED { if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED {
t.Errorf("Updated loot method = %d, want %d", 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) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true) member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true)
for j := range operationsPerGoroutine { for j := range operationsPerGoroutine {
if j%2 == 0 { if j%2 == 0 {
// Try to add member (will mostly fail due to full group) // Try to add member (will mostly fail due to full group)
@ -329,18 +329,18 @@ func TestGroupConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
// Test concurrent option updates // Test concurrent option updates
t.Run("ConcurrentOptionUpdates", func(t *testing.T) { t.Run("ConcurrentOptionUpdates", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := range operationsPerGoroutine { for j := range operationsPerGoroutine {
if j%2 == 0 { if j%2 == 0 {
// Read options // Read options
@ -357,18 +357,18 @@ func TestGroupConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
// Test concurrent raid operations // Test concurrent raid operations
t.Run("ConcurrentRaidOperations", func(t *testing.T) { t.Run("ConcurrentRaidOperations", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := range operationsPerGoroutine { for j := range operationsPerGoroutine {
switch j % 4 { switch j % 4 {
case 0: case 0:
@ -384,18 +384,18 @@ func TestGroupConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
// Test concurrent member info updates // Test concurrent member info updates
t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) { t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for range operationsPerGoroutine { for range operationsPerGoroutine {
members := group.GetMembers() members := group.GetMembers()
if len(members) > 0 { if len(members) > 0 {
@ -406,7 +406,7 @@ func TestGroupConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
} }
@ -521,8 +521,8 @@ func TestGroupManagerInvitations(t *testing.T) {
MaxGroups: 1000, MaxGroups: 1000,
MaxRaidGroups: 4, MaxRaidGroups: 4,
InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing
UpdateInterval: 0, // Disable background updates for testing UpdateInterval: 0, // Disable background updates for testing
BuffUpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing
EnableCrossServer: true, EnableCrossServer: true,
EnableRaids: true, EnableRaids: true,
EnableQuestSharing: true, EnableQuestSharing: true,
@ -555,7 +555,7 @@ func TestGroupManagerInvitations(t *testing.T) {
t.Logf("Accept invite result = %d (expected due to missing leader lookup in test)", acceptResult) 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 // let's manually add the member to test the group functionality
err := manager.AddGroupMember(groupID, member, false) err := manager.AddGroupMember(groupID, member, false)
if err != nil { if err != nil {
@ -570,11 +570,11 @@ func TestGroupManagerInvitations(t *testing.T) {
// Test invitation timeout // Test invitation timeout
member2 := createMockEntity(3, "Member2", true) member2 := createMockEntity(3, "Member2", true)
manager.Invite(leader, member2) manager.Invite(leader, member2)
// Wait for timeout (now timeout is 200ms so wait 250ms) // Wait for timeout (now timeout is 200ms so wait 250ms)
time.Sleep(250 * time.Millisecond) 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) // but we're mainly testing that the invite was cleaned up)
acceptResult = manager.AcceptInvite(member2, nil, true) acceptResult = manager.AcceptInvite(member2, nil, true)
if acceptResult == GROUP_INVITE_SUCCESS { if acceptResult == GROUP_INVITE_SUCCESS {
@ -611,26 +611,26 @@ func TestGroupManagerConcurrency(t *testing.T) {
// Test concurrent group creation and removal // Test concurrent group creation and removal
t.Run("ConcurrentGroupCreation", func(t *testing.T) { t.Run("ConcurrentGroupCreation", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := range groupsPerGoroutine { for j := range groupsPerGoroutine {
leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true) leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true)
// Create group // Create group
groupID, err := manager.NewGroup(leader, nil, 0) groupID, err := manager.NewGroup(leader, nil, 0)
if err != nil { if err != nil {
continue continue
} }
// Add some members // Add some members
for k := range 3 { for k := range 3 {
member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true) member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true)
_ = manager.AddGroupMember(groupID, member, false) _ = manager.AddGroupMember(groupID, member, false)
} }
// Sometimes remove the group // Sometimes remove the group
if j%2 == 0 { if j%2 == 0 {
_ = manager.RemoveGroup(groupID) _ = manager.RemoveGroup(groupID)
@ -638,7 +638,7 @@ func TestGroupManagerConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
@ -647,7 +647,7 @@ func TestGroupManagerConcurrency(t *testing.T) {
// Create some groups // Create some groups
groups := make([]int32, 10) groups := make([]int32, 10)
leaders := make([]*mockEntity, 10) leaders := make([]*mockEntity, 10)
for i := range 10 { for i := range 10 {
leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true) leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true)
leaders[i] = leader leaders[i] = leader
@ -656,20 +656,20 @@ func TestGroupManagerConcurrency(t *testing.T) {
} }
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := range 100 { for j := range 100 {
leaderIdx := goroutineID % len(leaders) leaderIdx := goroutineID % len(leaders)
leader := leaders[leaderIdx] leader := leaders[leaderIdx]
member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true) member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true)
// Send invite // Send invite
_ = manager.Invite(leader, member) _ = manager.Invite(leader, member)
// Sometimes accept, sometimes decline // Sometimes accept, sometimes decline
if j%3 == 0 { if j%3 == 0 {
_ = manager.AcceptInvite(member, nil, false) _ = manager.AcceptInvite(member, nil, false)
@ -680,9 +680,9 @@ func TestGroupManagerConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
// Cleanup groups // Cleanup groups
for _, groupID := range groups { for _, groupID := range groups {
_ = manager.RemoveGroup(groupID) _ = manager.RemoveGroup(groupID)
@ -692,11 +692,11 @@ func TestGroupManagerConcurrency(t *testing.T) {
// Test concurrent statistics updates // Test concurrent statistics updates
t.Run("ConcurrentStatistics", func(t *testing.T) { t.Run("ConcurrentStatistics", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := range numGoroutines { for i := range numGoroutines {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for range 1000 { for range 1000 {
_ = manager.GetStats() _ = manager.GetStats()
_ = manager.GetGroupCount() _ = manager.GetGroupCount()
@ -704,7 +704,7 @@ func TestGroupManagerConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
} }
@ -860,4 +860,363 @@ 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,36 +9,36 @@ import (
// Manager provides group management with embedded database operations // Manager provides group management with embedded database operations
type Manager struct { type Manager struct {
// Core fields with embedded database operations // Core fields with embedded database operations
MasterList *MasterList `json:"master_list"` MasterList *MasterList `json:"master_list"`
Config GroupManagerConfig `json:"config"` Config GroupManagerConfig `json:"config"`
Stats GroupManagerStats `json:"stats"` Stats GroupManagerStats `json:"stats"`
// Group ID generation // Group ID generation
nextGroupID int32 `json:"-" db:"next_group_id"` nextGroupID int32 `json:"-" db:"next_group_id"`
nextGroupIDMutex sync.Mutex `json:"-"` nextGroupIDMutex sync.Mutex `json:"-"`
// Pending invitations // Pending invitations
PendingInvites map[string]*GroupInvite `json:"pending_invites"` PendingInvites map[string]*GroupInvite `json:"pending_invites"`
RaidPendingInvites map[string]*GroupInvite `json:"raid_pending_invites"` RaidPendingInvites map[string]*GroupInvite `json:"raid_pending_invites"`
invitesMutex sync.RWMutex `json:"-"` invitesMutex sync.RWMutex `json:"-"`
// Event handlers // Event handlers
EventHandlers []GroupEventHandler `json:"-"` EventHandlers []GroupEventHandler `json:"-"`
eventHandlersMutex sync.RWMutex `json:"-"` eventHandlersMutex sync.RWMutex `json:"-"`
// Statistics // Statistics
statsMutex sync.RWMutex `json:"-"` statsMutex sync.RWMutex `json:"-"`
// Background processing // Background processing
stopChan chan struct{} `json:"-"` stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"` wg sync.WaitGroup `json:"-"`
// Integration interfaces // Integration interfaces
database GroupDatabase `json:"-"` database GroupDatabase `json:"-"`
packetHandler GroupPacketHandler `json:"-"` packetHandler GroupPacketHandler `json:"-"`
validator GroupValidator `json:"-"` validator GroupValidator `json:"-"`
notifier GroupNotifier `json:"-"` notifier GroupNotifier `json:"-"`
// Database integration - embedded operations // Database integration - embedded operations
db any `json:"-"` // Database connection db any `json:"-"` // Database connection
isNew bool `json:"-"` // Flag for new managers isNew bool `json:"-"` // Flag for new managers
@ -58,7 +58,7 @@ func NewManager(config GroupManagerConfig, db any) *Manager {
db: db, db: db,
isNew: true, isNew: true,
} }
return manager return manager
} }
@ -72,7 +72,7 @@ func (m *Manager) Save() error {
func (m *Manager) Delete() error { func (m *Manager) Delete() error {
// Stop the manager first // Stop the manager first
m.Stop() m.Stop()
// TODO: Implement database delete logic // TODO: Implement database delete logic
return nil return nil
} }
@ -165,7 +165,7 @@ func (m *Manager) RemoveGroup(groupID int32) error {
// Disband the group // Disband the group
group.Disband() group.Disband()
// Remove from master list // Remove from master list
if !m.MasterList.RemoveGroup(groupID) { if !m.MasterList.RemoveGroup(groupID) {
return fmt.Errorf("failed to remove group %d from master list", 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() defer m.statsMutex.RUnlock()
return m.Stats return m.Stats
} }

View File

@ -514,4 +514,4 @@ func (m *Manager) fireGroupInviteDeclinedEvent(leader, member Entity) {
for _, handler := range m.EventHandlers { for _, handler := range m.EventHandlers {
go handler.OnGroupInviteDeclined(leader, member) go handler.OnGroupInviteDeclined(leader, member)
} }
} }

View File

@ -290,7 +290,7 @@ func TestManagerConcurrentOperations(t *testing.T) {
for j := range operationsPerGoroutine { for j := range operationsPerGoroutine {
leader := createMockEntity(int32(id*1000+j), fmt.Sprintf("Leader%d_%d", id, j), true) leader := createMockEntity(int32(id*1000+j), fmt.Sprintf("Leader%d_%d", id, j), true)
// Create group // Create group
groupID, err := manager.NewGroup(leader, nil, 0) groupID, err := manager.NewGroup(leader, nil, 0)
if err != nil { if err != nil {
@ -417,7 +417,7 @@ func TestManagerEventHandlers(t *testing.T) {
} }
manager := NewManager(config, nil) manager := NewManager(config, nil)
// Track events // Track events
events := make([]string, 0) events := make([]string, 0)
var eventsMutex sync.Mutex 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 { func (m *mockEventHandler) OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error {
return nil return nil
} }

View File

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