first pass on group tests

This commit is contained in:
Sky Johnson 2025-08-01 23:16:49 -05:00
parent 0534c49610
commit e35e41f643
7 changed files with 1002 additions and 108 deletions

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

View File

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

View 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++
}
})
})
}

View File

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

View File

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

View File

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

View File

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