modernize groups first path

This commit is contained in:
Sky Johnson 2025-08-07 22:37:04 -05:00
parent d3ffe7b4ee
commit a29406a57a
12 changed files with 1790 additions and 1882 deletions

6
go.mod
View File

@ -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

View File

@ -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()
}
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

View File

@ -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)

213
internal/groups/master.go Normal file
View File

@ -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
}

View File

@ -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"`
}

View File

@ -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)
}
}

View File

@ -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 {