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
|
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 golang.org/x/text v0.27.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // 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
|
package groups
|
||||||
|
|
||||||
// Entity is the interface for entities that can be part of groups
|
// Entity represents a game entity that can be part of a group
|
||||||
// This interface is implemented by Player, NPC, and Bot types
|
|
||||||
type Entity interface {
|
type Entity interface {
|
||||||
// Basic entity information
|
// Basic entity information
|
||||||
GetID() int32
|
GetID() int32
|
||||||
@ -16,27 +15,20 @@ type Entity interface {
|
|||||||
GetPower() int32
|
GetPower() int32
|
||||||
GetTotalPower() int32
|
GetTotalPower() int32
|
||||||
|
|
||||||
// Entity type checks
|
// Entity types
|
||||||
IsPlayer() bool
|
IsPlayer() bool
|
||||||
IsNPC() bool
|
|
||||||
IsBot() bool
|
IsBot() bool
|
||||||
|
IsNPC() bool
|
||||||
IsDead() bool
|
IsDead() bool
|
||||||
|
|
||||||
// Zone information
|
// World positioning
|
||||||
GetZone() Zone
|
GetZone() Zone
|
||||||
|
|
||||||
// Distance calculation
|
|
||||||
GetDistance(other Entity) float32
|
GetDistance(other Entity) float32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zone interface for zone information
|
// Zone represents a game zone
|
||||||
type Zone interface {
|
type Zone interface {
|
||||||
GetZoneID() int32
|
GetZoneID() int32
|
||||||
GetInstanceID() int32
|
GetInstanceID() int32
|
||||||
GetZoneName() string
|
GetZoneName() string
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn interface for distance calculations
|
|
||||||
type Spawn interface {
|
|
||||||
// Minimal spawn interface for distance calculations
|
|
||||||
}
|
}
|
@ -2,27 +2,85 @@ package groups
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewGroup creates a new group with the given ID and options
|
// Group represents a player group with embedded database operations
|
||||||
func NewGroup(id int32, options *GroupOptions) *Group {
|
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 {
|
if options == nil {
|
||||||
defaultOpts := DefaultGroupOptions()
|
defaultOpts := DefaultGroupOptions()
|
||||||
options = &defaultOpts
|
options = &defaultOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
group := &Group{
|
group := &Group{
|
||||||
id: id,
|
GroupID: id,
|
||||||
options: *options,
|
Options: *options,
|
||||||
members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE),
|
Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE),
|
||||||
raidGroups: make([]int32, 0),
|
RaidGroups: make([]int32, 0),
|
||||||
createdTime: time.Now(),
|
CreatedTime: time.Now(),
|
||||||
lastActivity: time.Now(),
|
LastActivity: time.Now(),
|
||||||
disbanded: false,
|
Disbanded: false,
|
||||||
messageQueue: make(chan *GroupMessage, 100),
|
messageQueue: make(chan *GroupMessage, 100),
|
||||||
updateQueue: make(chan *GroupUpdate, 100),
|
updateQueue: make(chan *GroupUpdate, 100),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start background processing
|
// Start background processing
|
||||||
@ -32,9 +90,33 @@ func NewGroup(id int32, options *GroupOptions) *Group {
|
|||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the group ID
|
// GetID returns the group ID (implements Identifiable interface)
|
||||||
func (g *Group) GetID() int32 {
|
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
|
// GetSize returns the number of members in the group
|
||||||
@ -42,7 +124,7 @@ func (g *Group) GetSize() int32 {
|
|||||||
g.membersMutex.RLock()
|
g.membersMutex.RLock()
|
||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
return int32(len(g.members))
|
return int32(len(g.Members))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMembers returns a copy of the member list
|
// GetMembers returns a copy of the member list
|
||||||
@ -50,8 +132,8 @@ func (g *Group) GetMembers() []*GroupMemberInfo {
|
|||||||
g.membersMutex.RLock()
|
g.membersMutex.RLock()
|
||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
members := make([]*GroupMemberInfo, len(g.members))
|
members := make([]*GroupMemberInfo, len(g.Members))
|
||||||
for i, member := range g.members {
|
for i, member := range g.Members {
|
||||||
members[i] = member.Copy()
|
members[i] = member.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +147,7 @@ func (g *Group) AddMember(member Entity, isLeader bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.disbandMutex.RLock()
|
g.disbandMutex.RLock()
|
||||||
if g.disbanded {
|
if g.Disbanded {
|
||||||
g.disbandMutex.RUnlock()
|
g.disbandMutex.RUnlock()
|
||||||
return fmt.Errorf("group has been disbanded")
|
return fmt.Errorf("group has been disbanded")
|
||||||
}
|
}
|
||||||
@ -75,12 +157,12 @@ func (g *Group) AddMember(member Entity, isLeader bool) error {
|
|||||||
defer g.membersMutex.Unlock()
|
defer g.membersMutex.Unlock()
|
||||||
|
|
||||||
// Check if group is full
|
// 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")
|
return fmt.Errorf("group is full")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if member is already in the group
|
// Check if member is already in the group
|
||||||
for _, gmi := range g.members {
|
for _, gmi := range g.Members {
|
||||||
if gmi.Member == member {
|
if gmi.Member == member {
|
||||||
return fmt.Errorf("member is already in the group")
|
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
|
// Create new group member info
|
||||||
gmi := &GroupMemberInfo{
|
gmi := &GroupMemberInfo{
|
||||||
GroupID: g.id,
|
GroupID: g.GroupID,
|
||||||
Name: member.GetName(),
|
Name: member.GetName(),
|
||||||
Leader: isLeader,
|
Leader: isLeader,
|
||||||
Member: member,
|
Member: member,
|
||||||
@ -114,7 +196,7 @@ func (g *Group) AddMember(member Entity, isLeader bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to members list
|
// Add to members list
|
||||||
g.members = append(g.members, gmi)
|
g.Members = append(g.Members, gmi)
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
|
|
||||||
// Set group reference on the entity
|
// 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 {
|
zoneID, instanceID int32, peerAddress string, peerPort int16, isRaidLooter bool) error {
|
||||||
|
|
||||||
g.disbandMutex.RLock()
|
g.disbandMutex.RLock()
|
||||||
if g.disbanded {
|
if g.Disbanded {
|
||||||
g.disbandMutex.RUnlock()
|
g.disbandMutex.RUnlock()
|
||||||
return fmt.Errorf("group has been disbanded")
|
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()
|
defer g.membersMutex.Unlock()
|
||||||
|
|
||||||
// Check if group is full
|
// 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")
|
return fmt.Errorf("group is full")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new group member info for peer member
|
// Create new group member info for peer member
|
||||||
gmi := &GroupMemberInfo{
|
gmi := &GroupMemberInfo{
|
||||||
GroupID: g.id,
|
GroupID: g.GroupID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Zone: zoneName,
|
Zone: zoneName,
|
||||||
HPCurrent: hpCur,
|
HPCurrent: hpCur,
|
||||||
@ -176,7 +258,7 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to members list
|
// Add to members list
|
||||||
g.members = append(g.members, gmi)
|
g.Members = append(g.Members, gmi)
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
|
|
||||||
// Send group update
|
// Send group update
|
||||||
@ -195,14 +277,14 @@ func (g *Group) RemoveMember(member Entity) error {
|
|||||||
defer g.membersMutex.Unlock()
|
defer g.membersMutex.Unlock()
|
||||||
|
|
||||||
// Find and remove the member
|
// Find and remove the member
|
||||||
for i, gmi := range g.members {
|
for i, gmi := range g.Members {
|
||||||
if gmi.Member == member {
|
if gmi.Member == member {
|
||||||
// Clear group reference on entity
|
// Clear group reference on entity
|
||||||
// TODO: Clear group member info on entity
|
// TODO: Clear group member info on entity
|
||||||
// member.SetGroupMemberInfo(nil)
|
// member.SetGroupMemberInfo(nil)
|
||||||
|
|
||||||
// Remove from slice
|
// 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()
|
g.updateLastActivity()
|
||||||
|
|
||||||
// If this was a bot, camp it
|
// 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()
|
defer g.membersMutex.Unlock()
|
||||||
|
|
||||||
// Find and remove the member
|
// Find and remove the member
|
||||||
for i, gmi := range g.members {
|
for i, gmi := range g.Members {
|
||||||
if gmi.Name == name && gmi.IsClient == isClient {
|
if gmi.Name == name && gmi.IsClient == isClient {
|
||||||
// Handle mentorship cleanup
|
// Handle mentorship cleanup
|
||||||
if isClient && charID > 0 {
|
if isClient && charID > 0 {
|
||||||
for _, otherGmi := range g.members {
|
for _, otherGmi := range g.Members {
|
||||||
if otherGmi.MentorTargetCharID == charID {
|
if otherGmi.MentorTargetCharID == charID {
|
||||||
otherGmi.MentorTargetCharID = 0
|
otherGmi.MentorTargetCharID = 0
|
||||||
// TODO: Enable reset mentorship on client
|
// TODO: Enable reset mentorship on client
|
||||||
@ -243,7 +325,7 @@ func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove from slice
|
// 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()
|
g.updateLastActivity()
|
||||||
|
|
||||||
// Send group update
|
// 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
|
// Disband disbands the group and removes all members
|
||||||
func (g *Group) Disband() {
|
func (g *Group) Disband() {
|
||||||
g.disbandMutex.Lock()
|
g.disbandMutex.Lock()
|
||||||
if g.disbanded {
|
if g.Disbanded {
|
||||||
g.disbandMutex.Unlock()
|
g.disbandMutex.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.disbanded = true
|
g.Disbanded = true
|
||||||
g.disbandMutex.Unlock()
|
g.disbandMutex.Unlock()
|
||||||
|
|
||||||
// Stop background processing first to avoid deadlock
|
// Stop background processing first to avoid deadlock
|
||||||
@ -275,11 +357,11 @@ func (g *Group) Disband() {
|
|||||||
|
|
||||||
// Clear raid groups
|
// Clear raid groups
|
||||||
g.raidGroupsMutex.Lock()
|
g.raidGroupsMutex.Lock()
|
||||||
g.raidGroups = nil
|
g.RaidGroups = nil
|
||||||
g.raidGroupsMutex.Unlock()
|
g.raidGroupsMutex.Unlock()
|
||||||
|
|
||||||
// Remove all members
|
// Remove all members
|
||||||
for _, gmi := range g.members {
|
for _, gmi := range g.Members {
|
||||||
if gmi.Member != nil {
|
if gmi.Member != nil {
|
||||||
// Clear group reference on entity
|
// Clear group reference on entity
|
||||||
// TODO: Clear group member info on entity
|
// TODO: Clear group member info on entity
|
||||||
@ -310,7 +392,7 @@ func (g *Group) Disband() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear members list
|
// Clear members list
|
||||||
g.members = nil
|
g.Members = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendGroupUpdate sends an update to all group members
|
// 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
|
// sendGroupUpdate internal method to send group updates
|
||||||
func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) {
|
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.ExcludeClient = excludeClient
|
||||||
update.ForceRaidUpdate = forceRaidUpdate
|
update.ForceRaidUpdate = forceRaidUpdate
|
||||||
|
|
||||||
@ -391,7 +473,7 @@ func (g *Group) MakeLeader(newLeader Entity) error {
|
|||||||
var newLeaderGMI *GroupMemberInfo
|
var newLeaderGMI *GroupMemberInfo
|
||||||
|
|
||||||
// Find the new leader and update leadership
|
// Find the new leader and update leadership
|
||||||
for _, gmi := range g.members {
|
for _, gmi := range g.Members {
|
||||||
if gmi.Member == newLeader {
|
if gmi.Member == newLeader {
|
||||||
newLeaderGMI = gmi
|
newLeaderGMI = gmi
|
||||||
gmi.Leader = true
|
gmi.Leader = true
|
||||||
@ -418,7 +500,7 @@ func (g *Group) GetLeaderName() string {
|
|||||||
g.membersMutex.RLock()
|
g.membersMutex.RLock()
|
||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
for _, gmi := range g.members {
|
for _, gmi := range g.Members {
|
||||||
if gmi.Leader {
|
if gmi.Leader {
|
||||||
return gmi.Name
|
return gmi.Name
|
||||||
}
|
}
|
||||||
@ -446,7 +528,7 @@ func (g *Group) UpdateGroupMemberInfo(member Entity, groupMembersLocked bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the member and update their info
|
// Find the member and update their info
|
||||||
for _, gmi := range g.members {
|
for _, gmi := range g.Members {
|
||||||
if gmi.Member == member {
|
if gmi.Member == member {
|
||||||
gmi.UpdateStats()
|
gmi.UpdateStats()
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
@ -460,11 +542,11 @@ func (g *Group) GetGroupMemberByPosition(seeker Entity, mappedPosition int32) En
|
|||||||
g.membersMutex.RLock()
|
g.membersMutex.RLock()
|
||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
if mappedPosition < 0 || int(mappedPosition) >= len(g.members) {
|
if mappedPosition < 0 || int(mappedPosition) >= len(g.Members) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.members[mappedPosition].Member
|
return g.Members[mappedPosition].Member
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroupOptions returns a copy of the group options
|
// GetGroupOptions returns a copy of the group options
|
||||||
@ -472,7 +554,7 @@ func (g *Group) GetGroupOptions() GroupOptions {
|
|||||||
g.optionsMutex.RLock()
|
g.optionsMutex.RLock()
|
||||||
defer g.optionsMutex.RUnlock()
|
defer g.optionsMutex.RUnlock()
|
||||||
|
|
||||||
return g.options.Copy()
|
return g.Options.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetGroupOptions sets new group options
|
// SetGroupOptions sets new group options
|
||||||
@ -486,13 +568,13 @@ func (g *Group) SetGroupOptions(options *GroupOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.optionsMutex.Lock()
|
g.optionsMutex.Lock()
|
||||||
g.options = *options
|
g.Options = *options
|
||||||
g.optionsMutex.Unlock()
|
g.optionsMutex.Unlock()
|
||||||
|
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
|
|
||||||
// Send group update for options change
|
// 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
|
update.Options = options
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -509,13 +591,13 @@ func (g *Group) GetLastLooterIndex() int8 {
|
|||||||
g.optionsMutex.RLock()
|
g.optionsMutex.RLock()
|
||||||
defer g.optionsMutex.RUnlock()
|
defer g.optionsMutex.RUnlock()
|
||||||
|
|
||||||
return g.options.LastLootedIndex
|
return g.Options.LastLootedIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNextLooterIndex sets the next looter index
|
// SetNextLooterIndex sets the next looter index
|
||||||
func (g *Group) SetNextLooterIndex(newIndex int8) {
|
func (g *Group) SetNextLooterIndex(newIndex int8) {
|
||||||
g.optionsMutex.Lock()
|
g.optionsMutex.Lock()
|
||||||
g.options.LastLootedIndex = newIndex
|
g.Options.LastLootedIndex = newIndex
|
||||||
g.optionsMutex.Unlock()
|
g.optionsMutex.Unlock()
|
||||||
|
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
@ -528,12 +610,12 @@ func (g *Group) GetRaidGroups() []int32 {
|
|||||||
g.raidGroupsMutex.RLock()
|
g.raidGroupsMutex.RLock()
|
||||||
defer g.raidGroupsMutex.RUnlock()
|
defer g.raidGroupsMutex.RUnlock()
|
||||||
|
|
||||||
if g.raidGroups == nil {
|
if g.RaidGroups == nil {
|
||||||
return []int32{}
|
return []int32{}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make([]int32, len(g.raidGroups))
|
groups := make([]int32, len(g.RaidGroups))
|
||||||
copy(groups, g.raidGroups)
|
copy(groups, g.RaidGroups)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,10 +625,10 @@ func (g *Group) ReplaceRaidGroups(groups []int32) {
|
|||||||
defer g.raidGroupsMutex.Unlock()
|
defer g.raidGroupsMutex.Unlock()
|
||||||
|
|
||||||
if groups == nil {
|
if groups == nil {
|
||||||
g.raidGroups = make([]int32, 0)
|
g.RaidGroups = make([]int32, 0)
|
||||||
} else {
|
} else {
|
||||||
g.raidGroups = make([]int32, len(groups))
|
g.RaidGroups = make([]int32, len(groups))
|
||||||
copy(g.raidGroups, groups)
|
copy(g.RaidGroups, groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
@ -557,7 +639,7 @@ func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool {
|
|||||||
g.raidGroupsMutex.RLock()
|
g.raidGroupsMutex.RLock()
|
||||||
defer g.raidGroupsMutex.RUnlock()
|
defer g.raidGroupsMutex.RUnlock()
|
||||||
|
|
||||||
for _, id := range g.raidGroups {
|
for _, id := range g.RaidGroups {
|
||||||
if id == groupID {
|
if id == groupID {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -572,13 +654,13 @@ func (g *Group) AddGroupToRaid(groupID int32) {
|
|||||||
defer g.raidGroupsMutex.Unlock()
|
defer g.raidGroupsMutex.Unlock()
|
||||||
|
|
||||||
// Check if already in raid
|
// Check if already in raid
|
||||||
for _, id := range g.raidGroups {
|
for _, id := range g.RaidGroups {
|
||||||
if id == groupID {
|
if id == groupID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g.raidGroups = append(g.raidGroups, groupID)
|
g.RaidGroups = append(g.RaidGroups, groupID)
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,9 +669,9 @@ func (g *Group) RemoveGroupFromRaid(groupID int32) {
|
|||||||
g.raidGroupsMutex.Lock()
|
g.raidGroupsMutex.Lock()
|
||||||
defer g.raidGroupsMutex.Unlock()
|
defer g.raidGroupsMutex.Unlock()
|
||||||
|
|
||||||
for i, id := range g.raidGroups {
|
for i, id := range g.RaidGroups {
|
||||||
if id == groupID {
|
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()
|
g.updateLastActivity()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -601,7 +683,7 @@ func (g *Group) IsGroupRaid() bool {
|
|||||||
g.raidGroupsMutex.RLock()
|
g.raidGroupsMutex.RLock()
|
||||||
defer g.raidGroupsMutex.RUnlock()
|
defer g.raidGroupsMutex.RUnlock()
|
||||||
|
|
||||||
return len(g.raidGroups) > 0
|
return len(g.RaidGroups) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearGroupRaid clears all raid associations
|
// ClearGroupRaid clears all raid associations
|
||||||
@ -609,7 +691,7 @@ func (g *Group) ClearGroupRaid() {
|
|||||||
g.raidGroupsMutex.Lock()
|
g.raidGroupsMutex.Lock()
|
||||||
defer g.raidGroupsMutex.Unlock()
|
defer g.raidGroupsMutex.Unlock()
|
||||||
|
|
||||||
g.raidGroups = make([]int32, 0)
|
g.RaidGroups = make([]int32, 0)
|
||||||
g.updateLastActivity()
|
g.updateLastActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,26 +700,26 @@ func (g *Group) IsDisbanded() bool {
|
|||||||
g.disbandMutex.RLock()
|
g.disbandMutex.RLock()
|
||||||
defer g.disbandMutex.RUnlock()
|
defer g.disbandMutex.RUnlock()
|
||||||
|
|
||||||
return g.disbanded
|
return g.Disbanded
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCreatedTime returns when the group was created
|
// GetCreatedTime returns when the group was created
|
||||||
func (g *Group) GetCreatedTime() time.Time {
|
func (g *Group) GetCreatedTime() time.Time {
|
||||||
return g.createdTime
|
return g.CreatedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLastActivity returns the last activity time
|
// GetLastActivity returns the last activity time
|
||||||
func (g *Group) GetLastActivity() time.Time {
|
func (g *Group) GetLastActivity() time.Time {
|
||||||
g.activityMutex.RLock()
|
g.activityMutex.RLock()
|
||||||
defer g.activityMutex.RUnlock()
|
defer g.activityMutex.RUnlock()
|
||||||
return g.lastActivity
|
return g.LastActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateLastActivity updates the last activity timestamp
|
// updateLastActivity updates the last activity timestamp
|
||||||
func (g *Group) updateLastActivity() {
|
func (g *Group) updateLastActivity() {
|
||||||
g.activityMutex.Lock()
|
g.activityMutex.Lock()
|
||||||
defer g.activityMutex.Unlock()
|
defer g.activityMutex.Unlock()
|
||||||
g.lastActivity = time.Now()
|
g.LastActivity = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// processMessages processes messages and updates in the background
|
// processMessages processes messages and updates in the background
|
||||||
@ -666,7 +748,7 @@ func (g *Group) handleMessage(msg *GroupMessage) {
|
|||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
// Send message to all group members except the excluded client
|
// 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 {
|
if gmi.Client != nil && gmi.Client != msg.ExcludeClient {
|
||||||
// TODO: Send message to client
|
// TODO: Send message to client
|
||||||
// This would require integration with the client system
|
// This would require integration with the client system
|
||||||
@ -684,7 +766,7 @@ func (g *Group) handleUpdate(update *GroupUpdate) {
|
|||||||
defer g.membersMutex.RUnlock()
|
defer g.membersMutex.RUnlock()
|
||||||
|
|
||||||
// Send update to all group members except the excluded client
|
// 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 {
|
if gmi.Client != nil && gmi.Client != update.ExcludeClient {
|
||||||
// TODO: Send update to client
|
// TODO: Send update to client
|
||||||
// This would require integration with the client system
|
// This would require integration with the client system
|
||||||
|
@ -110,7 +110,7 @@ func TestGroupCreation(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
group := NewGroup(tt.groupID, tt.options)
|
group := NewGroup(tt.groupID, tt.options, nil)
|
||||||
|
|
||||||
if (group == nil) != tt.expectNil {
|
if (group == nil) != tt.expectNil {
|
||||||
t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil)
|
t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil)
|
||||||
@ -133,7 +133,7 @@ func TestGroupCreation(t *testing.T) {
|
|||||||
|
|
||||||
// TestGroupMemberManagement tests adding and removing members
|
// TestGroupMemberManagement tests adding and removing members
|
||||||
func TestGroupMemberManagement(t *testing.T) {
|
func TestGroupMemberManagement(t *testing.T) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
leader := createMockEntity(1, "Leader", true)
|
leader := createMockEntity(1, "Leader", true)
|
||||||
@ -190,7 +190,7 @@ func TestGroupMemberManagement(t *testing.T) {
|
|||||||
|
|
||||||
// TestGroupLeadership tests leadership transfer
|
// TestGroupLeadership tests leadership transfer
|
||||||
func TestGroupLeadership(t *testing.T) {
|
func TestGroupLeadership(t *testing.T) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
leader := createMockEntity(1, "Leader", true)
|
leader := createMockEntity(1, "Leader", true)
|
||||||
@ -227,7 +227,7 @@ func TestGroupLeadership(t *testing.T) {
|
|||||||
|
|
||||||
// TestGroupOptions tests group options management
|
// TestGroupOptions tests group options management
|
||||||
func TestGroupOptions(t *testing.T) {
|
func TestGroupOptions(t *testing.T) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
// Test default options
|
// Test default options
|
||||||
@ -257,7 +257,7 @@ func TestGroupOptions(t *testing.T) {
|
|||||||
|
|
||||||
// TestGroupRaidFunctionality tests raid-related functionality
|
// TestGroupRaidFunctionality tests raid-related functionality
|
||||||
func TestGroupRaidFunctionality(t *testing.T) {
|
func TestGroupRaidFunctionality(t *testing.T) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
// Initially not a raid
|
// Initially not a raid
|
||||||
@ -288,7 +288,7 @@ func TestGroupRaidFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// TestGroupConcurrency tests concurrent access to group operations
|
// TestGroupConcurrency tests concurrent access to group operations
|
||||||
func TestGroupConcurrency(t *testing.T) {
|
func TestGroupConcurrency(t *testing.T) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
const numGoroutines = 100
|
const numGoroutines = 100
|
||||||
@ -425,7 +425,7 @@ func TestGroupManagerCreation(t *testing.T) {
|
|||||||
EnableStatistics: true,
|
EnableStatistics: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
if manager == nil {
|
if manager == nil {
|
||||||
@ -451,7 +451,7 @@ func TestGroupManagerGroupOperations(t *testing.T) {
|
|||||||
EnableQuestSharing: true,
|
EnableQuestSharing: true,
|
||||||
EnableStatistics: false, // Disable statistics for testing
|
EnableStatistics: false, // Disable statistics for testing
|
||||||
}
|
}
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
leader := createMockEntity(1, "Leader", true)
|
leader := createMockEntity(1, "Leader", true)
|
||||||
@ -520,7 +520,7 @@ func TestGroupManagerInvitations(t *testing.T) {
|
|||||||
config := GroupManagerConfig{
|
config := GroupManagerConfig{
|
||||||
MaxGroups: 1000,
|
MaxGroups: 1000,
|
||||||
MaxRaidGroups: 4,
|
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
|
UpdateInterval: 0, // Disable background updates for testing
|
||||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||||
EnableCrossServer: true,
|
EnableCrossServer: true,
|
||||||
@ -528,7 +528,7 @@ func TestGroupManagerInvitations(t *testing.T) {
|
|||||||
EnableQuestSharing: true,
|
EnableQuestSharing: true,
|
||||||
EnableStatistics: false, // Disable statistics for testing
|
EnableStatistics: false, // Disable statistics for testing
|
||||||
}
|
}
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
leader := createMockEntity(1, "Leader", true)
|
leader := createMockEntity(1, "Leader", true)
|
||||||
@ -571,8 +571,8 @@ func TestGroupManagerInvitations(t *testing.T) {
|
|||||||
member2 := createMockEntity(3, "Member2", true)
|
member2 := createMockEntity(3, "Member2", true)
|
||||||
manager.Invite(leader, member2)
|
manager.Invite(leader, member2)
|
||||||
|
|
||||||
// Wait for timeout
|
// Wait for timeout (now timeout is 200ms so wait 250ms)
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
|
||||||
// Try to accept after timeout (will fail due to missing leader lookup,
|
// Try to accept after timeout (will fail due to missing leader lookup,
|
||||||
// but we're mainly testing that the invite was cleaned up)
|
// but we're mainly testing that the invite was cleaned up)
|
||||||
@ -600,7 +600,7 @@ func TestGroupManagerConcurrency(t *testing.T) {
|
|||||||
EnableQuestSharing: true,
|
EnableQuestSharing: true,
|
||||||
EnableStatistics: false, // Disable statistics for testing
|
EnableStatistics: false, // Disable statistics for testing
|
||||||
}
|
}
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
const numGoroutines = 50
|
const numGoroutines = 50
|
||||||
@ -726,7 +726,7 @@ func TestRaceConditions(t *testing.T) {
|
|||||||
EnableQuestSharing: true,
|
EnableQuestSharing: true,
|
||||||
EnableStatistics: false, // Disable statistics for testing
|
EnableStatistics: false, // Disable statistics for testing
|
||||||
}
|
}
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
const numGoroutines = 100
|
const numGoroutines = 100
|
||||||
@ -794,13 +794,13 @@ func TestRaceConditions(t *testing.T) {
|
|||||||
func BenchmarkGroupOperations(b *testing.B) {
|
func BenchmarkGroupOperations(b *testing.B) {
|
||||||
b.Run("GroupCreation", func(b *testing.B) {
|
b.Run("GroupCreation", func(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
group := NewGroup(int32(i), nil)
|
group := NewGroup(int32(i), nil, nil)
|
||||||
group.Disband()
|
group.Disband()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Run("MemberAddition", func(b *testing.B) {
|
b.Run("MemberAddition", func(b *testing.B) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@ -812,7 +812,7 @@ func BenchmarkGroupOperations(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("ConcurrentMemberAccess", func(b *testing.B) {
|
b.Run("ConcurrentMemberAccess", func(b *testing.B) {
|
||||||
group := NewGroup(1, nil)
|
group := NewGroup(1, nil, nil)
|
||||||
defer group.Disband()
|
defer group.Disband()
|
||||||
|
|
||||||
// Add some members
|
// Add some members
|
||||||
@ -841,7 +841,7 @@ func BenchmarkGroupOperations(b *testing.B) {
|
|||||||
EnableQuestSharing: true,
|
EnableQuestSharing: true,
|
||||||
EnableStatistics: false, // Disable statistics for testing
|
EnableStatistics: false, // Disable statistics for testing
|
||||||
}
|
}
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
defer manager.Stop()
|
defer manager.Stop()
|
||||||
|
|
||||||
// Create some groups
|
// 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,
|
EnableStatistics: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -49,7 +49,7 @@ func TestManagerGroupLifecycle(t *testing.T) {
|
|||||||
EnableStatistics: false,
|
EnableStatistics: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -134,7 +134,7 @@ func TestManagerInviteSystem(t *testing.T) {
|
|||||||
EnableStatistics: false,
|
EnableStatistics: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -200,7 +200,7 @@ func TestManagerRaidOperations(t *testing.T) {
|
|||||||
EnableStatistics: false,
|
EnableStatistics: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -271,7 +271,7 @@ func TestManagerConcurrentOperations(t *testing.T) {
|
|||||||
EnableStatistics: false,
|
EnableStatistics: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -343,7 +343,7 @@ func TestManagerStatistics(t *testing.T) {
|
|||||||
EnableStatistics: true,
|
EnableStatistics: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
err := manager.Start()
|
err := manager.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start manager: %v", err)
|
t.Fatalf("Failed to start manager: %v", err)
|
||||||
@ -416,7 +416,7 @@ func TestManagerEventHandlers(t *testing.T) {
|
|||||||
EnableStatistics: false,
|
EnableStatistics: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := NewGroupManager(config)
|
manager := NewManager(config, nil)
|
||||||
|
|
||||||
// Track events
|
// Track events
|
||||||
events := make([]string, 0)
|
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
|
// GroupOptions holds group configuration settings
|
||||||
type GroupOptions struct {
|
type GroupOptions struct {
|
||||||
LootMethod int8 `json:"loot_method"`
|
LootMethod int8 `json:"loot_method" db:"loot_method"`
|
||||||
LootItemsRarity int8 `json:"loot_items_rarity"`
|
LootItemsRarity int8 `json:"loot_items_rarity" db:"loot_items_rarity"`
|
||||||
AutoSplit int8 `json:"auto_split"`
|
AutoSplit int8 `json:"auto_split" db:"auto_split"`
|
||||||
DefaultYell int8 `json:"default_yell"`
|
DefaultYell int8 `json:"default_yell" db:"default_yell"`
|
||||||
GroupLockMethod int8 `json:"group_lock_method"`
|
GroupLockMethod int8 `json:"group_lock_method" db:"group_lock_method"`
|
||||||
GroupAutolock int8 `json:"group_autolock"`
|
GroupAutolock int8 `json:"group_autolock" db:"group_autolock"`
|
||||||
SoloAutolock int8 `json:"solo_autolock"`
|
SoloAutolock int8 `json:"solo_autolock" db:"solo_autolock"`
|
||||||
AutoLootMethod int8 `json:"auto_loot_method"`
|
AutoLootMethod int8 `json:"auto_loot_method" db:"auto_loot_method"`
|
||||||
LastLootedIndex int8 `json:"last_looted_index"`
|
LastLootedIndex int8 `json:"last_looted_index" db:"last_looted_index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupMemberInfo contains all information about a group member
|
// GroupMemberInfo contains all information about a group member
|
||||||
type GroupMemberInfo struct {
|
type GroupMemberInfo struct {
|
||||||
// Group and member identification
|
// Group and member identification
|
||||||
GroupID int32 `json:"group_id"`
|
GroupID int32 `json:"group_id" db:"group_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Zone string `json:"zone"`
|
Zone string `json:"zone" db:"zone"`
|
||||||
|
|
||||||
// Health and power stats
|
// Health and power stats
|
||||||
HPCurrent int32 `json:"hp_current"`
|
HPCurrent int32 `json:"hp_current" db:"hp_current"`
|
||||||
HPMax int32 `json:"hp_max"`
|
HPMax int32 `json:"hp_max" db:"hp_max"`
|
||||||
PowerCurrent int32 `json:"power_current"`
|
PowerCurrent int32 `json:"power_current" db:"power_current"`
|
||||||
PowerMax int32 `json:"power_max"`
|
PowerMax int32 `json:"power_max" db:"power_max"`
|
||||||
|
|
||||||
// Level and character info
|
// Level and character info
|
||||||
LevelCurrent int16 `json:"level_current"`
|
LevelCurrent int16 `json:"level_current" db:"level_current"`
|
||||||
LevelMax int16 `json:"level_max"`
|
LevelMax int16 `json:"level_max" db:"level_max"`
|
||||||
RaceID int8 `json:"race_id"`
|
RaceID int8 `json:"race_id" db:"race_id"`
|
||||||
ClassID int8 `json:"class_id"`
|
ClassID int8 `json:"class_id" db:"class_id"`
|
||||||
|
|
||||||
// Group status
|
// Group status
|
||||||
Leader bool `json:"leader"`
|
Leader bool `json:"leader" db:"leader"`
|
||||||
IsClient bool `json:"is_client"`
|
IsClient bool `json:"is_client" db:"is_client"`
|
||||||
IsRaidLooter bool `json:"is_raid_looter"`
|
IsRaidLooter bool `json:"is_raid_looter" db:"is_raid_looter"`
|
||||||
|
|
||||||
// Zone and instance info
|
// Zone and instance info
|
||||||
ZoneID int32 `json:"zone_id"`
|
ZoneID int32 `json:"zone_id" db:"zone_id"`
|
||||||
InstanceID int32 `json:"instance_id"`
|
InstanceID int32 `json:"instance_id" db:"instance_id"`
|
||||||
|
|
||||||
// Mentoring
|
// 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
|
// Network info for cross-server groups
|
||||||
ClientPeerAddress string `json:"client_peer_address"`
|
ClientPeerAddress string `json:"client_peer_address" db:"client_peer_address"`
|
||||||
ClientPeerPort int16 `json:"client_peer_port"`
|
ClientPeerPort int16 `json:"client_peer_port" db:"client_peer_port"`
|
||||||
|
|
||||||
// Entity reference (local members only)
|
// Entity reference (local members only)
|
||||||
Member Entity `json:"-"`
|
Member Entity `json:"-"`
|
||||||
@ -60,44 +60,11 @@ type GroupMemberInfo struct {
|
|||||||
Client any `json:"-"`
|
Client any `json:"-"`
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
JoinTime time.Time `json:"join_time"`
|
JoinTime time.Time `json:"join_time" db:"join_time"`
|
||||||
LastUpdate time.Time `json:"last_update"`
|
LastUpdate time.Time `json:"last_update" db:"last_update"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group represents a player group
|
// Group is now defined in group.go - this type definition removed to avoid duplication
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupMessage represents a message sent to the group
|
// GroupMessage represents a message sent to the group
|
||||||
type GroupMessage struct {
|
type GroupMessage struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user