modernize groups first path
This commit is contained in:
parent
d3ffe7b4ee
commit
a29406a57a
6
go.mod
6
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
|
||||
|
666
internal/groups/benchmark_test.go
Normal file
666
internal/groups/benchmark_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
517
internal/groups/manager_methods.go
Normal file
517
internal/groups/manager_methods.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
213
internal/groups/master.go
Normal 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
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user