first pass on group tests
This commit is contained in:
parent
0534c49610
commit
e35e41f643
42
internal/groups/entity_interface.go
Normal file
42
internal/groups/entity_interface.go
Normal file
@ -0,0 +1,42 @@
|
||||
package groups
|
||||
|
||||
// Entity is the interface for entities that can be part of groups
|
||||
// This interface is implemented by Player, NPC, and Bot types
|
||||
type Entity interface {
|
||||
// Basic entity information
|
||||
GetID() int32
|
||||
GetName() string
|
||||
GetLevel() int8
|
||||
GetClass() int8
|
||||
GetRace() int8
|
||||
|
||||
// Health and power
|
||||
GetHP() int32
|
||||
GetTotalHP() int32
|
||||
GetPower() int32
|
||||
GetTotalPower() int32
|
||||
|
||||
// Entity type checks
|
||||
IsPlayer() bool
|
||||
IsNPC() bool
|
||||
IsBot() bool
|
||||
IsDead() bool
|
||||
|
||||
// Zone information
|
||||
GetZone() Zone
|
||||
|
||||
// Distance calculation
|
||||
GetDistance(other Entity) float32
|
||||
}
|
||||
|
||||
// Zone interface for zone information
|
||||
type Zone interface {
|
||||
GetZoneID() int32
|
||||
GetInstanceID() int32
|
||||
GetZoneName() string
|
||||
}
|
||||
|
||||
// Spawn interface for distance calculations
|
||||
type Spawn interface {
|
||||
// Minimal spawn interface for distance calculations
|
||||
}
|
@ -3,8 +3,6 @@ package groups
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// NewGroup creates a new group with the given ID and options
|
||||
@ -61,7 +59,7 @@ func (g *Group) GetMembers() []*GroupMemberInfo {
|
||||
}
|
||||
|
||||
// AddMember adds a new member to the group
|
||||
func (g *Group) AddMember(member entity.Entity, isLeader bool) error {
|
||||
func (g *Group) AddMember(member Entity, isLeader bool) error {
|
||||
if member == nil {
|
||||
return fmt.Errorf("member cannot be nil")
|
||||
}
|
||||
@ -188,7 +186,7 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID
|
||||
}
|
||||
|
||||
// RemoveMember removes a member from the group
|
||||
func (g *Group) RemoveMember(member entity.Entity) error {
|
||||
func (g *Group) RemoveMember(member Entity) error {
|
||||
if member == nil {
|
||||
return fmt.Errorf("member cannot be nil")
|
||||
}
|
||||
@ -356,7 +354,7 @@ func (g *Group) SendGroupMessage(msgType int8, message string) {
|
||||
}
|
||||
|
||||
// GroupChatMessage sends a chat message from a member to the group
|
||||
func (g *Group) GroupChatMessage(from entity.Entity, language int32, message string, channel int16) {
|
||||
func (g *Group) GroupChatMessage(from Entity, language int32, message string, channel int16) {
|
||||
if from == nil {
|
||||
return
|
||||
}
|
||||
@ -382,7 +380,7 @@ func (g *Group) GroupChatMessageFromName(fromName string, language int32, messag
|
||||
}
|
||||
|
||||
// MakeLeader changes the group leader
|
||||
func (g *Group) MakeLeader(newLeader entity.Entity) error {
|
||||
func (g *Group) MakeLeader(newLeader Entity) error {
|
||||
if newLeader == nil {
|
||||
return fmt.Errorf("new leader cannot be nil")
|
||||
}
|
||||
@ -437,7 +435,7 @@ func (g *Group) ShareQuestWithGroup(questSharer any, quest any) bool {
|
||||
}
|
||||
|
||||
// UpdateGroupMemberInfo updates information for a specific member
|
||||
func (g *Group) UpdateGroupMemberInfo(member entity.Entity, groupMembersLocked bool) {
|
||||
func (g *Group) UpdateGroupMemberInfo(member Entity, groupMembersLocked bool) {
|
||||
if member == nil {
|
||||
return
|
||||
}
|
||||
@ -458,7 +456,7 @@ func (g *Group) UpdateGroupMemberInfo(member entity.Entity, groupMembersLocked b
|
||||
}
|
||||
|
||||
// GetGroupMemberByPosition returns a group member at a specific position
|
||||
func (g *Group) GetGroupMemberByPosition(seeker entity.Entity, mappedPosition int32) entity.Entity {
|
||||
func (g *Group) GetGroupMemberByPosition(seeker Entity, mappedPosition int32) Entity {
|
||||
g.membersMutex.RLock()
|
||||
defer g.membersMutex.RUnlock()
|
||||
|
||||
@ -681,8 +679,6 @@ func (g *Group) handleUpdate(update *GroupUpdate) {
|
||||
g.membersMutex.RLock()
|
||||
defer g.membersMutex.RUnlock()
|
||||
|
||||
isInRaid := g.IsGroupRaid()
|
||||
|
||||
// Send update to all group members except the excluded client
|
||||
for _, gmi := range g.members {
|
||||
if gmi.Client != nil && gmi.Client != update.ExcludeClient {
|
||||
|
863
internal/groups/groups_test.go
Normal file
863
internal/groups/groups_test.go
Normal file
@ -0,0 +1,863 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock entity implementation for testing
|
||||
type mockEntity struct {
|
||||
id int32
|
||||
name string
|
||||
level int8
|
||||
class int8
|
||||
race int8
|
||||
hp int32
|
||||
maxHP int32
|
||||
power int32
|
||||
maxPower int32
|
||||
isPlayer bool
|
||||
isNPC bool
|
||||
isBot bool
|
||||
isDead bool
|
||||
zone *mockZone
|
||||
groupID int32
|
||||
groupInfo *GroupMemberInfo
|
||||
}
|
||||
|
||||
func (m *mockEntity) GetID() int32 { return m.id }
|
||||
func (m *mockEntity) GetName() string { return m.name }
|
||||
func (m *mockEntity) GetLevel() int8 { return m.level }
|
||||
func (m *mockEntity) GetClass() int8 { return m.class }
|
||||
func (m *mockEntity) GetRace() int8 { return m.race }
|
||||
func (m *mockEntity) GetHP() int32 { return m.hp }
|
||||
func (m *mockEntity) GetTotalHP() int32 { return m.maxHP }
|
||||
func (m *mockEntity) GetPower() int32 { return m.power }
|
||||
func (m *mockEntity) GetTotalPower() int32 { return m.maxPower }
|
||||
func (m *mockEntity) IsPlayer() bool { return m.isPlayer }
|
||||
func (m *mockEntity) IsNPC() bool { return m.isNPC }
|
||||
func (m *mockEntity) IsBot() bool { return m.isBot }
|
||||
func (m *mockEntity) IsDead() bool { return m.isDead }
|
||||
func (m *mockEntity) GetZone() Zone { return m.zone }
|
||||
func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 }
|
||||
|
||||
// GroupAware implementation
|
||||
func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo }
|
||||
func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info }
|
||||
func (m *mockEntity) GetGroupID() int32 { return m.groupID }
|
||||
func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID }
|
||||
func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 }
|
||||
|
||||
// Mock zone implementation
|
||||
type mockZone struct {
|
||||
zoneID int32
|
||||
instanceID int32
|
||||
zoneName string
|
||||
}
|
||||
|
||||
func (m *mockZone) GetZoneID() int32 { return m.zoneID }
|
||||
func (m *mockZone) GetInstanceID() int32 { return m.instanceID }
|
||||
func (m *mockZone) GetZoneName() string { return m.zoneName }
|
||||
|
||||
// Helper function to create mock entities
|
||||
func createMockEntity(id int32, name string, isPlayer bool) *mockEntity {
|
||||
return &mockEntity{
|
||||
id: id,
|
||||
name: name,
|
||||
level: 50,
|
||||
class: 1,
|
||||
race: 0,
|
||||
hp: 1500,
|
||||
maxHP: 1500,
|
||||
power: 800,
|
||||
maxPower: 800,
|
||||
isPlayer: isPlayer,
|
||||
zone: &mockZone{
|
||||
zoneID: 220,
|
||||
instanceID: 1,
|
||||
zoneName: "commonlands",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupCreation tests basic group creation
|
||||
func TestGroupCreation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
groupID int32
|
||||
options *GroupOptions
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "Create group with default options",
|
||||
groupID: 1,
|
||||
options: nil,
|
||||
expectNil: false,
|
||||
},
|
||||
{
|
||||
name: "Create group with custom options",
|
||||
groupID: 2,
|
||||
options: &GroupOptions{
|
||||
LootMethod: LOOT_METHOD_NEED_BEFORE_GREED,
|
||||
LootItemsRarity: LOOT_RARITY_RARE,
|
||||
AutoSplit: AUTO_SPLIT_ENABLED,
|
||||
},
|
||||
expectNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
group := NewGroup(tt.groupID, tt.options)
|
||||
|
||||
if (group == nil) != tt.expectNil {
|
||||
t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil)
|
||||
return
|
||||
}
|
||||
|
||||
if group.GetID() != tt.groupID {
|
||||
t.Errorf("Group ID = %d, want %d", group.GetID(), tt.groupID)
|
||||
}
|
||||
|
||||
if group.GetSize() != 0 {
|
||||
t.Errorf("Initial group size = %d, want 0", group.GetSize())
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
group.Disband()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupMemberManagement tests adding and removing members
|
||||
func TestGroupMemberManagement(t *testing.T) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
leader := createMockEntity(1, "Leader", true)
|
||||
member1 := createMockEntity(2, "Member1", true)
|
||||
member2 := createMockEntity(3, "Member2", true)
|
||||
|
||||
// Test adding leader
|
||||
err := group.AddMember(leader, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add leader: %v", err)
|
||||
}
|
||||
|
||||
if group.GetSize() != 1 {
|
||||
t.Errorf("Group size after adding leader = %d, want 1", group.GetSize())
|
||||
}
|
||||
|
||||
// Test adding members
|
||||
err = group.AddMember(member1, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add member1: %v", err)
|
||||
}
|
||||
|
||||
err = group.AddMember(member2, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add member2: %v", err)
|
||||
}
|
||||
|
||||
if group.GetSize() != 3 {
|
||||
t.Errorf("Group size after adding members = %d, want 3", group.GetSize())
|
||||
}
|
||||
|
||||
// Test duplicate member
|
||||
err = group.AddMember(member1, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error when adding duplicate member")
|
||||
}
|
||||
|
||||
// Test member removal
|
||||
err = group.RemoveMember(member1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove member1: %v", err)
|
||||
}
|
||||
|
||||
if group.GetSize() != 2 {
|
||||
t.Errorf("Group size after removing member = %d, want 2", group.GetSize())
|
||||
}
|
||||
|
||||
// Test removing non-existent member
|
||||
err = group.RemoveMember(member1)
|
||||
if err == nil {
|
||||
t.Error("Expected error when removing non-existent member")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupLeadership tests leadership transfer
|
||||
func TestGroupLeadership(t *testing.T) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
leader := createMockEntity(1, "Leader", true)
|
||||
member1 := createMockEntity(2, "Member1", true)
|
||||
member2 := createMockEntity(3, "Member2", true)
|
||||
|
||||
// Add members
|
||||
group.AddMember(leader, true)
|
||||
group.AddMember(member1, false)
|
||||
group.AddMember(member2, false)
|
||||
|
||||
// Test initial leader
|
||||
if group.GetLeaderName() != "Leader" {
|
||||
t.Errorf("Initial leader name = %s, want Leader", group.GetLeaderName())
|
||||
}
|
||||
|
||||
// Transfer leadership
|
||||
err := group.MakeLeader(member1)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to transfer leadership: %v", err)
|
||||
}
|
||||
|
||||
if group.GetLeaderName() != "Member1" {
|
||||
t.Errorf("New leader name = %s, want Member1", group.GetLeaderName())
|
||||
}
|
||||
|
||||
// Test invalid leadership transfer
|
||||
nonMember := createMockEntity(4, "NonMember", true)
|
||||
err = group.MakeLeader(nonMember)
|
||||
if err == nil {
|
||||
t.Error("Expected failure when making non-member leader")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupOptions tests group options management
|
||||
func TestGroupOptions(t *testing.T) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
// Test default options
|
||||
options := group.GetGroupOptions()
|
||||
if options.LootMethod != LOOT_METHOD_ROUND_ROBIN {
|
||||
t.Errorf("Default loot method = %d, want %d", options.LootMethod, LOOT_METHOD_ROUND_ROBIN)
|
||||
}
|
||||
|
||||
// Test setting options
|
||||
newOptions := GroupOptions{
|
||||
LootMethod: LOOT_METHOD_NEED_BEFORE_GREED,
|
||||
LootItemsRarity: LOOT_RARITY_RARE,
|
||||
AutoSplit: AUTO_SPLIT_ENABLED,
|
||||
GroupLockMethod: LOCK_METHOD_INVITE_ONLY,
|
||||
}
|
||||
|
||||
group.SetGroupOptions(&newOptions)
|
||||
|
||||
options = group.GetGroupOptions()
|
||||
if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED {
|
||||
t.Errorf("Updated loot method = %d, want %d", options.LootMethod, LOOT_METHOD_NEED_BEFORE_GREED)
|
||||
}
|
||||
if options.AutoSplit != AUTO_SPLIT_ENABLED {
|
||||
t.Errorf("Updated auto split = %d, want %d", options.AutoSplit, AUTO_SPLIT_ENABLED)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupRaidFunctionality tests raid-related functionality
|
||||
func TestGroupRaidFunctionality(t *testing.T) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
// Initially not a raid
|
||||
if group.IsGroupRaid() {
|
||||
t.Error("New group should not be a raid")
|
||||
}
|
||||
|
||||
// Add raid groups
|
||||
raidGroups := []int32{1, 2, 3, 4}
|
||||
group.ReplaceRaidGroups(raidGroups)
|
||||
|
||||
if !group.IsGroupRaid() {
|
||||
t.Error("Group should be a raid after setting raid groups")
|
||||
}
|
||||
|
||||
// Test raid group retrieval
|
||||
retrievedGroups := group.GetRaidGroups()
|
||||
if len(retrievedGroups) != len(raidGroups) {
|
||||
t.Errorf("Retrieved raid groups length = %d, want %d", len(retrievedGroups), len(raidGroups))
|
||||
}
|
||||
|
||||
// Clear raid
|
||||
group.ClearGroupRaid()
|
||||
if group.IsGroupRaid() {
|
||||
t.Error("Group should not be a raid after clearing")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupConcurrency tests concurrent access to group operations
|
||||
func TestGroupConcurrency(t *testing.T) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
const numGoroutines = 100
|
||||
const operationsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Test concurrent member additions and removals
|
||||
t.Run("ConcurrentMemberOperations", func(t *testing.T) {
|
||||
// Add initial members
|
||||
for i := 0; i < MAX_GROUP_SIZE-1; i++ {
|
||||
member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true)
|
||||
group.AddMember(member, i == 0)
|
||||
}
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true)
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
if j%2 == 0 {
|
||||
// Try to add member (will mostly fail due to full group)
|
||||
_ = group.AddMember(member, false)
|
||||
} else {
|
||||
// Remove and re-add existing member
|
||||
members := group.GetMembers()
|
||||
if len(members) > 0 {
|
||||
memberIdx := goroutineID % len(members)
|
||||
existingMember := members[memberIdx]
|
||||
_ = group.RemoveMember(existingMember.Member)
|
||||
_ = group.AddMember(existingMember.Member, existingMember.Leader)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
// Test concurrent option updates
|
||||
t.Run("ConcurrentOptionUpdates", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
if j%2 == 0 {
|
||||
// Read options
|
||||
_ = group.GetGroupOptions()
|
||||
} else {
|
||||
// Write options
|
||||
options := GroupOptions{
|
||||
LootMethod: int8(goroutineID % 4),
|
||||
LootItemsRarity: int8(goroutineID % 5),
|
||||
AutoSplit: int8(goroutineID % 2),
|
||||
}
|
||||
group.SetGroupOptions(&options)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
// Test concurrent raid operations
|
||||
t.Run("ConcurrentRaidOperations", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
_ = group.IsGroupRaid()
|
||||
case 1:
|
||||
_ = group.GetRaidGroups()
|
||||
case 2:
|
||||
raidGroups := []int32{int32(goroutineID%4 + 1)}
|
||||
group.ReplaceRaidGroups(raidGroups)
|
||||
case 3:
|
||||
group.ClearGroupRaid()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
// Test concurrent member info updates
|
||||
t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for range operationsPerGoroutine {
|
||||
members := group.GetMembers()
|
||||
if len(members) > 0 {
|
||||
// Update member stats
|
||||
memberIdx := goroutineID % len(members)
|
||||
members[memberIdx].UpdateStats()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// TestGroupManagerCreation tests group manager creation
|
||||
func TestGroupManagerCreation(t *testing.T) {
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 30 * time.Second,
|
||||
UpdateInterval: 1 * time.Second,
|
||||
BuffUpdateInterval: 5 * time.Second,
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: true,
|
||||
}
|
||||
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewGroupManager returned nil")
|
||||
}
|
||||
|
||||
stats := manager.GetStats()
|
||||
if stats.ActiveGroups != 0 {
|
||||
t.Errorf("Initial active groups = %d, want 0", stats.ActiveGroups)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerGroupOperations tests group operations through manager
|
||||
func TestGroupManagerGroupOperations(t *testing.T) {
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 30 * time.Second,
|
||||
UpdateInterval: 0, // Disable background updates for testing
|
||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: false, // Disable statistics for testing
|
||||
}
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
leader := createMockEntity(1, "Leader", true)
|
||||
member1 := createMockEntity(2, "Member1", true)
|
||||
member2 := createMockEntity(3, "Member2", true)
|
||||
|
||||
// Create group
|
||||
groupID, err := manager.NewGroup(leader, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create group: %v", err)
|
||||
}
|
||||
|
||||
if groupID <= 0 {
|
||||
t.Errorf("Invalid group ID: %d", groupID)
|
||||
}
|
||||
|
||||
// Add members
|
||||
err = manager.AddGroupMember(groupID, member1, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add member1: %v", err)
|
||||
}
|
||||
|
||||
err = manager.AddGroupMember(groupID, member2, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add member2: %v", err)
|
||||
}
|
||||
|
||||
// Check group size
|
||||
size := manager.GetGroupSize(groupID)
|
||||
if size != 3 {
|
||||
t.Errorf("Group size = %d, want 3", size)
|
||||
}
|
||||
|
||||
// Test member checks
|
||||
if !manager.IsInGroup(groupID, leader) {
|
||||
t.Error("Leader should be in group")
|
||||
}
|
||||
|
||||
if !manager.IsInGroup(groupID, member1) {
|
||||
t.Error("Member1 should be in group")
|
||||
}
|
||||
|
||||
// Remove member
|
||||
err = manager.RemoveGroupMember(groupID, member1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove member1: %v", err)
|
||||
}
|
||||
|
||||
if manager.IsInGroup(groupID, member1) {
|
||||
t.Error("Member1 should not be in group after removal")
|
||||
}
|
||||
|
||||
// Remove group
|
||||
err = manager.RemoveGroup(groupID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove group: %v", err)
|
||||
}
|
||||
|
||||
if manager.IsGroupIDValid(groupID) {
|
||||
t.Error("Group should not be valid after removal")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerInvitations tests invitation system
|
||||
func TestGroupManagerInvitations(t *testing.T) {
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 2 * time.Second, // Short timeout for testing
|
||||
UpdateInterval: 0, // Disable background updates for testing
|
||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: false, // Disable statistics for testing
|
||||
}
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
leader := createMockEntity(1, "Leader", true)
|
||||
member := createMockEntity(2, "Member", true)
|
||||
|
||||
// Create group
|
||||
groupID, _ := manager.NewGroup(leader, nil, 0)
|
||||
|
||||
// Send invitation
|
||||
result := manager.Invite(leader, member)
|
||||
if result != GROUP_INVITE_SUCCESS {
|
||||
t.Errorf("Invite result = %d, want %d", result, GROUP_INVITE_SUCCESS)
|
||||
}
|
||||
|
||||
// Check pending invite
|
||||
inviterName := manager.HasPendingInvite(member)
|
||||
if inviterName != "Leader" {
|
||||
t.Errorf("Pending invite from = %s, want Leader", inviterName)
|
||||
}
|
||||
|
||||
// Accept invitation (will fail due to missing leader lookup, but that's expected in tests)
|
||||
acceptResult := manager.AcceptInvite(member, nil, true)
|
||||
if acceptResult != GROUP_INVITE_TARGET_NOT_FOUND {
|
||||
t.Logf("Accept invite result = %d (expected due to missing leader lookup in test)", acceptResult)
|
||||
}
|
||||
|
||||
// Since invite acceptance failed due to missing world integration,
|
||||
// let's manually add the member to test the group functionality
|
||||
err := manager.AddGroupMember(groupID, member, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to manually add member: %v", err)
|
||||
}
|
||||
|
||||
// Verify member is in group
|
||||
if !manager.IsInGroup(groupID, member) {
|
||||
t.Error("Member should be in group after adding")
|
||||
}
|
||||
|
||||
// Test invitation timeout
|
||||
member2 := createMockEntity(3, "Member2", true)
|
||||
manager.Invite(leader, member2)
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Try to accept after timeout (will fail due to missing leader lookup,
|
||||
// but we're mainly testing that the invite was cleaned up)
|
||||
acceptResult = manager.AcceptInvite(member2, nil, true)
|
||||
if acceptResult == GROUP_INVITE_SUCCESS {
|
||||
t.Error("Should not be able to accept expired invitation")
|
||||
}
|
||||
|
||||
// Verify the invite was cleaned up by checking it no longer exists
|
||||
if manager.HasPendingInvite(member2) != "" {
|
||||
t.Error("Expired invitation should have been cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerConcurrency tests concurrent manager operations
|
||||
func TestGroupManagerConcurrency(t *testing.T) {
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 30 * time.Second,
|
||||
UpdateInterval: 0, // Disable background updates for testing
|
||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: false, // Disable statistics for testing
|
||||
}
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
const numGoroutines = 50
|
||||
const groupsPerGoroutine = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Test concurrent group creation and removal
|
||||
t.Run("ConcurrentGroupCreation", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range groupsPerGoroutine {
|
||||
leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true)
|
||||
|
||||
// Create group
|
||||
groupID, err := manager.NewGroup(leader, nil, 0)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add some members
|
||||
for k := range 3 {
|
||||
member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true)
|
||||
_ = manager.AddGroupMember(groupID, member, false)
|
||||
}
|
||||
|
||||
// Sometimes remove the group
|
||||
if j%2 == 0 {
|
||||
_ = manager.RemoveGroup(groupID)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
// Test concurrent invitations
|
||||
t.Run("ConcurrentInvitations", func(t *testing.T) {
|
||||
// Create some groups
|
||||
groups := make([]int32, 10)
|
||||
leaders := make([]*mockEntity, 10)
|
||||
|
||||
for i := range 10 {
|
||||
leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true)
|
||||
leaders[i] = leader
|
||||
groupID, _ := manager.NewGroup(leader, nil, 0)
|
||||
groups[i] = groupID
|
||||
}
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 100 {
|
||||
leaderIdx := goroutineID % len(leaders)
|
||||
leader := leaders[leaderIdx]
|
||||
|
||||
member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true)
|
||||
|
||||
// Send invite
|
||||
_ = manager.Invite(leader, member)
|
||||
|
||||
// Sometimes accept, sometimes decline
|
||||
if j%3 == 0 {
|
||||
_ = manager.AcceptInvite(member, nil, false)
|
||||
} else if j%3 == 1 {
|
||||
manager.DeclineInvite(member)
|
||||
}
|
||||
// Otherwise let it expire
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Cleanup groups
|
||||
for _, groupID := range groups {
|
||||
_ = manager.RemoveGroup(groupID)
|
||||
}
|
||||
})
|
||||
|
||||
// Test concurrent statistics updates
|
||||
t.Run("ConcurrentStatistics", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for range 1000 {
|
||||
_ = manager.GetStats()
|
||||
_ = manager.GetGroupCount()
|
||||
_ = manager.GetAllGroups()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// TestRaceConditions tests for race conditions with -race flag
|
||||
func TestRaceConditions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping race condition test in short mode")
|
||||
}
|
||||
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 30 * time.Second,
|
||||
UpdateInterval: 0, // Disable background updates for testing
|
||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: false, // Disable statistics for testing
|
||||
}
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
const numGoroutines = 100
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Create a shared group
|
||||
leader := createMockEntity(1, "RaceLeader", true)
|
||||
groupID, _ := manager.NewGroup(leader, nil, 0)
|
||||
|
||||
// Add some initial members
|
||||
for i := range 5 {
|
||||
member := createMockEntity(int32(i+2), fmt.Sprintf("RaceMember%d", i+1), true)
|
||||
_ = manager.AddGroupMember(groupID, member, false)
|
||||
}
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 50 {
|
||||
switch j % 10 {
|
||||
case 0:
|
||||
// Get group
|
||||
_ = manager.GetGroup(groupID)
|
||||
case 1:
|
||||
// Get size
|
||||
_ = manager.GetGroupSize(groupID)
|
||||
case 2:
|
||||
// Check membership
|
||||
_ = manager.IsInGroup(groupID, leader)
|
||||
case 3:
|
||||
// Get leader
|
||||
_ = manager.GetGroupLeader(groupID)
|
||||
case 4:
|
||||
// Send message
|
||||
manager.SimpleGroupMessage(groupID, fmt.Sprintf("Message %d", goroutineID))
|
||||
case 5:
|
||||
// Update options
|
||||
options := DefaultGroupOptions()
|
||||
options.LootMethod = int8(goroutineID % 4)
|
||||
_ = manager.SetGroupOptions(groupID, &options)
|
||||
case 6:
|
||||
// Get options
|
||||
_, _ = manager.GetDefaultGroupOptions(groupID)
|
||||
case 7:
|
||||
// Send group update
|
||||
manager.SendGroupUpdate(groupID, nil, false)
|
||||
case 8:
|
||||
// Check raid status
|
||||
_ = manager.IsInRaidGroup(groupID, groupID+1, false)
|
||||
case 9:
|
||||
// Get stats
|
||||
_ = manager.GetStats()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkGroupOperations(b *testing.B) {
|
||||
b.Run("GroupCreation", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
group := NewGroup(int32(i), nil)
|
||||
group.Disband()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MemberAddition", func(b *testing.B) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
member := createMockEntity(int32(i), fmt.Sprintf("Member%d", i), true)
|
||||
_ = group.AddMember(member, false)
|
||||
_ = group.RemoveMember(member)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ConcurrentMemberAccess", func(b *testing.B) {
|
||||
group := NewGroup(1, nil)
|
||||
defer group.Disband()
|
||||
|
||||
// Add some members
|
||||
for i := range MAX_GROUP_SIZE {
|
||||
member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true)
|
||||
group.AddMember(member, i == 0)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = group.GetMembers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ManagerGroupLookup", func(b *testing.B) {
|
||||
config := GroupManagerConfig{
|
||||
MaxGroups: 1000,
|
||||
MaxRaidGroups: 4,
|
||||
InviteTimeout: 30 * time.Second,
|
||||
UpdateInterval: 0, // Disable background updates for testing
|
||||
BuffUpdateInterval: 0, // Disable background updates for testing
|
||||
EnableCrossServer: true,
|
||||
EnableRaids: true,
|
||||
EnableQuestSharing: true,
|
||||
EnableStatistics: false, // Disable statistics for testing
|
||||
}
|
||||
manager := NewGroupManager(config)
|
||||
defer manager.Stop()
|
||||
|
||||
// Create some groups
|
||||
for i := range 100 {
|
||||
leader := createMockEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true)
|
||||
manager.NewGroup(leader, nil, 0)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
groupID := int32((i % 100) + 1)
|
||||
_ = manager.GetGroup(groupID)
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"eq2emu/internal/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -26,41 +25,41 @@ type GroupAware interface {
|
||||
// GroupManager interface for managing groups
|
||||
type GroupManagerInterface interface {
|
||||
// Group creation and management
|
||||
NewGroup(leader entity.Entity, options *GroupOptions, overrideGroupID int32) (int32, error)
|
||||
NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error)
|
||||
RemoveGroup(groupID int32) error
|
||||
GetGroup(groupID int32) *Group
|
||||
IsGroupIDValid(groupID int32) bool
|
||||
|
||||
// Member management
|
||||
AddGroupMember(groupID int32, member entity.Entity, isLeader bool) error
|
||||
AddGroupMember(groupID int32, member Entity, isLeader bool) error
|
||||
AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error
|
||||
RemoveGroupMember(groupID int32, member entity.Entity) error
|
||||
RemoveGroupMember(groupID int32, member Entity) error
|
||||
RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error
|
||||
|
||||
// Group updates
|
||||
SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool)
|
||||
|
||||
// Invitations
|
||||
Invite(leader entity.Entity, member entity.Entity) int8
|
||||
AddInvite(leader entity.Entity, member entity.Entity) bool
|
||||
AcceptInvite(member entity.Entity, groupOverrideID *int32, autoAddGroup bool) int8
|
||||
DeclineInvite(member entity.Entity)
|
||||
ClearPendingInvite(member entity.Entity)
|
||||
HasPendingInvite(member entity.Entity) string
|
||||
Invite(leader Entity, member Entity) int8
|
||||
AddInvite(leader Entity, member Entity) bool
|
||||
AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8
|
||||
DeclineInvite(member Entity)
|
||||
ClearPendingInvite(member Entity)
|
||||
HasPendingInvite(member Entity) string
|
||||
|
||||
// Group utilities
|
||||
GetGroupSize(groupID int32) int32
|
||||
IsInGroup(groupID int32, member entity.Entity) bool
|
||||
IsPlayerInGroup(groupID int32, charID int32) entity.Entity
|
||||
IsInGroup(groupID int32, member Entity) bool
|
||||
IsPlayerInGroup(groupID int32, charID int32) Entity
|
||||
IsSpawnInGroup(groupID int32, name string) bool
|
||||
GetGroupLeader(groupID int32) entity.Entity
|
||||
MakeLeader(groupID int32, newLeader entity.Entity) bool
|
||||
GetGroupLeader(groupID int32) Entity
|
||||
MakeLeader(groupID int32, newLeader Entity) bool
|
||||
|
||||
// Messaging
|
||||
SimpleGroupMessage(groupID int32, message string)
|
||||
SendGroupMessage(groupID int32, msgType int8, message string)
|
||||
GroupMessage(groupID int32, message string)
|
||||
GroupChatMessage(groupID int32, from entity.Entity, language int32, message string, channel int16)
|
||||
GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16)
|
||||
GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16)
|
||||
SendGroupChatMessage(groupID int32, channel int16, message string)
|
||||
|
||||
@ -84,17 +83,17 @@ type GroupManagerInterface interface {
|
||||
// GroupEventHandler interface for handling group events
|
||||
type GroupEventHandler interface {
|
||||
// Group lifecycle events
|
||||
OnGroupCreated(group *Group, leader entity.Entity) error
|
||||
OnGroupCreated(group *Group, leader Entity) error
|
||||
OnGroupDisbanded(group *Group) error
|
||||
OnGroupMemberJoined(group *Group, member entity.Entity) error
|
||||
OnGroupMemberLeft(group *Group, member entity.Entity) error
|
||||
OnGroupLeaderChanged(group *Group, oldLeader, newLeader entity.Entity) error
|
||||
OnGroupMemberJoined(group *Group, member Entity) error
|
||||
OnGroupMemberLeft(group *Group, member Entity) error
|
||||
OnGroupLeaderChanged(group *Group, oldLeader, newLeader Entity) error
|
||||
|
||||
// Invitation events
|
||||
OnGroupInviteSent(leader, member entity.Entity) error
|
||||
OnGroupInviteAccepted(leader, member entity.Entity, groupID int32) error
|
||||
OnGroupInviteDeclined(leader, member entity.Entity) error
|
||||
OnGroupInviteExpired(leader, member entity.Entity) error
|
||||
OnGroupInviteSent(leader, member Entity) error
|
||||
OnGroupInviteAccepted(leader, member Entity, groupID int32) error
|
||||
OnGroupInviteDeclined(leader, member Entity) error
|
||||
OnGroupInviteExpired(leader, member Entity) error
|
||||
|
||||
// Raid events
|
||||
OnRaidFormed(groups []*Group) error
|
||||
@ -104,7 +103,7 @@ type GroupEventHandler interface {
|
||||
OnRaidInviteDeclined(leaderGroup *Group, targetGroup *Group) error
|
||||
|
||||
// Group activity events
|
||||
OnGroupMessage(group *Group, from entity.Entity, message string, channel int16) error
|
||||
OnGroupMessage(group *Group, from Entity, message string, channel int16) error
|
||||
OnGroupOptionsChanged(group *Group, oldOptions, newOptions *GroupOptions) error
|
||||
OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error
|
||||
}
|
||||
@ -146,8 +145,8 @@ type GroupPacketHandler interface {
|
||||
SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient any) error
|
||||
|
||||
// Group invitation packets
|
||||
SendGroupInvite(inviter, invitee entity.Entity) error
|
||||
SendGroupInviteResponse(inviter, invitee entity.Entity, accepted bool) error
|
||||
SendGroupInvite(inviter, invitee Entity) error
|
||||
SendGroupInviteResponse(inviter, invitee Entity, accepted bool) error
|
||||
|
||||
// Group messaging packets
|
||||
SendGroupMessage(members []*GroupMemberInfo, message *GroupMessage) error
|
||||
@ -170,18 +169,18 @@ type GroupPacketHandler interface {
|
||||
// GroupValidator interface for validating group operations
|
||||
type GroupValidator interface {
|
||||
// Group creation validation
|
||||
ValidateGroupCreation(leader entity.Entity, options *GroupOptions) error
|
||||
ValidateGroupJoin(group *Group, member entity.Entity) error
|
||||
ValidateGroupLeave(group *Group, member entity.Entity) error
|
||||
ValidateGroupCreation(leader Entity, options *GroupOptions) error
|
||||
ValidateGroupJoin(group *Group, member Entity) error
|
||||
ValidateGroupLeave(group *Group, member Entity) error
|
||||
|
||||
// Invitation validation
|
||||
ValidateGroupInvite(leader, member entity.Entity) error
|
||||
ValidateGroupInvite(leader, member Entity) error
|
||||
ValidateRaidInvite(leaderGroup, targetGroup *Group) error
|
||||
|
||||
// Group operation validation
|
||||
ValidateLeadershipChange(group *Group, oldLeader, newLeader entity.Entity) error
|
||||
ValidateLeadershipChange(group *Group, oldLeader, newLeader Entity) error
|
||||
ValidateGroupOptions(group *Group, options *GroupOptions) error
|
||||
ValidateGroupMessage(group *Group, from entity.Entity, message string) error
|
||||
ValidateGroupMessage(group *Group, from Entity, message string) error
|
||||
|
||||
// Raid validation
|
||||
ValidateRaidFormation(groups []*Group) error
|
||||
@ -191,18 +190,18 @@ type GroupValidator interface {
|
||||
// GroupNotifier interface for sending notifications
|
||||
type GroupNotifier interface {
|
||||
// Group notifications
|
||||
NotifyGroupCreated(group *Group, leader entity.Entity) error
|
||||
NotifyGroupCreated(group *Group, leader Entity) error
|
||||
NotifyGroupDisbanded(group *Group, reason string) error
|
||||
NotifyGroupMemberJoined(group *Group, member entity.Entity) error
|
||||
NotifyGroupMemberLeft(group *Group, member entity.Entity, reason string) error
|
||||
NotifyGroupLeaderChanged(group *Group, oldLeader, newLeader entity.Entity) error
|
||||
NotifyGroupMemberJoined(group *Group, member Entity) error
|
||||
NotifyGroupMemberLeft(group *Group, member Entity, reason string) error
|
||||
NotifyGroupLeaderChanged(group *Group, oldLeader, newLeader Entity) error
|
||||
|
||||
// Invitation notifications
|
||||
NotifyGroupInviteSent(leader, member entity.Entity) error
|
||||
NotifyGroupInviteReceived(leader, member entity.Entity) error
|
||||
NotifyGroupInviteAccepted(leader, member entity.Entity, groupID int32) error
|
||||
NotifyGroupInviteDeclined(leader, member entity.Entity) error
|
||||
NotifyGroupInviteExpired(leader, member entity.Entity) error
|
||||
NotifyGroupInviteSent(leader, member Entity) error
|
||||
NotifyGroupInviteReceived(leader, member Entity) error
|
||||
NotifyGroupInviteAccepted(leader, member Entity, groupID int32) error
|
||||
NotifyGroupInviteDeclined(leader, member Entity) error
|
||||
NotifyGroupInviteExpired(leader, member Entity) error
|
||||
|
||||
// Raid notifications
|
||||
NotifyRaidFormed(groups []*Group) error
|
||||
@ -220,23 +219,23 @@ type GroupNotifier interface {
|
||||
// GroupStatistics interface for tracking group statistics
|
||||
type GroupStatistics interface {
|
||||
// Group statistics
|
||||
RecordGroupCreated(group *Group, leader entity.Entity)
|
||||
RecordGroupCreated(group *Group, leader Entity)
|
||||
RecordGroupDisbanded(group *Group, duration int64)
|
||||
RecordGroupMemberJoined(group *Group, member entity.Entity)
|
||||
RecordGroupMemberLeft(group *Group, member entity.Entity, duration int64)
|
||||
RecordGroupMemberJoined(group *Group, member Entity)
|
||||
RecordGroupMemberLeft(group *Group, member Entity, duration int64)
|
||||
|
||||
// Invitation statistics
|
||||
RecordInviteSent(leader, member entity.Entity)
|
||||
RecordInviteAccepted(leader, member entity.Entity, responseTime int64)
|
||||
RecordInviteDeclined(leader, member entity.Entity, responseTime int64)
|
||||
RecordInviteExpired(leader, member entity.Entity)
|
||||
RecordInviteSent(leader, member Entity)
|
||||
RecordInviteAccepted(leader, member Entity, responseTime int64)
|
||||
RecordInviteDeclined(leader, member Entity, responseTime int64)
|
||||
RecordInviteExpired(leader, member Entity)
|
||||
|
||||
// Raid statistics
|
||||
RecordRaidFormed(groups []*Group)
|
||||
RecordRaidDisbanded(groups []*Group, duration int64)
|
||||
|
||||
// Activity statistics
|
||||
RecordGroupMessage(group *Group, from entity.Entity, messageType int8)
|
||||
RecordGroupMessage(group *Group, from Entity, messageType int8)
|
||||
RecordGroupActivity(group *Group, activityType string)
|
||||
|
||||
// Performance statistics
|
||||
@ -280,7 +279,7 @@ func (ga *GroupAdapter) GetMembers() []*GroupMemberInfo {
|
||||
}
|
||||
|
||||
// GetLeader returns the group leader
|
||||
func (ga *GroupAdapter) GetLeader() entity.Entity {
|
||||
func (ga *GroupAdapter) GetLeader() Entity {
|
||||
members := ga.group.GetMembers()
|
||||
for _, member := range members {
|
||||
if member.Leader {
|
||||
@ -306,7 +305,7 @@ func (ga *GroupAdapter) GetRaidGroups() []int32 {
|
||||
}
|
||||
|
||||
// IsMember checks if an entity is a member of the group
|
||||
func (ga *GroupAdapter) IsMember(entity entity.Entity) bool {
|
||||
func (ga *GroupAdapter) IsMember(entity Entity) bool {
|
||||
if entity == nil {
|
||||
return false
|
||||
}
|
||||
@ -343,7 +342,7 @@ func (ga *GroupAdapter) GetMemberByName(name string) *GroupMemberInfo {
|
||||
}
|
||||
|
||||
// GetMemberByEntity returns a member by entity
|
||||
func (ga *GroupAdapter) GetMemberByEntity(entity entity.Entity) *GroupMemberInfo {
|
||||
func (ga *GroupAdapter) GetMemberByEntity(entity Entity) *GroupMemberInfo {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
@ -358,7 +357,7 @@ func (ga *GroupAdapter) GetMemberByEntity(entity entity.Entity) *GroupMemberInfo
|
||||
}
|
||||
|
||||
// IsLeader checks if an entity is the group leader
|
||||
func (ga *GroupAdapter) IsLeader(entity entity.Entity) bool {
|
||||
func (ga *GroupAdapter) IsLeader(entity Entity) bool {
|
||||
if entity == nil {
|
||||
return false
|
||||
}
|
||||
@ -394,16 +393,16 @@ func (ga *GroupAdapter) GetLastActivity() time.Time {
|
||||
|
||||
// EntityGroupAdapter adapts entity functionality for group systems
|
||||
type EntityGroupAdapter struct {
|
||||
entity entity.Entity
|
||||
entity Entity
|
||||
}
|
||||
|
||||
// NewEntityGroupAdapter creates a new entity group adapter
|
||||
func NewEntityGroupAdapter(entity entity.Entity) *EntityGroupAdapter {
|
||||
func NewEntityGroupAdapter(entity Entity) *EntityGroupAdapter {
|
||||
return &EntityGroupAdapter{entity: entity}
|
||||
}
|
||||
|
||||
// GetEntity returns the wrapped entity
|
||||
func (ega *EntityGroupAdapter) GetEntity() entity.Entity {
|
||||
func (ega *EntityGroupAdapter) GetEntity() Entity {
|
||||
return ega.entity
|
||||
}
|
||||
|
||||
@ -497,6 +496,6 @@ func (ega *EntityGroupAdapter) IsDead() bool {
|
||||
}
|
||||
|
||||
// GetDistance returns distance to another entity
|
||||
func (ega *EntityGroupAdapter) GetDistance(other entity.Entity) float32 {
|
||||
return ega.entity.GetDistance(&other.Spawn)
|
||||
func (ega *EntityGroupAdapter) GetDistance(other Entity) float32 {
|
||||
return ega.entity.GetDistance(other)
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ package groups
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// NewGroupManager creates a new group manager with the given configuration
|
||||
@ -54,7 +52,7 @@ func (gm *GroupManager) Stop() error {
|
||||
}
|
||||
|
||||
// NewGroup creates a new group with the given leader and options
|
||||
func (gm *GroupManager) NewGroup(leader entity.Entity, options *GroupOptions, overrideGroupID int32) (int32, error) {
|
||||
func (gm *GroupManager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) {
|
||||
if leader == nil {
|
||||
return 0, fmt.Errorf("leader cannot be nil")
|
||||
}
|
||||
@ -138,7 +136,7 @@ func (gm *GroupManager) IsGroupIDValid(groupID int32) bool {
|
||||
}
|
||||
|
||||
// AddGroupMember adds a member to an existing group
|
||||
func (gm *GroupManager) AddGroupMember(groupID int32, member entity.Entity, isLeader bool) error {
|
||||
func (gm *GroupManager) AddGroupMember(groupID int32, member Entity, isLeader bool) error {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return fmt.Errorf("group %d not found", groupID)
|
||||
@ -164,7 +162,7 @@ func (gm *GroupManager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberI
|
||||
}
|
||||
|
||||
// RemoveGroupMember removes a member from a group
|
||||
func (gm *GroupManager) RemoveGroupMember(groupID int32, member entity.Entity) error {
|
||||
func (gm *GroupManager) RemoveGroupMember(groupID int32, member Entity) error {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return fmt.Errorf("group %d not found", groupID)
|
||||
@ -214,7 +212,7 @@ func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceR
|
||||
// Group invitation handling
|
||||
|
||||
// Invite handles inviting a player to a group
|
||||
func (gm *GroupManager) Invite(leader entity.Entity, member entity.Entity) int8 {
|
||||
func (gm *GroupManager) Invite(leader Entity, member Entity) int8 {
|
||||
if leader == nil || member == nil {
|
||||
return GROUP_INVITE_TARGET_NOT_FOUND
|
||||
}
|
||||
@ -226,7 +224,7 @@ func (gm *GroupManager) Invite(leader entity.Entity, member entity.Entity) int8
|
||||
|
||||
// Check if member already has an invite
|
||||
inviteKey := member.GetName()
|
||||
if gm.hasPendingInvite(inviteKey) {
|
||||
if gm.hasPendingInvite(inviteKey) != "" {
|
||||
return GROUP_INVITE_ALREADY_HAS_INVITE
|
||||
}
|
||||
|
||||
@ -248,12 +246,12 @@ func (gm *GroupManager) Invite(leader entity.Entity, member entity.Entity) int8
|
||||
}
|
||||
|
||||
// AddInvite adds a group invitation
|
||||
func (gm *GroupManager) AddInvite(leader entity.Entity, member entity.Entity) bool {
|
||||
func (gm *GroupManager) AddInvite(leader Entity, member Entity) bool {
|
||||
return gm.addInvite(leader, member)
|
||||
}
|
||||
|
||||
// addInvite internal method to add an invitation
|
||||
func (gm *GroupManager) addInvite(leader entity.Entity, member entity.Entity) bool {
|
||||
func (gm *GroupManager) addInvite(leader Entity, member Entity) bool {
|
||||
if leader == nil || member == nil {
|
||||
return false
|
||||
}
|
||||
@ -281,7 +279,7 @@ func (gm *GroupManager) addInvite(leader entity.Entity, member entity.Entity) bo
|
||||
}
|
||||
|
||||
// AcceptInvite handles accepting of a group invite
|
||||
func (gm *GroupManager) AcceptInvite(member entity.Entity, groupOverrideID *int32, autoAddGroup bool) int8 {
|
||||
func (gm *GroupManager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 {
|
||||
if member == nil {
|
||||
return GROUP_INVITE_TARGET_NOT_FOUND
|
||||
}
|
||||
@ -312,7 +310,7 @@ func (gm *GroupManager) AcceptInvite(member entity.Entity, groupOverrideID *int3
|
||||
}
|
||||
|
||||
// Find the leader
|
||||
var leader entity.Entity
|
||||
var leader Entity
|
||||
// TODO: Find leader entity by name
|
||||
// leader = world.GetPlayerByName(invite.InviterName)
|
||||
|
||||
@ -361,7 +359,7 @@ func (gm *GroupManager) AcceptInvite(member entity.Entity, groupOverrideID *int3
|
||||
}
|
||||
|
||||
// DeclineInvite handles declining of a group invite
|
||||
func (gm *GroupManager) DeclineInvite(member entity.Entity) {
|
||||
func (gm *GroupManager) DeclineInvite(member Entity) {
|
||||
if member == nil {
|
||||
return
|
||||
}
|
||||
@ -369,7 +367,7 @@ func (gm *GroupManager) DeclineInvite(member entity.Entity) {
|
||||
inviteKey := member.GetName()
|
||||
|
||||
gm.invitesMutex.Lock()
|
||||
invite, exists := gm.pendingInvites[inviteKey]
|
||||
_, exists := gm.pendingInvites[inviteKey]
|
||||
if exists {
|
||||
delete(gm.pendingInvites, inviteKey)
|
||||
}
|
||||
@ -380,7 +378,7 @@ func (gm *GroupManager) DeclineInvite(member entity.Entity) {
|
||||
gm.updateStatsForDeclinedInvite()
|
||||
|
||||
// Fire event
|
||||
var leader entity.Entity
|
||||
var leader Entity
|
||||
// TODO: Find leader entity by name
|
||||
// leader = world.GetPlayerByName(invite.InviterName)
|
||||
gm.fireGroupInviteDeclinedEvent(leader, member)
|
||||
@ -388,7 +386,7 @@ func (gm *GroupManager) DeclineInvite(member entity.Entity) {
|
||||
}
|
||||
|
||||
// ClearPendingInvite clears a pending invite for a member
|
||||
func (gm *GroupManager) ClearPendingInvite(member entity.Entity) {
|
||||
func (gm *GroupManager) ClearPendingInvite(member Entity) {
|
||||
if member == nil {
|
||||
return
|
||||
}
|
||||
@ -401,7 +399,7 @@ func (gm *GroupManager) ClearPendingInvite(member entity.Entity) {
|
||||
}
|
||||
|
||||
// HasPendingInvite checks if a member has a pending invite and returns the inviter name
|
||||
func (gm *GroupManager) HasPendingInvite(member entity.Entity) string {
|
||||
func (gm *GroupManager) HasPendingInvite(member Entity) string {
|
||||
if member == nil {
|
||||
return ""
|
||||
}
|
||||
@ -436,7 +434,7 @@ func (gm *GroupManager) GetGroupSize(groupID int32) int32 {
|
||||
}
|
||||
|
||||
// IsInGroup checks if an entity is in a specific group
|
||||
func (gm *GroupManager) IsInGroup(groupID int32, member entity.Entity) bool {
|
||||
func (gm *GroupManager) IsInGroup(groupID int32, member Entity) bool {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil || member == nil {
|
||||
return false
|
||||
@ -453,7 +451,7 @@ func (gm *GroupManager) IsInGroup(groupID int32, member entity.Entity) bool {
|
||||
}
|
||||
|
||||
// IsPlayerInGroup checks if a player with the given character ID is in a group
|
||||
func (gm *GroupManager) IsPlayerInGroup(groupID int32, charID int32) entity.Entity {
|
||||
func (gm *GroupManager) IsPlayerInGroup(groupID int32, charID int32) Entity {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return nil
|
||||
@ -490,7 +488,7 @@ func (gm *GroupManager) IsSpawnInGroup(groupID int32, name string) bool {
|
||||
}
|
||||
|
||||
// GetGroupLeader returns the leader of a group
|
||||
func (gm *GroupManager) GetGroupLeader(groupID int32) entity.Entity {
|
||||
func (gm *GroupManager) GetGroupLeader(groupID int32) Entity {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return nil
|
||||
@ -507,7 +505,7 @@ func (gm *GroupManager) GetGroupLeader(groupID int32) entity.Entity {
|
||||
}
|
||||
|
||||
// MakeLeader changes the leader of a group
|
||||
func (gm *GroupManager) MakeLeader(groupID int32, newLeader entity.Entity) bool {
|
||||
func (gm *GroupManager) MakeLeader(groupID int32, newLeader Entity) bool {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return false
|
||||
@ -541,7 +539,7 @@ func (gm *GroupManager) GroupMessage(groupID int32, message string) {
|
||||
}
|
||||
|
||||
// GroupChatMessage sends a chat message from a member to the group
|
||||
func (gm *GroupManager) GroupChatMessage(groupID int32, from entity.Entity, language int32, message string, channel int16) {
|
||||
func (gm *GroupManager) GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16) {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group != nil {
|
||||
group.GroupChatMessage(from, language, message, channel)
|
||||
@ -934,7 +932,7 @@ func (gm *GroupManager) SetNotifier(notifier GroupNotifier) {
|
||||
// Event firing methods
|
||||
|
||||
// fireGroupCreatedEvent fires a group created event
|
||||
func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader entity.Entity) {
|
||||
func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader Entity) {
|
||||
gm.eventHandlersMutex.RLock()
|
||||
defer gm.eventHandlersMutex.RUnlock()
|
||||
|
||||
@ -954,7 +952,7 @@ func (gm *GroupManager) fireGroupDisbandedEvent(group *Group) {
|
||||
}
|
||||
|
||||
// fireGroupInviteSentEvent fires a group invite sent event
|
||||
func (gm *GroupManager) fireGroupInviteSentEvent(leader, member entity.Entity) {
|
||||
func (gm *GroupManager) fireGroupInviteSentEvent(leader, member Entity) {
|
||||
gm.eventHandlersMutex.RLock()
|
||||
defer gm.eventHandlersMutex.RUnlock()
|
||||
|
||||
@ -964,7 +962,7 @@ func (gm *GroupManager) fireGroupInviteSentEvent(leader, member entity.Entity) {
|
||||
}
|
||||
|
||||
// fireGroupInviteAcceptedEvent fires a group invite accepted event
|
||||
func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member entity.Entity, groupID int32) {
|
||||
func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member Entity, groupID int32) {
|
||||
gm.eventHandlersMutex.RLock()
|
||||
defer gm.eventHandlersMutex.RUnlock()
|
||||
|
||||
@ -974,7 +972,7 @@ func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member entity.Entit
|
||||
}
|
||||
|
||||
// fireGroupInviteDeclinedEvent fires a group invite declined event
|
||||
func (gm *GroupManager) fireGroupInviteDeclinedEvent(leader, member entity.Entity) {
|
||||
func (gm *GroupManager) fireGroupInviteDeclinedEvent(leader, member Entity) {
|
||||
gm.eventHandlersMutex.RLock()
|
||||
defer gm.eventHandlersMutex.RUnlock()
|
||||
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// Service provides a high-level interface for group management
|
||||
@ -128,7 +126,7 @@ func (s *Service) GetManager() GroupManagerInterface {
|
||||
// High-level group operations
|
||||
|
||||
// CreateGroup creates a new group with validation
|
||||
func (s *Service) CreateGroup(leader entity.Entity, options *GroupOptions) (int32, error) {
|
||||
func (s *Service) CreateGroup(leader Entity, options *GroupOptions) (int32, error) {
|
||||
if leader == nil {
|
||||
return 0, fmt.Errorf("leader cannot be nil")
|
||||
}
|
||||
@ -150,7 +148,7 @@ func (s *Service) CreateGroup(leader entity.Entity, options *GroupOptions) (int3
|
||||
}
|
||||
|
||||
// InviteToGroup invites a member to join a group
|
||||
func (s *Service) InviteToGroup(leader entity.Entity, member entity.Entity) error {
|
||||
func (s *Service) InviteToGroup(leader Entity, member Entity) error {
|
||||
if leader == nil || member == nil {
|
||||
return fmt.Errorf("leader and member cannot be nil")
|
||||
}
|
||||
@ -190,7 +188,7 @@ func (s *Service) InviteToGroup(leader entity.Entity, member entity.Entity) erro
|
||||
}
|
||||
|
||||
// AcceptGroupInvite accepts a group invitation
|
||||
func (s *Service) AcceptGroupInvite(member entity.Entity) error {
|
||||
func (s *Service) AcceptGroupInvite(member Entity) error {
|
||||
if member == nil {
|
||||
return fmt.Errorf("member cannot be nil")
|
||||
}
|
||||
@ -212,14 +210,14 @@ func (s *Service) AcceptGroupInvite(member entity.Entity) error {
|
||||
}
|
||||
|
||||
// DeclineGroupInvite declines a group invitation
|
||||
func (s *Service) DeclineGroupInvite(member entity.Entity) {
|
||||
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.Entity) error {
|
||||
func (s *Service) LeaveGroup(member Entity) error {
|
||||
if member == nil {
|
||||
return fmt.Errorf("member cannot be nil")
|
||||
}
|
||||
@ -241,7 +239,7 @@ func (s *Service) DisbandGroup(groupID int32) error {
|
||||
}
|
||||
|
||||
// TransferLeadership transfers group leadership
|
||||
func (s *Service) TransferLeadership(groupID int32, newLeader entity.Entity) error {
|
||||
func (s *Service) TransferLeadership(groupID int32, newLeader Entity) error {
|
||||
if newLeader == nil {
|
||||
return fmt.Errorf("new leader cannot be nil")
|
||||
}
|
||||
@ -287,7 +285,7 @@ func (s *Service) GetGroupInfo(groupID int32) (*GroupInfo, error) {
|
||||
}
|
||||
|
||||
// GetMemberGroups returns all groups that contain any of the specified members
|
||||
func (s *Service) GetMemberGroups(members []entity.Entity) []*GroupInfo {
|
||||
func (s *Service) GetMemberGroups(members []Entity) []*GroupInfo {
|
||||
var groups []*GroupInfo
|
||||
|
||||
allGroups := s.manager.GetAllGroups()
|
||||
@ -449,7 +447,7 @@ func (s *Service) GetServiceStats() *ServiceStats {
|
||||
// Validation methods
|
||||
|
||||
// validateGroupCreation validates group creation parameters
|
||||
func (s *Service) validateGroupCreation(leader entity.Entity, options *GroupOptions) error {
|
||||
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 {
|
||||
@ -465,10 +463,10 @@ func (s *Service) validateGroupCreation(leader entity.Entity, options *GroupOpti
|
||||
}
|
||||
|
||||
// validateGroupInvitation validates group invitation parameters
|
||||
func (s *Service) validateGroupInvitation(leader entity.Entity, member entity.Entity) error {
|
||||
func (s *Service) validateGroupInvitation(leader Entity, member Entity) error {
|
||||
// Check distance if enabled
|
||||
if s.config.MaxInviteDistance > 0 {
|
||||
distance := leader.GetDistance(&member.Spawn)
|
||||
distance := leader.GetDistance(member)
|
||||
if distance > s.config.MaxInviteDistance {
|
||||
return fmt.Errorf("member is too far away (%.1f > %.1f)", distance, s.config.MaxInviteDistance)
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ package groups
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// GroupOptions holds group configuration settings
|
||||
@ -56,7 +54,7 @@ type GroupMemberInfo struct {
|
||||
ClientPeerPort int16 `json:"client_peer_port"`
|
||||
|
||||
// Entity reference (local members only)
|
||||
Member entity.Entity `json:"-"`
|
||||
Member Entity `json:"-"`
|
||||
|
||||
// Client reference (players only) - interface to avoid circular deps
|
||||
Client any `json:"-"`
|
||||
|
Loading…
x
Reference in New Issue
Block a user