diff --git a/go.mod b/go.mod index cd1a840..bf0f3ac 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,16 @@ module eq2emu go 1.24.5 -require zombiezen.com/go/sqlite v1.4.2 +require ( + github.com/go-sql-driver/mysql v1.9.3 + zombiezen.com/go/sqlite v1.4.2 +) require golang.org/x/text v0.27.0 // indirect require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect diff --git a/internal/groups/benchmark_test.go b/internal/groups/benchmark_test.go new file mode 100644 index 0000000..8e84e84 --- /dev/null +++ b/internal/groups/benchmark_test.go @@ -0,0 +1,666 @@ +package groups + +import ( + "fmt" + "math/rand" + "sync/atomic" + "testing" + "time" +) + +// Mock implementations for benchmarking use the existing mock entities from groups_test.go + +// Helper functions for creating test data + +// createTestEntity creates a mock entity for benchmarking +func createTestEntity(id int32, name string, isPlayer bool) *mockEntity { + entity := createMockEntity(id, name, isPlayer) + // Randomize some properties for more realistic benchmarking + entity.level = int8(rand.Intn(80) + 1) + entity.class = int8(rand.Intn(25) + 1) + entity.race = int8(rand.Intn(18) + 1) + entity.hp = int32(rand.Intn(5000) + 1000) + entity.maxHP = int32(rand.Intn(5000) + 1000) + entity.power = int32(rand.Intn(3000) + 500) + entity.maxPower = int32(rand.Intn(3000) + 500) + entity.isBot = !isPlayer && rand.Intn(2) == 1 + entity.isNPC = !isPlayer && rand.Intn(2) == 0 + entity.zone = &mockZone{ + zoneID: int32(rand.Intn(100) + 1), + instanceID: int32(rand.Intn(10)), + zoneName: fmt.Sprintf("Zone %d", rand.Intn(100)+1), + } + return entity +} + +// 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( + int32(i+1), + 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 +} + +// BenchmarkGroupCreation measures group creation performance +func BenchmarkGroupCreation(b *testing.B) { + b.Run("NewGroup", func(b *testing.B) { + for i := 0; i < b.N; i++ { + group := NewGroup(int32(i+1), nil, nil) + group.Disband() // Clean up background goroutine + } + }) + + b.Run("NewGroupWithOptions", func(b *testing.B) { + options := DefaultGroupOptions() + for i := 0; i < b.N; i++ { + group := NewGroup(int32(i+1), &options, nil) + group.Disband() // Clean up background goroutine + } + }) + + b.Run("NewGroupParallel", func(b *testing.B) { + var idCounter int64 + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + id := atomic.AddInt64(&idCounter, 1) + group := NewGroup(int32(id), nil, nil) + group.Disband() // Clean up background goroutine + } + }) + }) +} + +// BenchmarkGroupMemberOperations measures member management performance +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++ { + // Create a new group for each iteration to avoid full group + testGroup := NewGroup(int32(i+1000), nil, nil) + entity := createTestEntity(int32(i+1), fmt.Sprintf("BenchPlayer%d", i), true) + testGroup.AddMember(entity, false) + testGroup.Disband() // Clean up background goroutine + } + }) + + b.Run("GetSize", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.GetSize() + } + }) + }) + + 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() { + _ = group.GetLeaderName() + } + }) + }) + + 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 { + group.UpdateGroupMemberInfo(member.Member, false) + } + } + }) +} + +// BenchmarkGroupOptions measures group options performance +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() { + _ = group.GetGroupOptions() + } + }) + }) + + b.Run("SetGroupOptions", func(b *testing.B) { + options := DefaultGroupOptions() + for i := 0; i < b.N; i++ { + options.LootMethod = int8(i % 4) + group.SetGroupOptions(&options) + } + }) + + b.Run("GetLastLooterIndex", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.GetLastLooterIndex() + } + }) + }) + + b.Run("SetNextLooterIndex", func(b *testing.B) { + for i := 0; i < b.N; i++ { + group.SetNextLooterIndex(int8(i % 6)) + } + }) +} + +// BenchmarkGroupRaidOperations measures raid functionality performance +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() { + _ = group.GetRaidGroups() + } + }) + }) + + b.Run("IsGroupRaid", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.IsGroupRaid() + } + }) + }) + + 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 + groupID := int32(2000 + (i % 10)) + 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() + // Re-add groups for next iteration + group.ReplaceRaidGroups(raidGroups) + } + }) +} + +// BenchmarkGroupMessaging measures messaging performance +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), + CHANNEL_GROUP_CHAT, + ) + } + }) +} + +// BenchmarkGroupState measures group state operations +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() { + _ = group.GetID() + } + }) + }) + + b.Run("IsDisbanded", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.IsDisbanded() + } + }) + }) + + b.Run("GetCreatedTime", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.GetCreatedTime() + } + }) + }) + + b.Run("GetLastActivity", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = group.GetLastActivity() + } + }) + }) +} + +// 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) + 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("GetGroup", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + id := int32(rand.Intn(numGroups) + 1) + _ = ml.GetGroup(id) + } + }) + }) + + b.Run("AddGroup", func(b *testing.B) { + startID := int32(numGroups + 1) + var addedGroups []*Group + b.ResetTimer() + for i := 0; i < b.N; i++ { + group := createTestGroup(b, startID+int32(i), 3) + ml.AddGroup(group) + addedGroups = append(addedGroups, group) + } + // Cleanup added groups + for _, group := range addedGroups { + 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() + } + }) +} + +// BenchmarkManagerOperations measures manager performance +func BenchmarkManagerOperations(b *testing.B) { + config := GroupManagerConfig{ + MaxGroups: 1000, + InviteTimeout: 30 * time.Second, + UpdateInterval: 1 * time.Second, + BuffUpdateInterval: 5 * time.Second, + EnableStatistics: true, + } + + manager := NewManager(config, nil) + + // Pre-populate with groups + b.StopTimer() + for i := 0; i < 100; i++ { + leader := createTestEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) + manager.NewGroup(leader, nil, 0) + } + b.StartTimer() + + b.Run("NewGroup", func(b *testing.B) { + startID := int32(1000) + for i := 0; i < b.N; i++ { + leader := createTestEntity(startID+int32(i), fmt.Sprintf("BenchLeader%d", i), true) + _, err := manager.NewGroup(leader, nil, 0) + if err != nil { + b.Fatalf("Failed to create group: %v", err) + } + } + }) + + b.Run("GetGroup", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + groupID := int32(rand.Intn(100) + 1) + _ = manager.GetGroup(groupID) + } + }) + }) + + b.Run("IsGroupIDValid", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + groupID := int32(rand.Intn(100) + 1) + _ = manager.IsGroupIDValid(groupID) + } + }) + }) + + b.Run("GetGroupCount", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = manager.GetGroupCount() + } + }) + }) + + 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() { + _ = manager.GetStats() + } + }) + }) +} + +// BenchmarkInviteSystem measures invitation system performance +func BenchmarkInviteSystem(b *testing.B) { + config := GroupManagerConfig{ + 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) + member := createTestEntity(int32(i+1001), fmt.Sprintf("Member%d", i+1), true) + manager.AddInvite(leader, member) + } + }) + + b.Run("HasPendingInvite", func(b *testing.B) { + // Add some invites first + for i := 0; i < 100; i++ { + leader := createTestEntity(int32(i+1), fmt.Sprintf("TestLeader%d", i+1), true) + 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() { + member := createTestEntity(int32(rand.Intn(100)+2001), fmt.Sprintf("TestMember%d", rand.Intn(100)+1), true) + _ = manager.HasPendingInvite(member) + } + }) + }) + + 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) + member := createTestEntity(int32(i+4001), fmt.Sprintf("InviteMember%d", i+1), true) + _ = manager.Invite(leader, member) + } + }) + + b.Run("DeclineInvite", func(b *testing.B) { + // Add invites to decline + for i := 0; i < b.N; i++ { + leader := createTestEntity(int32(i+5001), fmt.Sprintf("DeclineLeader%d", i+1), true) + member := createTestEntity(int32(i+6001), fmt.Sprintf("DeclineMember%d", i+1), true) + manager.AddInvite(leader, member) + manager.DeclineInvite(member) + } + }) +} + +// BenchmarkConcurrentOperations measures concurrent access performance +func BenchmarkConcurrentOperations(b *testing.B) { + config := GroupManagerConfig{ + 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) + case 1: + _ = manager.GetGroupSize(groupID) + case 2: + _ = manager.IsGroupIDValid(groupID) + case 3: + member := createTestEntity(int32(rand.Intn(1000)+10000), "ConcurrentMember", true) + manager.AddGroupMember(groupID, member, false) + } + } + }) + }) + + b.Run("ConcurrentInviteOperations", func(b *testing.B) { + var counter int64 + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + 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) + case 1: + _ = manager.HasPendingInvite(member) + case 2: + manager.DeclineInvite(member) + } + } + }) + }) +} + +// BenchmarkMemoryAllocation measures memory allocation patterns +func BenchmarkMemoryAllocation(b *testing.B) { + b.Run("GroupAllocation", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + group := NewGroup(int32(i+1), nil, nil) + group.Disband() // Clean up background goroutine + } + }) + + b.Run("MasterListAllocation", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ml := NewMasterList() + _ = ml + } + }) + + b.Run("ManagerAllocation", func(b *testing.B) { + config := GroupManagerConfig{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + manager := NewManager(config, nil) + _ = manager + } + }) + + b.Run("GroupMemberInfoAllocation", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + gmi := &GroupMemberInfo{ + GroupID: int32(i + 1), + Name: fmt.Sprintf("Member%d", i+1), + Zone: "TestZone", + HPCurrent: 1000, + HPMax: 1000, + PowerCurrent: 500, + PowerMax: 500, + LevelCurrent: 50, + LevelMax: 80, + RaceID: 1, + ClassID: 1, + Leader: false, + IsClient: true, + JoinTime: time.Now(), + LastUpdate: time.Now(), + } + _ = gmi + } + }) +} + +// 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/entity_interface.go b/internal/groups/entities.go similarity index 55% rename from internal/groups/entity_interface.go rename to internal/groups/entities.go index 0ded381..113adb3 100644 --- a/internal/groups/entity_interface.go +++ b/internal/groups/entities.go @@ -1,7 +1,6 @@ package groups -// Entity is the interface for entities that can be part of groups -// This interface is implemented by Player, NPC, and Bot types +// Entity represents a game entity that can be part of a group type Entity interface { // Basic entity information GetID() int32 @@ -16,27 +15,20 @@ type Entity interface { GetPower() int32 GetTotalPower() int32 - // Entity type checks + // Entity types IsPlayer() bool - IsNPC() bool IsBot() bool + IsNPC() bool IsDead() bool - // Zone information + // World positioning GetZone() Zone - - // Distance calculation GetDistance(other Entity) float32 } -// Zone interface for zone information +// Zone represents a game zone type Zone interface { GetZoneID() int32 GetInstanceID() int32 GetZoneName() string -} - -// Spawn interface for distance calculations -type Spawn interface { - // Minimal spawn interface for distance calculations } \ No newline at end of file diff --git a/internal/groups/group.go b/internal/groups/group.go index 684703a..efafe27 100644 --- a/internal/groups/group.go +++ b/internal/groups/group.go @@ -2,27 +2,85 @@ package groups import ( "fmt" + "sync" "time" ) -// NewGroup creates a new group with the given ID and options -func NewGroup(id int32, options *GroupOptions) *Group { +// 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"` + + // 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:"-"` + wg sync.WaitGroup `json:"-"` + + // Database integration - embedded operations + db any `json:"-"` // Database connection + isNew bool `json:"-"` // Flag for new groups +} + +// New creates a new group +func New(db any) *Group { + group := &Group{ + GroupID: 0, // Will be set when saved + Options: DefaultGroupOptions(), + Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), + RaidGroups: make([]int32, 0), + CreatedTime: time.Now(), + LastActivity: time.Now(), + Disbanded: false, + messageQueue: make(chan *GroupMessage, 100), + updateQueue: make(chan *GroupUpdate, 100), + stopChan: make(chan struct{}), + db: db, + isNew: true, + } + + // Start background processing + group.wg.Add(1) + go group.processMessages() + + return group +} + +// NewGroup creates a new group with specified ID and options +func NewGroup(id int32, options *GroupOptions, db any) *Group { if options == nil { defaultOpts := DefaultGroupOptions() options = &defaultOpts } group := &Group{ - id: id, - options: *options, - members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), - raidGroups: make([]int32, 0), - createdTime: time.Now(), - lastActivity: time.Now(), - disbanded: false, + GroupID: id, + Options: *options, + Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), + RaidGroups: make([]int32, 0), + CreatedTime: time.Now(), + LastActivity: time.Now(), + Disbanded: false, messageQueue: make(chan *GroupMessage, 100), updateQueue: make(chan *GroupUpdate, 100), stopChan: make(chan struct{}), + db: db, + isNew: false, } // Start background processing @@ -32,9 +90,33 @@ func NewGroup(id int32, options *GroupOptions) *Group { return group } -// GetID returns the group ID +// GetID returns the group ID (implements Identifiable interface) func (g *Group) GetID() int32 { - return g.id + return g.GroupID +} + +// Save saves the group to the database +func (g *Group) Save() error { + // TODO: Implement database save logic + // This would require integration with the actual database system + return nil +} + +// Delete removes the group from the database +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 +} + +// Reload refreshes the group from the database +func (g *Group) Reload() error { + // TODO: Implement database reload logic + // This would require integration with the actual database system + return nil } // GetSize returns the number of members in the group @@ -42,7 +124,7 @@ func (g *Group) GetSize() int32 { g.membersMutex.RLock() defer g.membersMutex.RUnlock() - return int32(len(g.members)) + return int32(len(g.Members)) } // GetMembers returns a copy of the member list @@ -50,8 +132,8 @@ func (g *Group) GetMembers() []*GroupMemberInfo { g.membersMutex.RLock() defer g.membersMutex.RUnlock() - members := make([]*GroupMemberInfo, len(g.members)) - for i, member := range g.members { + members := make([]*GroupMemberInfo, len(g.Members)) + for i, member := range g.Members { members[i] = member.Copy() } @@ -65,7 +147,7 @@ func (g *Group) AddMember(member Entity, isLeader bool) error { } g.disbandMutex.RLock() - if g.disbanded { + if g.Disbanded { g.disbandMutex.RUnlock() return fmt.Errorf("group has been disbanded") } @@ -75,12 +157,12 @@ func (g *Group) AddMember(member Entity, isLeader bool) error { defer g.membersMutex.Unlock() // Check if group is full - if len(g.members) >= MAX_GROUP_SIZE { + if len(g.Members) >= MAX_GROUP_SIZE { return fmt.Errorf("group is full") } // Check if member is already in the group - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Member == member { return fmt.Errorf("member is already in the group") } @@ -88,7 +170,7 @@ func (g *Group) AddMember(member Entity, isLeader bool) error { // Create new group member info gmi := &GroupMemberInfo{ - GroupID: g.id, + GroupID: g.GroupID, Name: member.GetName(), Leader: isLeader, Member: member, @@ -114,7 +196,7 @@ func (g *Group) AddMember(member Entity, isLeader bool) error { } // Add to members list - g.members = append(g.members, gmi) + g.Members = append(g.Members, gmi) g.updateLastActivity() // Set group reference on the entity @@ -134,7 +216,7 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID zoneID, instanceID int32, peerAddress string, peerPort int16, isRaidLooter bool) error { g.disbandMutex.RLock() - if g.disbanded { + if g.Disbanded { g.disbandMutex.RUnlock() return fmt.Errorf("group has been disbanded") } @@ -144,13 +226,13 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID defer g.membersMutex.Unlock() // Check if group is full - if len(g.members) >= MAX_GROUP_SIZE { + if len(g.Members) >= MAX_GROUP_SIZE { return fmt.Errorf("group is full") } // Create new group member info for peer member gmi := &GroupMemberInfo{ - GroupID: g.id, + GroupID: g.GroupID, Name: name, Zone: zoneName, HPCurrent: hpCur, @@ -176,7 +258,7 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID } // Add to members list - g.members = append(g.members, gmi) + g.Members = append(g.Members, gmi) g.updateLastActivity() // Send group update @@ -195,14 +277,14 @@ func (g *Group) RemoveMember(member Entity) error { defer g.membersMutex.Unlock() // Find and remove the member - for i, gmi := range g.members { + for i, gmi := range g.Members { if gmi.Member == member { // Clear group reference on entity // TODO: Clear group member info on entity // member.SetGroupMemberInfo(nil) // Remove from slice - g.members = append(g.members[:i], g.members[i+1:]...) + g.Members = append(g.Members[:i], g.Members[i+1:]...) g.updateLastActivity() // If this was a bot, camp it @@ -227,11 +309,11 @@ func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) err defer g.membersMutex.Unlock() // Find and remove the member - for i, gmi := range g.members { + for i, gmi := range g.Members { if gmi.Name == name && gmi.IsClient == isClient { // Handle mentorship cleanup if isClient && charID > 0 { - for _, otherGmi := range g.members { + for _, otherGmi := range g.Members { if otherGmi.MentorTargetCharID == charID { otherGmi.MentorTargetCharID = 0 // TODO: Enable reset mentorship on client @@ -243,7 +325,7 @@ func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) err } // Remove from slice - g.members = append(g.members[:i], g.members[i+1:]...) + g.Members = append(g.Members[:i], g.Members[i+1:]...) g.updateLastActivity() // Send group update @@ -259,11 +341,11 @@ func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) err // Disband disbands the group and removes all members func (g *Group) Disband() { g.disbandMutex.Lock() - if g.disbanded { + if g.Disbanded { g.disbandMutex.Unlock() return } - g.disbanded = true + g.Disbanded = true g.disbandMutex.Unlock() // Stop background processing first to avoid deadlock @@ -275,11 +357,11 @@ func (g *Group) Disband() { // Clear raid groups g.raidGroupsMutex.Lock() - g.raidGroups = nil + g.RaidGroups = nil g.raidGroupsMutex.Unlock() // Remove all members - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Member != nil { // Clear group reference on entity // TODO: Clear group member info on entity @@ -310,7 +392,7 @@ func (g *Group) Disband() { } // Clear members list - g.members = nil + g.Members = nil } // SendGroupUpdate sends an update to all group members @@ -320,7 +402,7 @@ func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) { // sendGroupUpdate internal method to send group updates func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) { - update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.id) + update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.GroupID) update.ExcludeClient = excludeClient update.ForceRaidUpdate = forceRaidUpdate @@ -391,7 +473,7 @@ func (g *Group) MakeLeader(newLeader Entity) error { var newLeaderGMI *GroupMemberInfo // Find the new leader and update leadership - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Member == newLeader { newLeaderGMI = gmi gmi.Leader = true @@ -418,7 +500,7 @@ func (g *Group) GetLeaderName() string { g.membersMutex.RLock() defer g.membersMutex.RUnlock() - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Leader { return gmi.Name } @@ -446,7 +528,7 @@ func (g *Group) UpdateGroupMemberInfo(member Entity, groupMembersLocked bool) { } // Find the member and update their info - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Member == member { gmi.UpdateStats() g.updateLastActivity() @@ -460,11 +542,11 @@ func (g *Group) GetGroupMemberByPosition(seeker Entity, mappedPosition int32) En g.membersMutex.RLock() defer g.membersMutex.RUnlock() - if mappedPosition < 0 || int(mappedPosition) >= len(g.members) { + if mappedPosition < 0 || int(mappedPosition) >= len(g.Members) { return nil } - return g.members[mappedPosition].Member + return g.Members[mappedPosition].Member } // GetGroupOptions returns a copy of the group options @@ -472,7 +554,7 @@ func (g *Group) GetGroupOptions() GroupOptions { g.optionsMutex.RLock() defer g.optionsMutex.RUnlock() - return g.options.Copy() + return g.Options.Copy() } // SetGroupOptions sets new group options @@ -486,13 +568,13 @@ func (g *Group) SetGroupOptions(options *GroupOptions) error { } g.optionsMutex.Lock() - g.options = *options + g.Options = *options g.optionsMutex.Unlock() g.updateLastActivity() // Send group update for options change - update := NewGroupUpdate(GROUP_UPDATE_FLAG_OPTIONS, g.id) + update := NewGroupUpdate(GROUP_UPDATE_FLAG_OPTIONS, g.GroupID) update.Options = options select { @@ -509,13 +591,13 @@ func (g *Group) GetLastLooterIndex() int8 { g.optionsMutex.RLock() defer g.optionsMutex.RUnlock() - return g.options.LastLootedIndex + return g.Options.LastLootedIndex } // SetNextLooterIndex sets the next looter index func (g *Group) SetNextLooterIndex(newIndex int8) { g.optionsMutex.Lock() - g.options.LastLootedIndex = newIndex + g.Options.LastLootedIndex = newIndex g.optionsMutex.Unlock() g.updateLastActivity() @@ -528,12 +610,12 @@ func (g *Group) GetRaidGroups() []int32 { g.raidGroupsMutex.RLock() defer g.raidGroupsMutex.RUnlock() - if g.raidGroups == nil { + if g.RaidGroups == nil { return []int32{} } - groups := make([]int32, len(g.raidGroups)) - copy(groups, g.raidGroups) + groups := make([]int32, len(g.RaidGroups)) + copy(groups, g.RaidGroups) return groups } @@ -543,10 +625,10 @@ func (g *Group) ReplaceRaidGroups(groups []int32) { defer g.raidGroupsMutex.Unlock() if groups == nil { - g.raidGroups = make([]int32, 0) + g.RaidGroups = make([]int32, 0) } else { - g.raidGroups = make([]int32, len(groups)) - copy(g.raidGroups, groups) + g.RaidGroups = make([]int32, len(groups)) + copy(g.RaidGroups, groups) } g.updateLastActivity() @@ -557,7 +639,7 @@ func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool { g.raidGroupsMutex.RLock() defer g.raidGroupsMutex.RUnlock() - for _, id := range g.raidGroups { + for _, id := range g.RaidGroups { if id == groupID { return true } @@ -572,13 +654,13 @@ func (g *Group) AddGroupToRaid(groupID int32) { defer g.raidGroupsMutex.Unlock() // Check if already in raid - for _, id := range g.raidGroups { + for _, id := range g.RaidGroups { if id == groupID { return } } - g.raidGroups = append(g.raidGroups, groupID) + g.RaidGroups = append(g.RaidGroups, groupID) g.updateLastActivity() } @@ -587,9 +669,9 @@ func (g *Group) RemoveGroupFromRaid(groupID int32) { g.raidGroupsMutex.Lock() defer g.raidGroupsMutex.Unlock() - for i, id := range g.raidGroups { + for i, id := range g.RaidGroups { if id == groupID { - g.raidGroups = append(g.raidGroups[:i], g.raidGroups[i+1:]...) + g.RaidGroups = append(g.RaidGroups[:i], g.RaidGroups[i+1:]...) g.updateLastActivity() break } @@ -601,7 +683,7 @@ func (g *Group) IsGroupRaid() bool { g.raidGroupsMutex.RLock() defer g.raidGroupsMutex.RUnlock() - return len(g.raidGroups) > 0 + return len(g.RaidGroups) > 0 } // ClearGroupRaid clears all raid associations @@ -609,7 +691,7 @@ func (g *Group) ClearGroupRaid() { g.raidGroupsMutex.Lock() defer g.raidGroupsMutex.Unlock() - g.raidGroups = make([]int32, 0) + g.RaidGroups = make([]int32, 0) g.updateLastActivity() } @@ -618,26 +700,26 @@ func (g *Group) IsDisbanded() bool { g.disbandMutex.RLock() defer g.disbandMutex.RUnlock() - return g.disbanded + return g.Disbanded } // GetCreatedTime returns when the group was created func (g *Group) GetCreatedTime() time.Time { - return g.createdTime + return g.CreatedTime } // GetLastActivity returns the last activity time func (g *Group) GetLastActivity() time.Time { g.activityMutex.RLock() defer g.activityMutex.RUnlock() - return g.lastActivity + return g.LastActivity } // updateLastActivity updates the last activity timestamp func (g *Group) updateLastActivity() { g.activityMutex.Lock() defer g.activityMutex.Unlock() - g.lastActivity = time.Now() + g.LastActivity = time.Now() } // processMessages processes messages and updates in the background @@ -666,7 +748,7 @@ func (g *Group) handleMessage(msg *GroupMessage) { defer g.membersMutex.RUnlock() // Send message to all group members except the excluded client - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Client != nil && gmi.Client != msg.ExcludeClient { // TODO: Send message to client // This would require integration with the client system @@ -684,7 +766,7 @@ func (g *Group) handleUpdate(update *GroupUpdate) { defer g.membersMutex.RUnlock() // Send update to all group members except the excluded client - for _, gmi := range g.members { + for _, gmi := range g.Members { if gmi.Client != nil && gmi.Client != update.ExcludeClient { // TODO: Send update to client // This would require integration with the client system diff --git a/internal/groups/groups_test.go b/internal/groups/groups_test.go index cc2b588..32411aa 100644 --- a/internal/groups/groups_test.go +++ b/internal/groups/groups_test.go @@ -110,7 +110,7 @@ func TestGroupCreation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - group := NewGroup(tt.groupID, tt.options) + group := NewGroup(tt.groupID, tt.options, nil) if (group == nil) != tt.expectNil { t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil) @@ -133,7 +133,7 @@ func TestGroupCreation(t *testing.T) { // TestGroupMemberManagement tests adding and removing members func TestGroupMemberManagement(t *testing.T) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() leader := createMockEntity(1, "Leader", true) @@ -190,7 +190,7 @@ func TestGroupMemberManagement(t *testing.T) { // TestGroupLeadership tests leadership transfer func TestGroupLeadership(t *testing.T) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() leader := createMockEntity(1, "Leader", true) @@ -227,7 +227,7 @@ func TestGroupLeadership(t *testing.T) { // TestGroupOptions tests group options management func TestGroupOptions(t *testing.T) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() // Test default options @@ -257,7 +257,7 @@ func TestGroupOptions(t *testing.T) { // TestGroupRaidFunctionality tests raid-related functionality func TestGroupRaidFunctionality(t *testing.T) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() // Initially not a raid @@ -288,7 +288,7 @@ func TestGroupRaidFunctionality(t *testing.T) { // TestGroupConcurrency tests concurrent access to group operations func TestGroupConcurrency(t *testing.T) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() const numGoroutines = 100 @@ -425,7 +425,7 @@ func TestGroupManagerCreation(t *testing.T) { EnableStatistics: true, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() if manager == nil { @@ -451,7 +451,7 @@ func TestGroupManagerGroupOperations(t *testing.T) { EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() leader := createMockEntity(1, "Leader", true) @@ -520,7 +520,7 @@ func TestGroupManagerInvitations(t *testing.T) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, - InviteTimeout: 2 * time.Second, // Short timeout for testing + InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, @@ -528,7 +528,7 @@ func TestGroupManagerInvitations(t *testing.T) { EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() leader := createMockEntity(1, "Leader", true) @@ -571,8 +571,8 @@ func TestGroupManagerInvitations(t *testing.T) { member2 := createMockEntity(3, "Member2", true) manager.Invite(leader, member2) - // Wait for timeout - time.Sleep(3 * time.Second) + // 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, // but we're mainly testing that the invite was cleaned up) @@ -600,7 +600,7 @@ func TestGroupManagerConcurrency(t *testing.T) { EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() const numGoroutines = 50 @@ -726,7 +726,7 @@ func TestRaceConditions(t *testing.T) { EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() const numGoroutines = 100 @@ -794,13 +794,13 @@ func TestRaceConditions(t *testing.T) { func BenchmarkGroupOperations(b *testing.B) { b.Run("GroupCreation", func(b *testing.B) { for i := 0; i < b.N; i++ { - group := NewGroup(int32(i), nil) + group := NewGroup(int32(i), nil, nil) group.Disband() } }) b.Run("MemberAddition", func(b *testing.B) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() b.ResetTimer() @@ -812,7 +812,7 @@ func BenchmarkGroupOperations(b *testing.B) { }) b.Run("ConcurrentMemberAccess", func(b *testing.B) { - group := NewGroup(1, nil) + group := NewGroup(1, nil, nil) defer group.Disband() // Add some members @@ -841,7 +841,7 @@ func BenchmarkGroupOperations(b *testing.B) { EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } - manager := NewGroupManager(config) + manager := NewManager(config, nil) defer manager.Stop() // Create some groups diff --git a/internal/groups/manager.go b/internal/groups/manager.go index d2b0b1b..3a64b45 100644 --- a/internal/groups/manager.go +++ b/internal/groups/manager.go @@ -2,57 +2,120 @@ package groups import ( "fmt" + "sync" "time" ) -// NewGroupManager creates a new group manager with the given configuration -func NewGroupManager(config GroupManagerConfig) *GroupManager { - manager := &GroupManager{ - groups: make(map[int32]*Group), - nextGroupID: 1, - pendingInvites: make(map[string]*GroupInvite), - raidPendingInvites: make(map[string]*GroupInvite), - eventHandlers: make([]GroupEventHandler, 0), - config: config, - stopChan: make(chan struct{}), - } +// 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"` + + // Group ID generation + 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:"-"` + 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 +} +// New creates a new group manager +func NewManager(config GroupManagerConfig, db any) *Manager { + manager := &Manager{ + MasterList: NewMasterList(), + Config: config, + Stats: GroupManagerStats{}, + nextGroupID: 1, + PendingInvites: make(map[string]*GroupInvite), + RaidPendingInvites: make(map[string]*GroupInvite), + EventHandlers: make([]GroupEventHandler, 0), + stopChan: make(chan struct{}), + db: db, + isNew: true, + } + return manager } +// Save saves the manager state to the database +func (m *Manager) Save() error { + // TODO: Implement database save logic + return nil +} + +// Delete removes the manager from the database +func (m *Manager) Delete() error { + // Stop the manager first + m.Stop() + + // TODO: Implement database delete logic + return nil +} + +// Reload refreshes the manager from the database +func (m *Manager) Reload() error { + // TODO: Implement database reload logic + return nil +} + // Start starts the group manager background processes -func (gm *GroupManager) Start() error { +func (m *Manager) Start() error { // Start background processes - if gm.config.UpdateInterval > 0 { - gm.wg.Add(1) - go gm.updateGroupsLoop() + if m.Config.UpdateInterval > 0 { + m.wg.Add(1) + go m.updateGroupsLoop() } - if gm.config.BuffUpdateInterval > 0 { - gm.wg.Add(1) - go gm.updateBuffsLoop() + if m.Config.BuffUpdateInterval > 0 { + m.wg.Add(1) + go m.updateBuffsLoop() } - gm.wg.Add(1) - go gm.cleanupExpiredInvitesLoop() + m.wg.Add(1) + go m.cleanupExpiredInvitesLoop() - if gm.config.EnableStatistics { - gm.wg.Add(1) - go gm.updateStatsLoop() + if m.Config.EnableStatistics { + m.wg.Add(1) + go m.updateStatsLoop() } return nil } // Stop stops the group manager and all background processes -func (gm *GroupManager) Stop() error { - close(gm.stopChan) - gm.wg.Wait() +func (m *Manager) Stop() error { + close(m.stopChan) + m.wg.Wait() return nil } // NewGroup creates a new group with the given leader and options -func (gm *GroupManager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) { +func (m *Manager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) { if leader == nil { return 0, fmt.Errorf("leader cannot be nil") } @@ -61,19 +124,16 @@ func (gm *GroupManager) NewGroup(leader Entity, options *GroupOptions, overrideG if overrideGroupID > 0 { groupID = overrideGroupID } else { - groupID = gm.generateNextGroupID() + groupID = m.generateNextGroupID() } // Check if group ID already exists - gm.groupsMutex.RLock() - if _, exists := gm.groups[groupID]; exists && overrideGroupID == 0 { - gm.groupsMutex.RUnlock() + if m.MasterList.GetGroup(groupID) != nil && overrideGroupID == 0 { return 0, fmt.Errorf("group ID %d already exists", groupID) } - gm.groupsMutex.RUnlock() // Create new group - group := NewGroup(groupID, options) + group := NewGroup(groupID, options, m.db) // Add leader to the group if err := group.AddMember(leader, true); err != nil { @@ -82,62 +142,57 @@ func (gm *GroupManager) NewGroup(leader Entity, options *GroupOptions, overrideG } // Add group to manager - gm.groupsMutex.Lock() - gm.groups[groupID] = group - gm.groupsMutex.Unlock() + if !m.MasterList.AddGroup(group) { + group.Disband() + return 0, fmt.Errorf("failed to add group to master list") + } // Update statistics - gm.updateStatsForNewGroup() + m.updateStatsForNewGroup() // Fire event - gm.fireGroupCreatedEvent(group, leader) + m.fireGroupCreatedEvent(group, leader) return groupID, nil } // RemoveGroup removes a group from the manager -func (gm *GroupManager) RemoveGroup(groupID int32) error { - gm.groupsMutex.Lock() - group, exists := gm.groups[groupID] - if !exists { - gm.groupsMutex.Unlock() +func (m *Manager) RemoveGroup(groupID int32) error { + group := m.MasterList.GetGroup(groupID) + if group == nil { return fmt.Errorf("group %d not found", groupID) } - delete(gm.groups, groupID) - gm.groupsMutex.Unlock() // 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) + } // Update statistics - gm.updateStatsForRemovedGroup() + m.updateStatsForRemovedGroup() // Fire event - gm.fireGroupDisbandedEvent(group) + m.fireGroupDisbandedEvent(group) return nil } // GetGroup returns a group by ID -func (gm *GroupManager) GetGroup(groupID int32) *Group { - gm.groupsMutex.RLock() - defer gm.groupsMutex.RUnlock() - - return gm.groups[groupID] +func (m *Manager) GetGroup(groupID int32) *Group { + return m.MasterList.GetGroup(groupID) } // IsGroupIDValid checks if a group ID is valid and exists -func (gm *GroupManager) IsGroupIDValid(groupID int32) bool { - gm.groupsMutex.RLock() - defer gm.groupsMutex.RUnlock() - - _, exists := gm.groups[groupID] - return exists +func (m *Manager) IsGroupIDValid(groupID int32) bool { + return m.MasterList.GetGroup(groupID) != nil } // AddGroupMember adds a member to an existing group -func (gm *GroupManager) AddGroupMember(groupID int32, member Entity, isLeader bool) error { - group := gm.GetGroup(groupID) +func (m *Manager) AddGroupMember(groupID int32, member Entity, isLeader bool) error { + group := m.GetGroup(groupID) if group == nil { return fmt.Errorf("group %d not found", groupID) } @@ -146,8 +201,8 @@ func (gm *GroupManager) AddGroupMember(groupID int32, member Entity, isLeader bo } // AddGroupMemberFromPeer adds a member from a peer server to an existing group -func (gm *GroupManager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error { - group := gm.GetGroup(groupID) +func (m *Manager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error { + group := m.GetGroup(groupID) if group == nil { return fmt.Errorf("group %d not found", groupID) } @@ -162,8 +217,8 @@ func (gm *GroupManager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberI } // RemoveGroupMember removes a member from a group -func (gm *GroupManager) RemoveGroupMember(groupID int32, member Entity) error { - group := gm.GetGroup(groupID) +func (m *Manager) RemoveGroupMember(groupID int32, member Entity) error { + group := m.GetGroup(groupID) if group == nil { return fmt.Errorf("group %d not found", groupID) } @@ -175,15 +230,15 @@ func (gm *GroupManager) RemoveGroupMember(groupID int32, member Entity) error { // If group is now empty, remove it if group.GetSize() == 0 { - gm.RemoveGroup(groupID) + m.RemoveGroup(groupID) } return nil } // RemoveGroupMemberByName removes a member by name from a group -func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error { - group := gm.GetGroup(groupID) +func (m *Manager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error { + group := m.GetGroup(groupID) if group == nil { return fmt.Errorf("group %d not found", groupID) } @@ -195,24 +250,22 @@ func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isCl // If group is now empty, remove it if group.GetSize() == 0 { - gm.RemoveGroup(groupID) + m.RemoveGroup(groupID) } return nil } // SendGroupUpdate sends an update to all members of a group -func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { - group := gm.GetGroup(groupID) +func (m *Manager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { + group := m.GetGroup(groupID) if group != nil { group.SendGroupUpdate(excludeClient, forceRaidUpdate) } } -// Group invitation handling - // Invite handles inviting a player to a group -func (gm *GroupManager) Invite(leader Entity, member Entity) int8 { +func (m *Manager) Invite(leader Entity, member Entity) int8 { if leader == nil || member == nil { return GROUP_INVITE_TARGET_NOT_FOUND } @@ -224,7 +277,7 @@ func (gm *GroupManager) Invite(leader Entity, member Entity) int8 { // Check if member already has an invite inviteKey := member.GetName() - if gm.hasPendingInvite(inviteKey) != "" { + if m.hasPendingInvite(inviteKey) != "" { return GROUP_INVITE_ALREADY_HAS_INVITE } @@ -235,23 +288,23 @@ func (gm *GroupManager) Invite(leader Entity, member Entity) int8 { // } // Add the invite - if !gm.addInvite(leader, member) { + if !m.addInvite(leader, member) { return GROUP_INVITE_PERMISSION_DENIED } // Fire event - gm.fireGroupInviteSentEvent(leader, member) + m.fireGroupInviteSentEvent(leader, member) return GROUP_INVITE_SUCCESS } // AddInvite adds a group invitation -func (gm *GroupManager) AddInvite(leader Entity, member Entity) bool { - return gm.addInvite(leader, member) +func (m *Manager) AddInvite(leader Entity, member Entity) bool { + return m.addInvite(leader, member) } // addInvite internal method to add an invitation -func (gm *GroupManager) addInvite(leader Entity, member Entity) bool { +func (m *Manager) addInvite(leader Entity, member Entity) bool { if leader == nil || member == nil { return false } @@ -265,45 +318,45 @@ func (gm *GroupManager) addInvite(leader Entity, member Entity) bool { GroupID: 0, // Will be set when group is created IsRaidInvite: false, CreatedTime: time.Now(), - ExpiresTime: time.Now().Add(gm.config.InviteTimeout), + ExpiresTime: time.Now().Add(m.Config.InviteTimeout), } - gm.invitesMutex.Lock() - gm.pendingInvites[inviteKey] = invite - gm.invitesMutex.Unlock() + m.invitesMutex.Lock() + m.PendingInvites[inviteKey] = invite + m.invitesMutex.Unlock() // Update statistics - gm.updateStatsForInvite() + m.updateStatsForInvite() return true } // AcceptInvite handles accepting of a group invite -func (gm *GroupManager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 { +func (m *Manager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 { if member == nil { return GROUP_INVITE_TARGET_NOT_FOUND } inviteKey := member.GetName() - gm.invitesMutex.Lock() - invite, exists := gm.pendingInvites[inviteKey] + m.invitesMutex.Lock() + invite, exists := m.PendingInvites[inviteKey] if !exists { - gm.invitesMutex.Unlock() + m.invitesMutex.Unlock() return GROUP_INVITE_TARGET_NOT_FOUND } // Check if invite has expired if invite.IsExpired() { - delete(gm.pendingInvites, inviteKey) - gm.invitesMutex.Unlock() - gm.updateStatsForExpiredInvite() + delete(m.PendingInvites, inviteKey) + m.invitesMutex.Unlock() + m.updateStatsForExpiredInvite() return GROUP_INVITE_DECLINED } // Remove the invite - delete(gm.pendingInvites, inviteKey) - gm.invitesMutex.Unlock() + delete(m.PendingInvites, inviteKey) + m.invitesMutex.Unlock() if !autoAddGroup { return GROUP_INVITE_SUCCESS @@ -333,9 +386,9 @@ func (gm *GroupManager) AcceptInvite(member Entity, groupOverrideID *int32, auto // Create new group with leader var err error if groupID != 0 { - groupID, err = gm.NewGroup(leader, nil, groupID) + groupID, err = m.NewGroup(leader, nil, groupID) } else { - groupID, err = gm.NewGroup(leader, nil, 0) + groupID, err = m.NewGroup(leader, nil, 0) } if err != nil { return GROUP_INVITE_PERMISSION_DENIED @@ -345,75 +398,75 @@ func (gm *GroupManager) AcceptInvite(member Entity, groupOverrideID *int32, auto } // Add member to the group - if err := gm.AddGroupMember(groupID, member, false); err != nil { + if err := m.AddGroupMember(groupID, member, false); err != nil { return GROUP_INVITE_GROUP_FULL } // Update statistics - gm.updateStatsForAcceptedInvite() + m.updateStatsForAcceptedInvite() // Fire event - gm.fireGroupInviteAcceptedEvent(leader, member, groupID) + m.fireGroupInviteAcceptedEvent(leader, member, groupID) return GROUP_INVITE_SUCCESS } // DeclineInvite handles declining of a group invite -func (gm *GroupManager) DeclineInvite(member Entity) { +func (m *Manager) DeclineInvite(member Entity) { if member == nil { return } inviteKey := member.GetName() - gm.invitesMutex.Lock() - _, exists := gm.pendingInvites[inviteKey] + m.invitesMutex.Lock() + _, exists := m.PendingInvites[inviteKey] if exists { - delete(gm.pendingInvites, inviteKey) + delete(m.PendingInvites, inviteKey) } - gm.invitesMutex.Unlock() + m.invitesMutex.Unlock() if exists { // Update statistics - gm.updateStatsForDeclinedInvite() + m.updateStatsForDeclinedInvite() // Fire event var leader Entity // TODO: Find leader entity by name // leader = world.GetPlayerByName(invite.InviterName) - gm.fireGroupInviteDeclinedEvent(leader, member) + m.fireGroupInviteDeclinedEvent(leader, member) } } // ClearPendingInvite clears a pending invite for a member -func (gm *GroupManager) ClearPendingInvite(member Entity) { +func (m *Manager) ClearPendingInvite(member Entity) { if member == nil { return } inviteKey := member.GetName() - gm.invitesMutex.Lock() - delete(gm.pendingInvites, inviteKey) - gm.invitesMutex.Unlock() + m.invitesMutex.Lock() + delete(m.PendingInvites, inviteKey) + m.invitesMutex.Unlock() } // HasPendingInvite checks if a member has a pending invite and returns the inviter name -func (gm *GroupManager) HasPendingInvite(member Entity) string { +func (m *Manager) HasPendingInvite(member Entity) string { if member == nil { return "" } inviteKey := member.GetName() - return gm.hasPendingInvite(inviteKey) + return m.hasPendingInvite(inviteKey) } // hasPendingInvite internal method to check for pending invites -func (gm *GroupManager) hasPendingInvite(inviteKey string) string { - gm.invitesMutex.RLock() - defer gm.invitesMutex.RUnlock() +func (m *Manager) hasPendingInvite(inviteKey string) string { + m.invitesMutex.RLock() + defer m.invitesMutex.RUnlock() - if invite, exists := gm.pendingInvites[inviteKey]; exists { + if invite, exists := m.PendingInvites[inviteKey]; exists { if !invite.IsExpired() { return invite.InviterName } @@ -422,561 +475,36 @@ func (gm *GroupManager) hasPendingInvite(inviteKey string) string { return "" } -// Group utility methods - -// GetGroupSize returns the size of a group -func (gm *GroupManager) GetGroupSize(groupID int32) int32 { - group := gm.GetGroup(groupID) - if group == nil { - return 0 - } - return group.GetSize() -} - -// IsInGroup checks if an entity is in a specific group -func (gm *GroupManager) IsInGroup(groupID int32, member Entity) bool { - group := gm.GetGroup(groupID) - if group == nil || member == nil { - return false - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Member == member { - return true - } - } - - return false -} - -// IsPlayerInGroup checks if a player with the given character ID is in a group -func (gm *GroupManager) IsPlayerInGroup(groupID int32, charID int32) Entity { - group := gm.GetGroup(groupID) - if group == nil { - return nil - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.IsClient && gmi.Member != nil { - // TODO: Check character ID - // if gmi.Member.GetCharacterID() == charID { - // return gmi.Member - // } - } - } - - return nil -} - -// IsSpawnInGroup checks if a spawn with the given name is in a group -func (gm *GroupManager) IsSpawnInGroup(groupID int32, name string) bool { - group := gm.GetGroup(groupID) - if group == nil { - return false - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Name == name { - return true - } - } - - return false -} - -// GetGroupLeader returns the leader of a group -func (gm *GroupManager) GetGroupLeader(groupID int32) Entity { - group := gm.GetGroup(groupID) - if group == nil { - return nil - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Leader { - return gmi.Member - } - } - - return nil -} - -// MakeLeader changes the leader of a group -func (gm *GroupManager) MakeLeader(groupID int32, newLeader Entity) bool { - group := gm.GetGroup(groupID) - if group == nil { - return false - } - - err := group.MakeLeader(newLeader) - return err == nil -} - -// Group messaging - -// SimpleGroupMessage sends a simple message to all members of a group -func (gm *GroupManager) SimpleGroupMessage(groupID int32, message string) { - group := gm.GetGroup(groupID) - if group != nil { - group.SimpleGroupMessage(message) - } -} - -// SendGroupMessage sends a formatted message to all members of a group -func (gm *GroupManager) SendGroupMessage(groupID int32, msgType int8, message string) { - group := gm.GetGroup(groupID) - if group != nil { - group.SendGroupMessage(msgType, message) - } -} - -// GroupMessage sends a message to all members of a group (alias for SimpleGroupMessage) -func (gm *GroupManager) GroupMessage(groupID int32, message string) { - gm.SimpleGroupMessage(groupID, message) -} - -// GroupChatMessage sends a chat message from a member to the group -func (gm *GroupManager) GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16) { - group := gm.GetGroup(groupID) - if group != nil { - group.GroupChatMessage(from, language, message, channel) - } -} - -// GroupChatMessageFromName sends a chat message from a named sender to the group -func (gm *GroupManager) GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) { - group := gm.GetGroup(groupID) - if group != nil { - group.GroupChatMessageFromName(fromName, language, message, channel) - } -} - -// SendGroupChatMessage sends a formatted chat message to the group -func (gm *GroupManager) SendGroupChatMessage(groupID int32, channel int16, message string) { - gm.GroupChatMessageFromName(groupID, "System", 0, message, channel) -} - -// Raid functionality - -// ClearGroupRaid clears raid associations for a group -func (gm *GroupManager) ClearGroupRaid(groupID int32) { - group := gm.GetGroup(groupID) - if group != nil { - group.ClearGroupRaid() - } -} - -// RemoveGroupFromRaid removes a group from a raid -func (gm *GroupManager) RemoveGroupFromRaid(groupID, targetGroupID int32) { - group := gm.GetGroup(groupID) - if group != nil { - group.RemoveGroupFromRaid(targetGroupID) - } -} - -// IsInRaidGroup checks if two groups are in the same raid -func (gm *GroupManager) IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool { - group := gm.GetGroup(groupID) - if group == nil { - return false - } - return group.IsInRaidGroup(targetGroupID, isLeaderGroup) -} - -// GetRaidGroups returns the raid groups for a specific group -func (gm *GroupManager) GetRaidGroups(groupID int32) []int32 { - group := gm.GetGroup(groupID) - if group == nil { - return []int32{} - } - return group.GetRaidGroups() -} - -// ReplaceRaidGroups replaces the raid groups for a specific group -func (gm *GroupManager) ReplaceRaidGroups(groupID int32, newGroups []int32) { - group := gm.GetGroup(groupID) - if group != nil { - group.ReplaceRaidGroups(newGroups) - } -} - -// Group options - -// GetDefaultGroupOptions returns the default group options for a group -func (gm *GroupManager) GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) { - group := gm.GetGroup(groupID) - if group == nil { - return GroupOptions{}, false - } - return group.GetGroupOptions(), true -} - -// SetGroupOptions sets group options for a specific group -func (gm *GroupManager) SetGroupOptions(groupID int32, options *GroupOptions) error { - group := gm.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - return group.SetGroupOptions(options) -} - -// Utility methods - // generateNextGroupID generates the next available group ID -func (gm *GroupManager) generateNextGroupID() int32 { - gm.nextGroupIDMutex.Lock() - defer gm.nextGroupIDMutex.Unlock() +func (m *Manager) generateNextGroupID() int32 { + m.nextGroupIDMutex.Lock() + defer m.nextGroupIDMutex.Unlock() - id := gm.nextGroupID - gm.nextGroupID++ + id := m.nextGroupID + m.nextGroupID++ // Handle overflow - if gm.nextGroupID <= 0 { - gm.nextGroupID = 1 + if m.nextGroupID <= 0 { + m.nextGroupID = 1 } return id } // GetGroupCount returns the number of active groups -func (gm *GroupManager) GetGroupCount() int32 { - gm.groupsMutex.RLock() - defer gm.groupsMutex.RUnlock() - - return int32(len(gm.groups)) +func (m *Manager) GetGroupCount() int32 { + return int32(m.MasterList.Size()) } // GetAllGroups returns all active groups -func (gm *GroupManager) GetAllGroups() []*Group { - gm.groupsMutex.RLock() - defer gm.groupsMutex.RUnlock() - - groups := make([]*Group, 0, len(gm.groups)) - for _, group := range gm.groups { - groups = append(groups, group) - } - - return groups -} - -// Background processing loops - -// updateGroupsLoop periodically updates all groups -func (gm *GroupManager) updateGroupsLoop() { - defer gm.wg.Done() - - ticker := time.NewTicker(gm.config.UpdateInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - gm.processGroupUpdates() - case <-gm.stopChan: - return - } - } -} - -// updateBuffsLoop periodically updates group buffs -func (gm *GroupManager) updateBuffsLoop() { - defer gm.wg.Done() - - ticker := time.NewTicker(gm.config.BuffUpdateInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - gm.updateGroupBuffs() - case <-gm.stopChan: - return - } - } -} - -// cleanupExpiredInvitesLoop periodically cleans up expired invites -func (gm *GroupManager) cleanupExpiredInvitesLoop() { - defer gm.wg.Done() - - ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds - defer ticker.Stop() - - for { - select { - case <-ticker.C: - gm.cleanupExpiredInvites() - case <-gm.stopChan: - return - } - } -} - -// updateStatsLoop periodically updates statistics -func (gm *GroupManager) updateStatsLoop() { - defer gm.wg.Done() - - ticker := time.NewTicker(1 * time.Minute) // Update stats every minute - defer ticker.Stop() - - for { - select { - case <-ticker.C: - gm.updateStatistics() - case <-gm.stopChan: - return - } - } -} - -// processGroupUpdates processes periodic group updates -func (gm *GroupManager) processGroupUpdates() { - groups := gm.GetAllGroups() - - for _, group := range groups { - if !group.IsDisbanded() { - // Update member information - members := group.GetMembers() - for _, gmi := range members { - if gmi.Member != nil { - group.UpdateGroupMemberInfo(gmi.Member, false) - } - } - } - } -} - -// updateGroupBuffs updates group buffs for all groups -func (gm *GroupManager) updateGroupBuffs() { - // TODO: Implement group buff updates - // This would require integration with the spell/buff system -} - -// cleanupExpiredInvites removes expired invitations -func (gm *GroupManager) cleanupExpiredInvites() { - gm.invitesMutex.Lock() - defer gm.invitesMutex.Unlock() - - now := time.Now() - expiredCount := 0 - - // Clean up regular invites - for key, invite := range gm.pendingInvites { - if now.After(invite.ExpiresTime) { - delete(gm.pendingInvites, key) - expiredCount++ - } - } - - // Clean up raid invites - for key, invite := range gm.raidPendingInvites { - if now.After(invite.ExpiresTime) { - delete(gm.raidPendingInvites, key) - expiredCount++ - } - } - - // Update statistics - if expiredCount > 0 { - gm.statsMutex.Lock() - gm.stats.ExpiredInvites += int64(expiredCount) - gm.statsMutex.Unlock() - } -} - -// updateStatistics updates manager statistics -func (gm *GroupManager) updateStatistics() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.groupsMutex.RLock() - activeGroups := int64(len(gm.groups)) - - var totalMembers int64 - var raidCount int64 - - for _, group := range gm.groups { - totalMembers += int64(group.GetSize()) - if group.IsGroupRaid() { - raidCount++ - } - } - gm.groupsMutex.RUnlock() - - gm.stats.ActiveGroups = activeGroups - gm.stats.ActiveRaids = raidCount - - if activeGroups > 0 { - gm.stats.AverageGroupSize = float64(totalMembers) / float64(activeGroups) - } else { - gm.stats.AverageGroupSize = 0 - } - - gm.stats.LastStatsUpdate = time.Now() -} - -// Statistics update methods - -// updateStatsForNewGroup updates statistics when a new group is created -func (gm *GroupManager) updateStatsForNewGroup() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.stats.TotalGroups++ -} - -// updateStatsForRemovedGroup updates statistics when a group is removed -func (gm *GroupManager) updateStatsForRemovedGroup() { - // Statistics are primarily tracked in updateStatistics() -} - -// updateStatsForInvite updates statistics when an invite is sent -func (gm *GroupManager) updateStatsForInvite() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.stats.TotalInvites++ -} - -// updateStatsForAcceptedInvite updates statistics when an invite is accepted -func (gm *GroupManager) updateStatsForAcceptedInvite() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.stats.AcceptedInvites++ -} - -// updateStatsForDeclinedInvite updates statistics when an invite is declined -func (gm *GroupManager) updateStatsForDeclinedInvite() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.stats.DeclinedInvites++ -} - -// updateStatsForExpiredInvite updates statistics when an invite expires -func (gm *GroupManager) updateStatsForExpiredInvite() { - if !gm.config.EnableStatistics { - return - } - - gm.statsMutex.Lock() - defer gm.statsMutex.Unlock() - - gm.stats.ExpiredInvites++ +func (m *Manager) GetAllGroups() []*Group { + return m.MasterList.GetAllGroups() } // GetStats returns current manager statistics -func (gm *GroupManager) GetStats() GroupManagerStats { - gm.statsMutex.RLock() - defer gm.statsMutex.RUnlock() +func (m *Manager) GetStats() GroupManagerStats { + m.statsMutex.RLock() + defer m.statsMutex.RUnlock() - return gm.stats -} - -// Event system integration - -// AddEventHandler adds an event handler -func (gm *GroupManager) AddEventHandler(handler GroupEventHandler) { - gm.eventHandlersMutex.Lock() - defer gm.eventHandlersMutex.Unlock() - - gm.eventHandlers = append(gm.eventHandlers, handler) -} - -// Integration interfaces - -// SetDatabase sets the database interface -func (gm *GroupManager) SetDatabase(db GroupDatabase) { - gm.database = db -} - -// SetPacketHandler sets the packet handler interface -func (gm *GroupManager) SetPacketHandler(handler GroupPacketHandler) { - gm.packetHandler = handler -} - -// SetValidator sets the validator interface -func (gm *GroupManager) SetValidator(validator GroupValidator) { - gm.validator = validator -} - -// SetNotifier sets the notifier interface -func (gm *GroupManager) SetNotifier(notifier GroupNotifier) { - gm.notifier = notifier -} - -// Event firing methods - -// fireGroupCreatedEvent fires a group created event -func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader Entity) { - gm.eventHandlersMutex.RLock() - defer gm.eventHandlersMutex.RUnlock() - - for _, handler := range gm.eventHandlers { - go handler.OnGroupCreated(group, leader) - } -} - -// fireGroupDisbandedEvent fires a group disbanded event -func (gm *GroupManager) fireGroupDisbandedEvent(group *Group) { - gm.eventHandlersMutex.RLock() - defer gm.eventHandlersMutex.RUnlock() - - for _, handler := range gm.eventHandlers { - go handler.OnGroupDisbanded(group) - } -} - -// fireGroupInviteSentEvent fires a group invite sent event -func (gm *GroupManager) fireGroupInviteSentEvent(leader, member Entity) { - gm.eventHandlersMutex.RLock() - defer gm.eventHandlersMutex.RUnlock() - - for _, handler := range gm.eventHandlers { - go handler.OnGroupInviteSent(leader, member) - } -} - -// fireGroupInviteAcceptedEvent fires a group invite accepted event -func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member Entity, groupID int32) { - gm.eventHandlersMutex.RLock() - defer gm.eventHandlersMutex.RUnlock() - - for _, handler := range gm.eventHandlers { - go handler.OnGroupInviteAccepted(leader, member, groupID) - } -} - -// fireGroupInviteDeclinedEvent fires a group invite declined event -func (gm *GroupManager) fireGroupInviteDeclinedEvent(leader, member Entity) { - gm.eventHandlersMutex.RLock() - defer gm.eventHandlersMutex.RUnlock() - - for _, handler := range gm.eventHandlers { - go handler.OnGroupInviteDeclined(leader, member) - } -} + return m.Stats +} \ No newline at end of file diff --git a/internal/groups/manager_methods.go b/internal/groups/manager_methods.go new file mode 100644 index 0000000..1ceac6d --- /dev/null +++ b/internal/groups/manager_methods.go @@ -0,0 +1,517 @@ +package groups + +import ( + "fmt" + "time" +) + +// Group utility methods + +// GetGroupSize returns the size of a group +func (m *Manager) GetGroupSize(groupID int32) int32 { + group := m.GetGroup(groupID) + if group == nil { + return 0 + } + return group.GetSize() +} + +// IsInGroup checks if an entity is in a specific group +func (m *Manager) IsInGroup(groupID int32, member Entity) bool { + group := m.GetGroup(groupID) + if group == nil || member == nil { + return false + } + + members := group.GetMembers() + for _, gmi := range members { + if gmi.Member == member { + return true + } + } + + return false +} + +// IsPlayerInGroup checks if a player with the given character ID is in a group +func (m *Manager) IsPlayerInGroup(groupID int32, charID int32) Entity { + group := m.GetGroup(groupID) + if group == nil { + return nil + } + + members := group.GetMembers() + for _, gmi := range members { + if gmi.IsClient && gmi.Member != nil { + // TODO: Check character ID + // if gmi.Member.GetCharacterID() == charID { + // return gmi.Member + // } + } + } + + return nil +} + +// IsSpawnInGroup checks if a spawn with the given name is in a group +func (m *Manager) IsSpawnInGroup(groupID int32, name string) bool { + group := m.GetGroup(groupID) + if group == nil { + return false + } + + members := group.GetMembers() + for _, gmi := range members { + if gmi.Name == name { + return true + } + } + + return false +} + +// GetGroupLeader returns the leader of a group +func (m *Manager) GetGroupLeader(groupID int32) Entity { + group := m.GetGroup(groupID) + if group == nil { + return nil + } + + members := group.GetMembers() + for _, gmi := range members { + if gmi.Leader { + return gmi.Member + } + } + + return nil +} + +// MakeLeader changes the leader of a group +func (m *Manager) MakeLeader(groupID int32, newLeader Entity) bool { + group := m.GetGroup(groupID) + if group == nil { + return false + } + + err := group.MakeLeader(newLeader) + return err == nil +} + +// Group messaging + +// SimpleGroupMessage sends a simple message to all members of a group +func (m *Manager) SimpleGroupMessage(groupID int32, message string) { + group := m.GetGroup(groupID) + if group != nil { + group.SimpleGroupMessage(message) + } +} + +// SendGroupMessage sends a formatted message to all members of a group +func (m *Manager) SendGroupMessage(groupID int32, msgType int8, message string) { + group := m.GetGroup(groupID) + if group != nil { + group.SendGroupMessage(msgType, message) + } +} + +// GroupMessage sends a message to all members of a group (alias for SimpleGroupMessage) +func (m *Manager) GroupMessage(groupID int32, message string) { + m.SimpleGroupMessage(groupID, message) +} + +// GroupChatMessage sends a chat message from a member to the group +func (m *Manager) GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16) { + group := m.GetGroup(groupID) + if group != nil { + group.GroupChatMessage(from, language, message, channel) + } +} + +// GroupChatMessageFromName sends a chat message from a named sender to the group +func (m *Manager) GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) { + group := m.GetGroup(groupID) + if group != nil { + group.GroupChatMessageFromName(fromName, language, message, channel) + } +} + +// SendGroupChatMessage sends a formatted chat message to the group +func (m *Manager) SendGroupChatMessage(groupID int32, channel int16, message string) { + m.GroupChatMessageFromName(groupID, "System", 0, message, channel) +} + +// Raid functionality + +// ClearGroupRaid clears raid associations for a group +func (m *Manager) ClearGroupRaid(groupID int32) { + group := m.GetGroup(groupID) + if group != nil { + group.ClearGroupRaid() + } +} + +// RemoveGroupFromRaid removes a group from a raid +func (m *Manager) RemoveGroupFromRaid(groupID, targetGroupID int32) { + group := m.GetGroup(groupID) + if group != nil { + group.RemoveGroupFromRaid(targetGroupID) + } +} + +// IsInRaidGroup checks if two groups are in the same raid +func (m *Manager) IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool { + group := m.GetGroup(groupID) + if group == nil { + return false + } + return group.IsInRaidGroup(targetGroupID, isLeaderGroup) +} + +// GetRaidGroups returns the raid groups for a specific group +func (m *Manager) GetRaidGroups(groupID int32) []int32 { + group := m.GetGroup(groupID) + if group == nil { + return []int32{} + } + return group.GetRaidGroups() +} + +// ReplaceRaidGroups replaces the raid groups for a specific group +func (m *Manager) ReplaceRaidGroups(groupID int32, newGroups []int32) { + group := m.GetGroup(groupID) + if group != nil { + group.ReplaceRaidGroups(newGroups) + } +} + +// Group options + +// GetDefaultGroupOptions returns the default group options for a group +func (m *Manager) GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) { + group := m.GetGroup(groupID) + if group == nil { + return GroupOptions{}, false + } + return group.GetGroupOptions(), true +} + +// SetGroupOptions sets group options for a specific group +func (m *Manager) SetGroupOptions(groupID int32, options *GroupOptions) error { + group := m.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group %d not found", groupID) + } + return group.SetGroupOptions(options) +} + +// Background processing loops + +// updateGroupsLoop periodically updates all groups +func (m *Manager) updateGroupsLoop() { + defer m.wg.Done() + + ticker := time.NewTicker(m.Config.UpdateInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.processGroupUpdates() + case <-m.stopChan: + return + } + } +} + +// updateBuffsLoop periodically updates group buffs +func (m *Manager) updateBuffsLoop() { + defer m.wg.Done() + + ticker := time.NewTicker(m.Config.BuffUpdateInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.updateGroupBuffs() + case <-m.stopChan: + return + } + } +} + +// cleanupExpiredInvitesLoop periodically cleans up expired invites +func (m *Manager) cleanupExpiredInvitesLoop() { + defer m.wg.Done() + + ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.cleanupExpiredInvites() + case <-m.stopChan: + return + } + } +} + +// updateStatsLoop periodically updates statistics +func (m *Manager) updateStatsLoop() { + defer m.wg.Done() + + ticker := time.NewTicker(1 * time.Minute) // Update stats every minute + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.updateStatistics() + case <-m.stopChan: + return + } + } +} + +// processGroupUpdates processes periodic group updates +func (m *Manager) processGroupUpdates() { + groups := m.GetAllGroups() + + for _, group := range groups { + if !group.IsDisbanded() { + // Update member information + members := group.GetMembers() + for _, gmi := range members { + if gmi.Member != nil { + group.UpdateGroupMemberInfo(gmi.Member, false) + } + } + } + } +} + +// updateGroupBuffs updates group buffs for all groups +func (m *Manager) updateGroupBuffs() { + // TODO: Implement group buff updates + // This would require integration with the spell/buff system +} + +// cleanupExpiredInvites removes expired invitations +func (m *Manager) cleanupExpiredInvites() { + m.invitesMutex.Lock() + defer m.invitesMutex.Unlock() + + now := time.Now() + expiredCount := 0 + + // Clean up regular invites + for key, invite := range m.PendingInvites { + if now.After(invite.ExpiresTime) { + delete(m.PendingInvites, key) + expiredCount++ + } + } + + // Clean up raid invites + for key, invite := range m.RaidPendingInvites { + if now.After(invite.ExpiresTime) { + delete(m.RaidPendingInvites, key) + expiredCount++ + } + } + + // Update statistics + if expiredCount > 0 { + m.statsMutex.Lock() + m.Stats.ExpiredInvites += int64(expiredCount) + m.statsMutex.Unlock() + } +} + +// updateStatistics updates manager statistics +func (m *Manager) updateStatistics() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + activeGroups := m.MasterList.GetActiveGroups() + raidGroups := m.MasterList.GetRaidGroups() + + var totalMembers int64 + var raidMembers int64 + + for _, group := range activeGroups { + totalMembers += int64(group.GetSize()) + if group.IsGroupRaid() { + raidMembers += int64(group.GetSize()) + } + } + + m.Stats.ActiveGroups = int64(len(activeGroups)) + m.Stats.ActiveRaids = int64(len(raidGroups)) + + if len(activeGroups) > 0 { + m.Stats.AverageGroupSize = float64(totalMembers) / float64(len(activeGroups)) + } else { + m.Stats.AverageGroupSize = 0 + } + + m.Stats.LastStatsUpdate = time.Now() +} + +// Statistics update methods + +// updateStatsForNewGroup updates statistics when a new group is created +func (m *Manager) updateStatsForNewGroup() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + m.Stats.TotalGroups++ +} + +// updateStatsForRemovedGroup updates statistics when a group is removed +func (m *Manager) updateStatsForRemovedGroup() { + // Statistics are primarily tracked in updateStatistics() +} + +// updateStatsForInvite updates statistics when an invite is sent +func (m *Manager) updateStatsForInvite() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + m.Stats.TotalInvites++ +} + +// updateStatsForAcceptedInvite updates statistics when an invite is accepted +func (m *Manager) updateStatsForAcceptedInvite() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + m.Stats.AcceptedInvites++ +} + +// updateStatsForDeclinedInvite updates statistics when an invite is declined +func (m *Manager) updateStatsForDeclinedInvite() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + m.Stats.DeclinedInvites++ +} + +// updateStatsForExpiredInvite updates statistics when an invite expires +func (m *Manager) updateStatsForExpiredInvite() { + if !m.Config.EnableStatistics { + return + } + + m.statsMutex.Lock() + defer m.statsMutex.Unlock() + + m.Stats.ExpiredInvites++ +} + +// Event system integration + +// AddEventHandler adds an event handler +func (m *Manager) AddEventHandler(handler GroupEventHandler) { + m.eventHandlersMutex.Lock() + defer m.eventHandlersMutex.Unlock() + + m.EventHandlers = append(m.EventHandlers, handler) +} + +// Integration interfaces + +// SetDatabase sets the database interface +func (m *Manager) SetDatabase(db GroupDatabase) { + m.database = db +} + +// SetPacketHandler sets the packet handler interface +func (m *Manager) SetPacketHandler(handler GroupPacketHandler) { + m.packetHandler = handler +} + +// SetValidator sets the validator interface +func (m *Manager) SetValidator(validator GroupValidator) { + m.validator = validator +} + +// SetNotifier sets the notifier interface +func (m *Manager) SetNotifier(notifier GroupNotifier) { + m.notifier = notifier +} + +// Event firing methods + +// fireGroupCreatedEvent fires a group created event +func (m *Manager) fireGroupCreatedEvent(group *Group, leader Entity) { + m.eventHandlersMutex.RLock() + defer m.eventHandlersMutex.RUnlock() + + for _, handler := range m.EventHandlers { + go handler.OnGroupCreated(group, leader) + } +} + +// fireGroupDisbandedEvent fires a group disbanded event +func (m *Manager) fireGroupDisbandedEvent(group *Group) { + m.eventHandlersMutex.RLock() + defer m.eventHandlersMutex.RUnlock() + + for _, handler := range m.EventHandlers { + go handler.OnGroupDisbanded(group) + } +} + +// fireGroupInviteSentEvent fires a group invite sent event +func (m *Manager) fireGroupInviteSentEvent(leader, member Entity) { + m.eventHandlersMutex.RLock() + defer m.eventHandlersMutex.RUnlock() + + for _, handler := range m.EventHandlers { + go handler.OnGroupInviteSent(leader, member) + } +} + +// fireGroupInviteAcceptedEvent fires a group invite accepted event +func (m *Manager) fireGroupInviteAcceptedEvent(leader, member Entity, groupID int32) { + m.eventHandlersMutex.RLock() + defer m.eventHandlersMutex.RUnlock() + + for _, handler := range m.EventHandlers { + go handler.OnGroupInviteAccepted(leader, member, groupID) + } +} + +// fireGroupInviteDeclinedEvent fires a group invite declined event +func (m *Manager) fireGroupInviteDeclinedEvent(leader, member Entity) { + m.eventHandlersMutex.RLock() + defer m.eventHandlersMutex.RUnlock() + + 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 3c4f08b..c1bd4b0 100644 --- a/internal/groups/manager_test.go +++ b/internal/groups/manager_test.go @@ -21,7 +21,7 @@ func TestManagerBackground(t *testing.T) { EnableStatistics: true, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -49,7 +49,7 @@ func TestManagerGroupLifecycle(t *testing.T) { EnableStatistics: false, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -134,7 +134,7 @@ func TestManagerInviteSystem(t *testing.T) { EnableStatistics: false, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -200,7 +200,7 @@ func TestManagerRaidOperations(t *testing.T) { EnableStatistics: false, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -271,7 +271,7 @@ func TestManagerConcurrentOperations(t *testing.T) { EnableStatistics: false, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -343,7 +343,7 @@ func TestManagerStatistics(t *testing.T) { EnableStatistics: true, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) err := manager.Start() if err != nil { t.Fatalf("Failed to start manager: %v", err) @@ -416,7 +416,7 @@ func TestManagerEventHandlers(t *testing.T) { EnableStatistics: false, } - manager := NewGroupManager(config) + manager := NewManager(config, nil) // Track events events := make([]string, 0) diff --git a/internal/groups/master.go b/internal/groups/master.go new file mode 100644 index 0000000..78cd00d --- /dev/null +++ b/internal/groups/master.go @@ -0,0 +1,213 @@ +package groups + +import ( + "fmt" + + "eq2emu/internal/common" +) + +// MasterList manages all groups using generic MasterList pattern +type MasterList struct { + *common.MasterList[int32, *Group] +} + +// NewMasterList creates a new master list for groups +func NewMasterList() *MasterList { + return &MasterList{ + MasterList: common.NewMasterList[int32, *Group](), + } +} + +// AddGroup adds a group to the master list +func (ml *MasterList) AddGroup(group *Group) bool { + return ml.MasterList.Add(group) +} + +// GetGroup retrieves a group by ID +func (ml *MasterList) GetGroup(groupID int32) *Group { + return ml.MasterList.Get(groupID) +} + +// RemoveGroup removes a group by ID +func (ml *MasterList) RemoveGroup(groupID int32) bool { + return ml.MasterList.Remove(groupID) +} + +// GetAllGroups returns all groups +func (ml *MasterList) GetAllGroups() []*Group { + return ml.MasterList.GetAllSlice() +} + +// GetGroupsByFilter returns groups matching the filter function +func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group { + return ml.MasterList.Filter(filter) +} + +// 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 + } + } + return false + }) +} + +// GetGroupsBySize returns groups of the specified size +func (ml *MasterList) GetGroupsBySize(size int32) []*Group { + return ml.GetGroupsByFilter(func(group *Group) bool { + return group.GetSize() == size + }) +} + +// GetRaidGroups returns all groups that are part of raids +func (ml *MasterList) GetRaidGroups() []*Group { + return ml.GetGroupsByFilter(func(group *Group) bool { + return group.IsGroupRaid() + }) +} + +// GetSoloGroups returns all groups with only one member +func (ml *MasterList) GetSoloGroups() []*Group { + return ml.GetGroupsBySize(1) +} + +// GetFullGroups returns all groups at maximum capacity +func (ml *MasterList) GetFullGroups() []*Group { + return ml.GetGroupsBySize(MAX_GROUP_SIZE) +} + +// 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 + }) +} + +// 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 + }) +} + +// GetGroupStatistics returns statistics about the groups in the master list +func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats { + allGroups := ml.GetAllGroups() + activeGroups := ml.GetActiveGroups() + raidGroups := ml.GetRaidGroups() + + var totalMembers int32 + var totalRaidMembers int32 + + for _, group := range activeGroups { + totalMembers += group.GetSize() + if group.IsGroupRaid() { + totalRaidMembers += group.GetSize() + } + } + + var averageGroupSize float64 + if len(activeGroups) > 0 { + averageGroupSize = float64(totalMembers) / float64(len(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())), + } +} + +// 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"` +} + +// Cleanup removes disbanded groups from the master list +func (ml *MasterList) Cleanup() int32 { + disbandedGroups := ml.GetGroupsByFilter(func(group *Group) bool { + return group.IsDisbanded() + }) + + removed := int32(0) + for _, group := range disbandedGroups { + if ml.RemoveGroup(group.GetID()) { + removed++ + } + } + + return removed +} + +// ValidateAll validates all groups in the master list +func (ml *MasterList) ValidateAll() []error { + var errors []error + + allGroups := ml.GetAllGroups() + for _, group := range allGroups { + // Check for basic validity + 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)", + 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)) + } + } + + return errors +} \ No newline at end of file diff --git a/internal/groups/service.go b/internal/groups/service.go deleted file mode 100644 index 2096a05..0000000 --- a/internal/groups/service.go +++ /dev/null @@ -1,528 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "time" -) - -// Service provides a high-level interface for group management -type Service struct { - manager *GroupManager - config ServiceConfig - started bool - startMutex sync.Mutex -} - -// ServiceConfig holds configuration for the group service -type ServiceConfig struct { - // Group manager configuration - ManagerConfig GroupManagerConfig `json:"manager_config"` - - // Service-specific settings - AutoCreateGroups bool `json:"auto_create_groups"` - AllowCrossZoneGroups bool `json:"allow_cross_zone_groups"` - AllowBotMembers bool `json:"allow_bot_members"` - AllowNPCMembers bool `json:"allow_npc_members"` - MaxInviteDistance float32 `json:"max_invite_distance"` - GroupLevelRange int8 `json:"group_level_range"` - EnableGroupPvP bool `json:"enable_group_pvp"` - EnableGroupBuffs bool `json:"enable_group_buffs"` - LogLevel string `json:"log_level"` - - // Integration settings - DatabaseEnabled bool `json:"database_enabled"` - EventsEnabled bool `json:"events_enabled"` - StatisticsEnabled bool `json:"statistics_enabled"` - ValidationEnabled bool `json:"validation_enabled"` -} - -// DefaultServiceConfig returns default service configuration -func DefaultServiceConfig() ServiceConfig { - return ServiceConfig{ - ManagerConfig: GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 1 * time.Second, - BuffUpdateInterval: 5 * time.Second, - EnableCrossServer: false, - EnableRaids: true, - EnableQuestSharing: true, - EnableAutoInvite: false, - EnableStatistics: true, - }, - AutoCreateGroups: true, - AllowCrossZoneGroups: true, - AllowBotMembers: true, - AllowNPCMembers: false, - MaxInviteDistance: 100.0, - GroupLevelRange: 10, - EnableGroupPvP: false, - EnableGroupBuffs: true, - LogLevel: "info", - DatabaseEnabled: true, - EventsEnabled: true, - StatisticsEnabled: true, - ValidationEnabled: true, - } -} - -// NewService creates a new group service -func NewService(config ServiceConfig) *Service { - return &Service{ - manager: NewGroupManager(config.ManagerConfig), - config: config, - started: false, - } -} - -// Start starts the group service -func (s *Service) Start() error { - s.startMutex.Lock() - defer s.startMutex.Unlock() - - if s.started { - return fmt.Errorf("service already started") - } - - if err := s.manager.Start(); err != nil { - return fmt.Errorf("failed to start group manager: %v", err) - } - - s.started = true - return nil -} - -// Stop stops the group service -func (s *Service) Stop() error { - s.startMutex.Lock() - defer s.startMutex.Unlock() - - if !s.started { - return nil - } - - if err := s.manager.Stop(); err != nil { - return fmt.Errorf("failed to stop group manager: %v", err) - } - - s.started = false - return nil -} - -// IsStarted returns true if the service is started -func (s *Service) IsStarted() bool { - s.startMutex.Lock() - defer s.startMutex.Unlock() - return s.started -} - -// GetManager returns the underlying group manager -func (s *Service) GetManager() GroupManagerInterface { - return s.manager -} - -// High-level group operations - -// CreateGroup creates a new group with validation -func (s *Service) CreateGroup(leader Entity, options *GroupOptions) (int32, error) { - if leader == nil { - return 0, fmt.Errorf("leader cannot be nil") - } - - // Validate leader can create group - if s.config.ValidationEnabled { - if err := s.validateGroupCreation(leader, options); err != nil { - return 0, fmt.Errorf("group creation validation failed: %v", err) - } - } - - // Use default options if none provided - if options == nil { - defaultOpts := DefaultGroupOptions() - options = &defaultOpts - } - - return s.manager.NewGroup(leader, options, 0) -} - -// InviteToGroup invites a member to join a group -func (s *Service) InviteToGroup(leader Entity, member Entity) error { - if leader == nil || member == nil { - return fmt.Errorf("leader and member cannot be nil") - } - - // Validate the invitation - if s.config.ValidationEnabled { - if err := s.validateGroupInvitation(leader, member); err != nil { - return fmt.Errorf("invitation validation failed: %v", err) - } - } - - // Send the invitation - result := s.manager.Invite(leader, member) - - switch result { - case GROUP_INVITE_SUCCESS: - return nil - case GROUP_INVITE_ALREADY_IN_GROUP: - return fmt.Errorf("member is already in a group") - case GROUP_INVITE_ALREADY_HAS_INVITE: - return fmt.Errorf("member already has a pending invite") - case GROUP_INVITE_GROUP_FULL: - return fmt.Errorf("group is full") - case GROUP_INVITE_DECLINED: - return fmt.Errorf("invitation was declined") - case GROUP_INVITE_TARGET_NOT_FOUND: - return fmt.Errorf("target not found") - case GROUP_INVITE_SELF_INVITE: - return fmt.Errorf("cannot invite yourself") - case GROUP_INVITE_PERMISSION_DENIED: - return fmt.Errorf("permission denied") - case GROUP_INVITE_TARGET_BUSY: - return fmt.Errorf("target is busy") - default: - return fmt.Errorf("unknown invitation error: %d", result) - } -} - -// AcceptGroupInvite accepts a group invitation -func (s *Service) AcceptGroupInvite(member Entity) error { - if member == nil { - return fmt.Errorf("member cannot be nil") - } - - result := s.manager.AcceptInvite(member, nil, true) - - switch result { - case GROUP_INVITE_SUCCESS: - return nil - case GROUP_INVITE_TARGET_NOT_FOUND: - return fmt.Errorf("no pending invitation found") - case GROUP_INVITE_GROUP_FULL: - return fmt.Errorf("group is full") - case GROUP_INVITE_PERMISSION_DENIED: - return fmt.Errorf("permission denied") - default: - return fmt.Errorf("unknown acceptance error: %d", result) - } -} - -// DeclineGroupInvite declines a group invitation -func (s *Service) DeclineGroupInvite(member Entity) { - if member != nil { - s.manager.DeclineInvite(member) - } -} - -// LeaveGroup removes a member from their current group -func (s *Service) LeaveGroup(member Entity) error { - if member == nil { - return fmt.Errorf("member cannot be nil") - } - - // TODO: Get member's current group ID - // groupID := member.GetGroupID() - groupID := int32(0) // Placeholder - - if groupID == 0 { - return fmt.Errorf("member is not in a group") - } - - return s.manager.RemoveGroupMember(groupID, member) -} - -// DisbandGroup disbands a group -func (s *Service) DisbandGroup(groupID int32) error { - return s.manager.RemoveGroup(groupID) -} - -// TransferLeadership transfers group leadership -func (s *Service) TransferLeadership(groupID int32, newLeader Entity) error { - if newLeader == nil { - return fmt.Errorf("new leader cannot be nil") - } - - if !s.manager.IsGroupIDValid(groupID) { - return fmt.Errorf("invalid group ID") - } - - if !s.manager.MakeLeader(groupID, newLeader) { - return fmt.Errorf("failed to transfer leadership") - } - - return nil -} - -// Group information methods - -// GetGroupInfo returns detailed information about a group -func (s *Service) GetGroupInfo(groupID int32) (*GroupInfo, error) { - group := s.manager.GetGroup(groupID) - if group == nil { - return nil, fmt.Errorf("group not found") - } - - members := group.GetMembers() - options := group.GetGroupOptions() - raidGroups := group.GetRaidGroups() - - info := &GroupInfo{ - GroupID: group.GetID(), - Size: int(group.GetSize()), - Members: members, - Options: options, - RaidGroups: raidGroups, - IsRaid: group.IsGroupRaid(), - LeaderName: group.GetLeaderName(), - CreatedTime: group.GetCreatedTime(), - LastActivity: group.GetLastActivity(), - IsDisbanded: group.IsDisbanded(), - } - - return info, nil -} - -// GetMemberGroups returns all groups that contain any of the specified members -func (s *Service) GetMemberGroups(members []Entity) []*GroupInfo { - var groups []*GroupInfo - - allGroups := s.manager.GetAllGroups() - for _, group := range allGroups { - if group.IsDisbanded() { - continue - } - - groupMembers := group.GetMembers() - for _, member := range members { - for _, gmi := range groupMembers { - if gmi.Member == member { - if info, err := s.GetGroupInfo(group.GetID()); err == nil { - groups = append(groups, info) - } - break - } - } - } - } - - return groups -} - -// GetGroupsByZone returns all groups with members in the specified zone -func (s *Service) GetGroupsByZone(zoneID int32) []*GroupInfo { - var groups []*GroupInfo - - allGroups := s.manager.GetAllGroups() - for _, group := range allGroups { - if group.IsDisbanded() { - continue - } - - members := group.GetMembers() - hasZoneMember := false - - for _, member := range members { - if member.ZoneID == zoneID { - hasZoneMember = true - break - } - } - - if hasZoneMember { - if info, err := s.GetGroupInfo(group.GetID()); err == nil { - groups = append(groups, info) - } - } - } - - return groups -} - -// Raid operations - -// FormRaid forms a raid from multiple groups -func (s *Service) FormRaid(leaderGroupID int32, targetGroupIDs []int32) error { - if !s.config.ManagerConfig.EnableRaids { - return fmt.Errorf("raids are disabled") - } - - leaderGroup := s.manager.GetGroup(leaderGroupID) - if leaderGroup == nil { - return fmt.Errorf("leader group not found") - } - - // Validate all target groups exist - for _, groupID := range targetGroupIDs { - if !s.manager.IsGroupIDValid(groupID) { - return fmt.Errorf("invalid target group ID: %d", groupID) - } - } - - // Add all groups to the raid - allRaidGroups := append([]int32{leaderGroupID}, targetGroupIDs...) - - for _, groupID := range allRaidGroups { - s.manager.ReplaceRaidGroups(groupID, allRaidGroups) - } - - return nil -} - -// DisbandRaid disbands a raid -func (s *Service) DisbandRaid(groupID int32) error { - group := s.manager.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group not found") - } - - raidGroups := group.GetRaidGroups() - if len(raidGroups) == 0 { - return fmt.Errorf("group is not in a raid") - } - - // Clear raid associations for all groups - for _, raidGroupID := range raidGroups { - s.manager.ClearGroupRaid(raidGroupID) - } - - return nil -} - -// Service configuration - -// UpdateConfig updates the service configuration -func (s *Service) UpdateConfig(config ServiceConfig) error { - s.config = config - return nil -} - -// GetConfig returns the current service configuration -func (s *Service) GetConfig() ServiceConfig { - return s.config -} - -// Integration methods - -// SetDatabase sets the database interface -func (s *Service) SetDatabase(db GroupDatabase) { - s.manager.SetDatabase(db) -} - -// SetPacketHandler sets the packet handler interface -func (s *Service) SetPacketHandler(handler GroupPacketHandler) { - s.manager.SetPacketHandler(handler) -} - -// SetValidator sets the validator interface -func (s *Service) SetValidator(validator GroupValidator) { - s.manager.SetValidator(validator) -} - -// SetNotifier sets the notifier interface -func (s *Service) SetNotifier(notifier GroupNotifier) { - s.manager.SetNotifier(notifier) -} - -// AddEventHandler adds an event handler -func (s *Service) AddEventHandler(handler GroupEventHandler) { - s.manager.AddEventHandler(handler) -} - -// Statistics - -// GetServiceStats returns service statistics -func (s *Service) GetServiceStats() *ServiceStats { - managerStats := s.manager.GetStats() - - return &ServiceStats{ - ManagerStats: managerStats, - ServiceStartTime: time.Now(), // TODO: Track actual start time - IsStarted: s.started, - Config: s.config, - } -} - -// Validation methods - -// validateGroupCreation validates group creation parameters -func (s *Service) validateGroupCreation(leader Entity, options *GroupOptions) error { - // Check if leader is already in a group - // TODO: Check leader's group status - // if leader.GetGroupMemberInfo() != nil { - // return fmt.Errorf("leader is already in a group") - // } - - // Validate options - if options != nil && !options.IsValid() { - return fmt.Errorf("invalid group options") - } - - return nil -} - -// validateGroupInvitation validates group invitation parameters -func (s *Service) validateGroupInvitation(leader Entity, member Entity) error { - // Check distance if enabled - if s.config.MaxInviteDistance > 0 { - distance := leader.GetDistance(member) - if distance > s.config.MaxInviteDistance { - return fmt.Errorf("member is too far away (%.1f > %.1f)", distance, s.config.MaxInviteDistance) - } - } - - // Check level range if enabled - if s.config.GroupLevelRange > 0 { - leaderLevel := leader.GetLevel() - memberLevel := member.GetLevel() - levelDiff := leaderLevel - memberLevel - if levelDiff < 0 { - levelDiff = -levelDiff - } - - if levelDiff > s.config.GroupLevelRange { - return fmt.Errorf("level difference too large (%d > %d)", levelDiff, s.config.GroupLevelRange) - } - } - - // Check if member type is allowed - if member.IsBot() && !s.config.AllowBotMembers { - return fmt.Errorf("bot members are not allowed") - } - - if member.IsNPC() && !s.config.AllowNPCMembers { - return fmt.Errorf("NPC members are not allowed") - } - - // Check zone restrictions - if !s.config.AllowCrossZoneGroups { - if leader.GetZone() != member.GetZone() { - return fmt.Errorf("cross-zone groups are not allowed") - } - } - - return nil -} - -// GroupInfo holds detailed information about a group -type GroupInfo struct { - GroupID int32 `json:"group_id"` - Size int `json:"size"` - Members []*GroupMemberInfo `json:"members"` - Options GroupOptions `json:"options"` - RaidGroups []int32 `json:"raid_groups"` - IsRaid bool `json:"is_raid"` - LeaderName string `json:"leader_name"` - CreatedTime time.Time `json:"created_time"` - LastActivity time.Time `json:"last_activity"` - IsDisbanded bool `json:"is_disbanded"` -} - -// ServiceStats holds statistics about the service -type ServiceStats struct { - ManagerStats GroupManagerStats `json:"manager_stats"` - ServiceStartTime time.Time `json:"service_start_time"` - IsStarted bool `json:"is_started"` - Config ServiceConfig `json:"config"` -} diff --git a/internal/groups/service_test.go b/internal/groups/service_test.go deleted file mode 100644 index ecdd20f..0000000 --- a/internal/groups/service_test.go +++ /dev/null @@ -1,531 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "testing" -) - -// TestServiceLifecycle tests service start/stop -func TestServiceLifecycle(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - - service := NewService(config) - - // Test initial state - if service.IsStarted() { - t.Error("Service should not be started initially") - } - - // Test starting service - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - - if !service.IsStarted() { - t.Error("Service should be started after Start()") - } - - // Test starting already started service - err = service.Start() - if err == nil { - t.Error("Should get error starting already started service") - } - - // Test stopping service - err = service.Stop() - if err != nil { - t.Fatalf("Failed to stop service: %v", err) - } - - if service.IsStarted() { - t.Error("Service should not be started after Stop()") - } - - // Test stopping already stopped service - err = service.Stop() - if err != nil { - t.Error("Should not get error stopping already stopped service") - } -} - -// TestServiceGroupOperations tests high-level group operations -func TestServiceGroupOperations(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - config.ValidationEnabled = true - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - // Create entities - leader := createMockEntity(1, "Leader", true) - member1 := createMockEntity(2, "Member1", true) - _ = createMockEntity(3, "Member2", true) // member2 - currently unused - - // Test group creation - groupID, err := service.CreateGroup(leader, nil) - if err != nil { - t.Fatalf("Failed to create group: %v", err) - } - - // Test group info retrieval - info, err := service.GetGroupInfo(groupID) - if err != nil { - t.Fatalf("Failed to get group info: %v", err) - } - - if info.GroupID != groupID { - t.Errorf("Expected group ID %d, got %d", groupID, info.GroupID) - } - if info.Size != 1 { - t.Errorf("Expected size 1, got %d", info.Size) - } - if info.LeaderName != "Leader" { - t.Errorf("Expected leader name 'Leader', got '%s'", info.LeaderName) - } - - // Test invitations - err = service.InviteToGroup(leader, member1) - if err != nil { - t.Errorf("Failed to invite member1: %v", err) - } - - // Accept invitation (will fail due to missing world integration) - err = service.AcceptGroupInvite(member1) - if err == nil { - t.Log("Accept invite succeeded unexpectedly (test limitation)") - } - - // Manually add member to test other features - manager := service.GetManager() - err = manager.AddGroupMember(groupID, member1, false) - if err != nil { - t.Fatalf("Failed to manually add member1: %v", err) - } - - // Test duplicate invitation - // NOTE: The check for already grouped members is not implemented (see manager.go line 232) - // So this test will not work as expected until that's implemented - err = service.InviteToGroup(leader, member1) - if err == nil { - t.Log("Note: Already-in-group check not implemented in manager.Invite()") - // Skip this test for now - } - - // Test leadership transfer - err = service.TransferLeadership(groupID, member1) - if err != nil { - t.Errorf("Failed to transfer leadership: %v", err) - } - - // Verify leadership changed - info, _ = service.GetGroupInfo(groupID) - if info.LeaderName != "Member1" { - t.Errorf("Expected leader name 'Member1', got '%s'", info.LeaderName) - } - - // Test group disbanding - err = service.DisbandGroup(groupID) - if err != nil { - t.Errorf("Failed to disband group: %v", err) - } - - // Verify group was disbanded - _, err = service.GetGroupInfo(groupID) - if err == nil { - t.Error("Should get error getting info for disbanded group") - } -} - -// TestServiceValidation tests invitation validation -func TestServiceValidation(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ValidationEnabled = true - config.MaxInviteDistance = 50.0 - config.GroupLevelRange = 5 - config.AllowBotMembers = false - config.AllowNPCMembers = false - config.AllowCrossZoneGroups = false - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - // Create leader - leader := createMockEntity(1, "Leader", true) - leader.level = 50 - groupID, _ := service.CreateGroup(leader, nil) - - // Test distance validation - farMember := createMockEntity(2, "FarMember", true) - farMember.level = 50 - // Skip distance validation test since we can't override methods on mock entities - // In a real implementation, you would create a mock that returns different distances - - err = service.InviteToGroup(leader, farMember) - if err == nil { - t.Error("Should get error inviting far member") - } - - // End of distance test skip - - // Test level range validation - lowLevelMember := createMockEntity(3, "LowLevel", true) - lowLevelMember.level = 40 - - err = service.InviteToGroup(leader, lowLevelMember) - if err == nil { - t.Error("Should get error inviting member with large level difference") - } - - // Test bot member validation - botMember := createMockEntity(4, "Bot", true) - botMember.isBot = true - - err = service.InviteToGroup(leader, botMember) - if err == nil { - t.Error("Should get error inviting bot when not allowed") - } - - // Test NPC member validation - npcMember := createMockEntity(5, "NPC", false) - npcMember.isNPC = true - - err = service.InviteToGroup(leader, npcMember) - if err == nil { - t.Error("Should get error inviting NPC when not allowed") - } - - // Test cross-zone validation - differentZoneMember := createMockEntity(6, "DiffZone", true) - differentZoneMember.level = 50 - differentZoneMember.zone = &mockZone{ - zoneID: 300, - instanceID: 2, - zoneName: "differentzone", - } - - err = service.InviteToGroup(leader, differentZoneMember) - if err == nil { - t.Error("Should get error inviting member from different zone") - } - - // Clean up - service.DisbandGroup(groupID) -} - -// TestServiceRaidOperations tests raid functionality -func TestServiceRaidOperations(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableRaids = true - config.ManagerConfig.EnableStatistics = false - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - // Create multiple groups - groupIDs := make([]int32, 4) - for i := range 4 { - leader := createMockEntity(int32(i*10+1), fmt.Sprintf("Leader%d", i), true) - groupID, err := service.CreateGroup(leader, nil) - if err != nil { - t.Fatalf("Failed to create group %d: %v", i, err) - } - groupIDs[i] = groupID - } - - // Form raid - err = service.FormRaid(groupIDs[0], groupIDs[1:]) - if err != nil { - t.Fatalf("Failed to form raid: %v", err) - } - - // Verify raid status - for _, groupID := range groupIDs { - info, err := service.GetGroupInfo(groupID) - if err != nil { - t.Errorf("Failed to get info for group %d: %v", groupID, err) - } - if !info.IsRaid { - t.Errorf("Group %d should be in raid", groupID) - } - if len(info.RaidGroups) != 4 { - t.Errorf("Group %d should have 4 raid groups, got %d", groupID, len(info.RaidGroups)) - } - } - - // Disband raid - err = service.DisbandRaid(groupIDs[0]) - if err != nil { - t.Fatalf("Failed to disband raid: %v", err) - } - - // Verify raid disbanded - for _, groupID := range groupIDs { - info, _ := service.GetGroupInfo(groupID) - if info.IsRaid { - t.Errorf("Group %d should not be in raid after disband", groupID) - } - } - - // Clean up - for _, groupID := range groupIDs { - service.DisbandGroup(groupID) - } -} - -// TestServiceQueries tests group query methods -func TestServiceQueries(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - // Create groups in different zones - zone1 := &mockZone{zoneID: 100, instanceID: 1, zoneName: "zone1"} - zone2 := &mockZone{zoneID: 200, instanceID: 1, zoneName: "zone2"} - - // Group 1 in zone1 - leader1 := createMockEntity(1, "Leader1", true) - leader1.zone = zone1 - member1 := createMockEntity(2, "Member1", true) - member1.zone = zone1 - - groupID1, _ := service.CreateGroup(leader1, nil) - service.GetManager().AddGroupMember(groupID1, member1, false) - - // Group 2 in zone2 - leader2 := createMockEntity(3, "Leader2", true) - leader2.zone = zone2 - member2 := createMockEntity(4, "Member2", true) - member2.zone = zone2 - - groupID2, _ := service.CreateGroup(leader2, nil) - service.GetManager().AddGroupMember(groupID2, member2, false) - - // Test GetMemberGroups - memberGroups := service.GetMemberGroups([]Entity{member1, member2}) - if len(memberGroups) != 2 { - t.Errorf("Expected 2 groups, got %d", len(memberGroups)) - } - - // Test GetGroupsByZone - zone1Groups := service.GetGroupsByZone(100) - if len(zone1Groups) != 1 { - t.Errorf("Expected 1 group in zone1, got %d", len(zone1Groups)) - } - if zone1Groups[0].GroupID != groupID1 { - t.Errorf("Expected group %d in zone1, got %d", groupID1, zone1Groups[0].GroupID) - } - - zone2Groups := service.GetGroupsByZone(200) - if len(zone2Groups) != 1 { - t.Errorf("Expected 1 group in zone2, got %d", len(zone2Groups)) - } - if zone2Groups[0].GroupID != groupID2 { - t.Errorf("Expected group %d in zone2, got %d", groupID2, zone2Groups[0].GroupID) - } - - // Clean up - service.DisbandGroup(groupID1) - service.DisbandGroup(groupID2) -} - -// TestServiceConfiguration tests configuration management -func TestServiceConfiguration(t *testing.T) { - config := DefaultServiceConfig() - service := NewService(config) - - // Test getting config - retrievedConfig := service.GetConfig() - if retrievedConfig.MaxInviteDistance != config.MaxInviteDistance { - t.Error("Retrieved config doesn't match initial config") - } - - // Test updating config - newConfig := DefaultServiceConfig() - newConfig.MaxInviteDistance = 200.0 - newConfig.GroupLevelRange = 20 - - err := service.UpdateConfig(newConfig) - if err != nil { - t.Errorf("Failed to update config: %v", err) - } - - retrievedConfig = service.GetConfig() - if retrievedConfig.MaxInviteDistance != 200.0 { - t.Errorf("Expected max invite distance 200.0, got %f", retrievedConfig.MaxInviteDistance) - } - if retrievedConfig.GroupLevelRange != 20 { - t.Errorf("Expected group level range 20, got %d", retrievedConfig.GroupLevelRange) - } -} - -// TestServiceStatistics tests service statistics -func TestServiceStatistics(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.EnableStatistics = true - config.StatisticsEnabled = true - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - // Get initial stats - stats := service.GetServiceStats() - if !stats.IsStarted { - t.Error("Service should be started in stats") - } - - // Create some groups and verify stats - leader := createMockEntity(1, "Leader", true) - groupID, _ := service.CreateGroup(leader, nil) - - stats = service.GetServiceStats() - // ActiveGroups is only updated by background stats loop - // We can check TotalGroups instead which is updated immediately - if stats.ManagerStats.TotalGroups != 1 { - t.Errorf("Expected 1 total group in stats, got %d", stats.ManagerStats.TotalGroups) - } - - // Clean up - service.DisbandGroup(groupID) -} - -// TestServiceConcurrency tests concurrent service operations -func TestServiceConcurrency(t *testing.T) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - - service := NewService(config) - err := service.Start() - if err != nil { - t.Fatalf("Failed to start service: %v", err) - } - defer service.Stop() - - const numGoroutines = 20 - const operationsPerGoroutine = 10 - var wg sync.WaitGroup - - // Concurrent group operations - wg.Add(numGoroutines) - for i := range numGoroutines { - go func(id int) { - defer wg.Done() - - for j := range operationsPerGoroutine { - // Create group - leader := createMockEntity(int32(id*1000+j), fmt.Sprintf("Leader%d_%d", id, j), true) - groupID, err := service.CreateGroup(leader, nil) - if err != nil { - continue - } - - // Get group info - _, _ = service.GetGroupInfo(groupID) - - // Try some invitations - member := createMockEntity(int32(id*1000+j+500), fmt.Sprintf("Member%d_%d", id, j), true) - _ = service.InviteToGroup(leader, member) - - // Transfer leadership - _ = service.TransferLeadership(groupID, member) - - // Disband group - _ = service.DisbandGroup(groupID) - } - }(i) - } - wg.Wait() - - // Verify cleanup - stats := service.GetServiceStats() - if stats.ManagerStats.ActiveGroups != 0 { - t.Errorf("Expected 0 active groups after cleanup, got %d", stats.ManagerStats.ActiveGroups) - } -} - -// Benchmark tests for service -func BenchmarkServiceGroupCreation(b *testing.B) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - config.ValidationEnabled = false - - service := NewService(config) - service.Start() - defer service.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - leader := createMockEntity(int32(i), fmt.Sprintf("Leader%d", i), true) - groupID, _ := service.CreateGroup(leader, nil) - service.DisbandGroup(groupID) - } -} - -func BenchmarkServiceGroupInfo(b *testing.B) { - config := DefaultServiceConfig() - config.ManagerConfig.UpdateInterval = 0 - config.ManagerConfig.BuffUpdateInterval = 0 - config.ManagerConfig.EnableStatistics = false - - service := NewService(config) - service.Start() - defer service.Stop() - - // Create some groups - groupIDs := make([]int32, 10) - for i := range 10 { - leader := createMockEntity(int32(i), fmt.Sprintf("Leader%d", i), true) - groupID, _ := service.CreateGroup(leader, nil) - groupIDs[i] = groupID - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - groupID := groupIDs[i%len(groupIDs)] - _, _ = service.GetGroupInfo(groupID) - } - - // Clean up - for _, groupID := range groupIDs { - service.DisbandGroup(groupID) - } -} \ No newline at end of file diff --git a/internal/groups/types.go b/internal/groups/types.go index c464715..f014822 100644 --- a/internal/groups/types.go +++ b/internal/groups/types.go @@ -7,51 +7,51 @@ import ( // GroupOptions holds group configuration settings type GroupOptions struct { - LootMethod int8 `json:"loot_method"` - LootItemsRarity int8 `json:"loot_items_rarity"` - AutoSplit int8 `json:"auto_split"` - DefaultYell int8 `json:"default_yell"` - GroupLockMethod int8 `json:"group_lock_method"` - GroupAutolock int8 `json:"group_autolock"` - SoloAutolock int8 `json:"solo_autolock"` - AutoLootMethod int8 `json:"auto_loot_method"` - LastLootedIndex int8 `json:"last_looted_index"` + LootMethod int8 `json:"loot_method" db:"loot_method"` + LootItemsRarity int8 `json:"loot_items_rarity" db:"loot_items_rarity"` + AutoSplit int8 `json:"auto_split" db:"auto_split"` + DefaultYell int8 `json:"default_yell" db:"default_yell"` + GroupLockMethod int8 `json:"group_lock_method" db:"group_lock_method"` + GroupAutolock int8 `json:"group_autolock" db:"group_autolock"` + SoloAutolock int8 `json:"solo_autolock" db:"solo_autolock"` + AutoLootMethod int8 `json:"auto_loot_method" db:"auto_loot_method"` + LastLootedIndex int8 `json:"last_looted_index" db:"last_looted_index"` } // GroupMemberInfo contains all information about a group member type GroupMemberInfo struct { // Group and member identification - GroupID int32 `json:"group_id"` - Name string `json:"name"` - Zone string `json:"zone"` + GroupID int32 `json:"group_id" db:"group_id"` + Name string `json:"name" db:"name"` + Zone string `json:"zone" db:"zone"` // Health and power stats - HPCurrent int32 `json:"hp_current"` - HPMax int32 `json:"hp_max"` - PowerCurrent int32 `json:"power_current"` - PowerMax int32 `json:"power_max"` + HPCurrent int32 `json:"hp_current" db:"hp_current"` + HPMax int32 `json:"hp_max" db:"hp_max"` + PowerCurrent int32 `json:"power_current" db:"power_current"` + PowerMax int32 `json:"power_max" db:"power_max"` // Level and character info - LevelCurrent int16 `json:"level_current"` - LevelMax int16 `json:"level_max"` - RaceID int8 `json:"race_id"` - ClassID int8 `json:"class_id"` + LevelCurrent int16 `json:"level_current" db:"level_current"` + LevelMax int16 `json:"level_max" db:"level_max"` + RaceID int8 `json:"race_id" db:"race_id"` + ClassID int8 `json:"class_id" db:"class_id"` // Group status - Leader bool `json:"leader"` - IsClient bool `json:"is_client"` - IsRaidLooter bool `json:"is_raid_looter"` + Leader bool `json:"leader" db:"leader"` + IsClient bool `json:"is_client" db:"is_client"` + IsRaidLooter bool `json:"is_raid_looter" db:"is_raid_looter"` // Zone and instance info - ZoneID int32 `json:"zone_id"` - InstanceID int32 `json:"instance_id"` + ZoneID int32 `json:"zone_id" db:"zone_id"` + InstanceID int32 `json:"instance_id" db:"instance_id"` // Mentoring - MentorTargetCharID int32 `json:"mentor_target_char_id"` + MentorTargetCharID int32 `json:"mentor_target_char_id" db:"mentor_target_char_id"` // Network info for cross-server groups - ClientPeerAddress string `json:"client_peer_address"` - ClientPeerPort int16 `json:"client_peer_port"` + ClientPeerAddress string `json:"client_peer_address" db:"client_peer_address"` + ClientPeerPort int16 `json:"client_peer_port" db:"client_peer_port"` // Entity reference (local members only) Member Entity `json:"-"` @@ -60,44 +60,11 @@ type GroupMemberInfo struct { Client any `json:"-"` // Timestamps - JoinTime time.Time `json:"join_time"` - LastUpdate time.Time `json:"last_update"` + JoinTime time.Time `json:"join_time" db:"join_time"` + LastUpdate time.Time `json:"last_update" db:"last_update"` } -// Group represents a player group -type Group struct { - // Group identification - id int32 - - // Group options and configuration - options GroupOptions - optionsMutex sync.RWMutex - - // Group members - members []*GroupMemberInfo - membersMutex sync.RWMutex - - // Raid functionality - raidGroups []int32 - raidGroupsMutex sync.RWMutex - - // Group statistics - createdTime time.Time - lastActivity time.Time - activityMutex sync.RWMutex - - // Group status - disbanded bool - disbandMutex sync.RWMutex - - // Communication channels - messageQueue chan *GroupMessage - updateQueue chan *GroupUpdate - - // Background processing - stopChan chan struct{} - wg sync.WaitGroup -} +// Group is now defined in group.go - this type definition removed to avoid duplication // GroupMessage represents a message sent to the group type GroupMessage struct {