From 575f94f3c1b5060700e022fa34327ebd0c5223f7 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 29 Aug 2025 14:02:21 -0500 Subject: [PATCH] simplify groups --- internal/groups/entities.go | 34 - internal/groups/group.go | 797 -------------- internal/groups/groups.go | 1633 ++++++++++++++++++++++++++++ internal/groups/interfaces.go | 501 --------- internal/groups/manager.go | 510 --------- internal/groups/manager_methods.go | 528 --------- internal/groups/master.go | 848 --------------- internal/groups/types.go | 257 ----- internal/packets/opcodes.go | 22 + 9 files changed, 1655 insertions(+), 3475 deletions(-) delete mode 100644 internal/groups/entities.go delete mode 100644 internal/groups/group.go create mode 100644 internal/groups/groups.go delete mode 100644 internal/groups/interfaces.go delete mode 100644 internal/groups/manager.go delete mode 100644 internal/groups/manager_methods.go delete mode 100644 internal/groups/master.go delete mode 100644 internal/groups/types.go diff --git a/internal/groups/entities.go b/internal/groups/entities.go deleted file mode 100644 index 3fbcb3a..0000000 --- a/internal/groups/entities.go +++ /dev/null @@ -1,34 +0,0 @@ -package groups - -// Entity represents a game entity that can be part of a group -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 types - IsPlayer() bool - IsBot() bool - IsNPC() bool - IsDead() bool - - // World positioning - GetZone() Zone - GetDistance(other Entity) float32 -} - -// Zone represents a game zone -type Zone interface { - GetZoneID() int32 - GetInstanceID() int32 - GetZoneName() string -} diff --git a/internal/groups/group.go b/internal/groups/group.go deleted file mode 100644 index 2534229..0000000 --- a/internal/groups/group.go +++ /dev/null @@ -1,797 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "time" -) - -// Group represents a player group with embedded database operations -type Group struct { - // Core fields - GroupID int32 `json:"group_id" db:"group_id"` - Options GroupOptions `json:"options"` - Members []*GroupMemberInfo `json:"members"` - RaidGroups []int32 `json:"raid_groups"` - CreatedTime time.Time `json:"created_time" db:"created_time"` - LastActivity time.Time `json:"last_activity" db:"last_activity"` - Disbanded bool `json:"disbanded" db:"disbanded"` - - // Internal fields - membersMutex sync.RWMutex `json:"-"` - raidGroupsMutex sync.RWMutex `json:"-"` - optionsMutex sync.RWMutex `json:"-"` - activityMutex sync.RWMutex `json:"-"` - disbandMutex sync.RWMutex `json:"-"` - - // Communication channels - messageQueue chan *GroupMessage `json:"-"` - updateQueue chan *GroupUpdate `json:"-"` - - // Background processing - stopChan chan struct{} `json:"-"` - wg sync.WaitGroup `json:"-"` - - // Database integration - embedded operations - db any `json:"-"` // Database connection - isNew bool `json:"-"` // Flag for new groups -} - -// New creates a new group -func New(db any) *Group { - group := &Group{ - GroupID: 0, // Will be set when saved - Options: DefaultGroupOptions(), - Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), - RaidGroups: make([]int32, 0), - CreatedTime: time.Now(), - LastActivity: time.Now(), - Disbanded: false, - messageQueue: make(chan *GroupMessage, 100), - updateQueue: make(chan *GroupUpdate, 100), - stopChan: make(chan struct{}), - db: db, - isNew: true, - } - - // Start background processing - group.wg.Add(1) - go group.processMessages() - - return group -} - -// NewGroup creates a new group with specified ID and options -func NewGroup(id int32, options *GroupOptions, db any) *Group { - if options == nil { - defaultOpts := DefaultGroupOptions() - options = &defaultOpts - } - - group := &Group{ - GroupID: id, - Options: *options, - Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), - RaidGroups: make([]int32, 0), - CreatedTime: time.Now(), - LastActivity: time.Now(), - Disbanded: false, - messageQueue: make(chan *GroupMessage, 100), - updateQueue: make(chan *GroupUpdate, 100), - stopChan: make(chan struct{}), - db: db, - isNew: false, - } - - // Start background processing - group.wg.Add(1) - go group.processMessages() - - return group -} - -// GetID returns the group ID (implements Identifiable interface) -func (g *Group) GetID() int32 { - return g.GroupID -} - -// Save saves the group to the database -func (g *Group) Save() error { - // TODO: Implement database save logic - // This would require integration with the actual database system - return nil -} - -// Delete removes the group from the database -func (g *Group) Delete() error { - // Disband the group first - g.Disband() - - // TODO: Implement database delete logic - // This would require integration with the actual database system - return nil -} - -// Reload refreshes the group from the database -func (g *Group) Reload() error { - // TODO: Implement database reload logic - // This would require integration with the actual database system - return nil -} - -// GetSize returns the number of members in the group -func (g *Group) GetSize() int32 { - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - return int32(len(g.Members)) -} - -// GetMembers returns a copy of the member list -func (g *Group) GetMembers() []*GroupMemberInfo { - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - members := make([]*GroupMemberInfo, len(g.Members)) - for i, member := range g.Members { - members[i] = member.Copy() - } - - return members -} - -// AddMember adds a new member to the group -func (g *Group) AddMember(member Entity, isLeader bool) error { - if member == nil { - return fmt.Errorf("member cannot be nil") - } - - g.disbandMutex.RLock() - if g.Disbanded { - g.disbandMutex.RUnlock() - return fmt.Errorf("group has been disbanded") - } - g.disbandMutex.RUnlock() - - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - // Check if group is full - if len(g.Members) >= MAX_GROUP_SIZE { - return fmt.Errorf("group is full") - } - - // Check if member is already in the group - for _, gmi := range g.Members { - if gmi.Member == member { - return fmt.Errorf("member is already in the group") - } - } - - // Create new group member info - gmi := &GroupMemberInfo{ - GroupID: g.GroupID, - Name: member.GetName(), - Leader: isLeader, - Member: member, - IsClient: member.IsPlayer(), - JoinTime: time.Now(), - LastUpdate: time.Now(), - } - - // Update member stats from entity - gmi.UpdateStats() - - // Set client reference if it's a player - if member.IsPlayer() { - // TODO: Get client reference from player - // gmi.Client = member.GetClient() - } - - // Update zone information - if zone := member.GetZone(); zone != nil { - gmi.ZoneID = zone.GetZoneID() - gmi.InstanceID = zone.GetInstanceID() - gmi.Zone = zone.GetZoneName() - } - - // Add to members list - g.Members = append(g.Members, gmi) - g.updateLastActivity() - - // Set group reference on the entity - // TODO: Set group member info on entity - // member.SetGroupMemberInfo(gmi) - - // Send group update - g.sendGroupUpdate(nil, false) - - return nil -} - -// AddMemberFromPeer adds a member from a peer server -func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID int8, - hpCur, hpMax int32, levelCur, levelMax int16, powerCur, powerMax int32, - raceID int8, zoneName string, mentorTargetCharID int32, - zoneID, instanceID int32, peerAddress string, peerPort int16, isRaidLooter bool) error { - - g.disbandMutex.RLock() - if g.Disbanded { - g.disbandMutex.RUnlock() - return fmt.Errorf("group has been disbanded") - } - g.disbandMutex.RUnlock() - - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - // Check if group is full - if len(g.Members) >= MAX_GROUP_SIZE { - return fmt.Errorf("group is full") - } - - // Create new group member info for peer member - gmi := &GroupMemberInfo{ - GroupID: g.GroupID, - Name: name, - Zone: zoneName, - HPCurrent: hpCur, - HPMax: hpMax, - PowerCurrent: powerCur, - PowerMax: powerMax, - LevelCurrent: levelCur, - LevelMax: levelMax, - RaceID: raceID, - ClassID: classID, - Leader: isLeader, - IsClient: isClient, - ZoneID: zoneID, - InstanceID: instanceID, - MentorTargetCharID: mentorTargetCharID, - ClientPeerAddress: peerAddress, - ClientPeerPort: peerPort, - IsRaidLooter: isRaidLooter, - Member: nil, // No local entity reference for peer members - Client: nil, // No local client reference for peer members - JoinTime: time.Now(), - LastUpdate: time.Now(), - } - - // Add to members list - g.Members = append(g.Members, gmi) - g.updateLastActivity() - - // Send group update - g.sendGroupUpdate(nil, false) - - return nil -} - -// RemoveMember removes a member from the group -func (g *Group) RemoveMember(member Entity) error { - if member == nil { - return fmt.Errorf("member cannot be nil") - } - - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - // Find and remove the member - for i, gmi := range g.Members { - if gmi.Member == member { - // Clear group reference on entity - // TODO: Clear group member info on entity - // member.SetGroupMemberInfo(nil) - - // Remove from slice - g.Members = append(g.Members[:i], g.Members[i+1:]...) - g.updateLastActivity() - - // If this was a bot, camp it - // TODO: Handle bot camping - // if member.IsBot() { - // member.Camp() - // } - - // Send group update - g.sendGroupUpdate(nil, false) - - return nil - } - } - - return fmt.Errorf("member not found in group") -} - -// RemoveMemberByName removes a member by name (for peer members) -func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) error { - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - // Find and remove the member - for i, gmi := range g.Members { - if gmi.Name == name && gmi.IsClient == isClient { - // Handle mentorship cleanup - if isClient && charID > 0 { - for _, otherGmi := range g.Members { - if otherGmi.MentorTargetCharID == charID { - otherGmi.MentorTargetCharID = 0 - // TODO: Enable reset mentorship on client - // if otherGmi.Client != nil { - // otherGmi.Client.GetPlayer().EnableResetMentorship() - // } - } - } - } - - // Remove from slice - g.Members = append(g.Members[:i], g.Members[i+1:]...) - g.updateLastActivity() - - // Send group update - g.sendGroupUpdate(nil, false) - - return nil - } - } - - return fmt.Errorf("member not found in group") -} - -// Disband disbands the group and removes all members -func (g *Group) Disband() { - g.disbandMutex.Lock() - if g.Disbanded { - g.disbandMutex.Unlock() - return - } - g.Disbanded = true - g.disbandMutex.Unlock() - - // Stop background processing first to avoid deadlock - close(g.stopChan) - g.wg.Wait() - - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - // Clear raid groups - g.raidGroupsMutex.Lock() - g.RaidGroups = nil - g.raidGroupsMutex.Unlock() - - // Remove all members - for _, gmi := range g.Members { - if gmi.Member != nil { - // Clear group reference on entity - // TODO: Clear group member info on entity - // gmi.Member.SetGroupMemberInfo(nil) - - // Handle bot camping - // TODO: Handle bot camping - // if gmi.Member.IsBot() { - // gmi.Member.Camp() - // } - } - - // Handle mentorship cleanup - if gmi.MentorTargetCharID > 0 { - // TODO: Enable reset mentorship on client - // if gmi.Client != nil { - // gmi.Client.GetPlayer().EnableResetMentorship() - // } - } - - // TODO: Set character/raid sheet changed flags - // if gmi.Client != nil { - // gmi.Client.GetPlayer().SetCharSheetChanged(true) - // if isInRaid { - // gmi.Client.GetPlayer().SetRaidSheetChanged(true) - // } - // } - } - - // Clear members list - g.Members = nil -} - -// SendGroupUpdate sends an update to all group members -func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) { - g.sendGroupUpdate(excludeClient, forceRaidUpdate) -} - -// sendGroupUpdate internal method to send group updates -func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) { - update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.GroupID) - update.ExcludeClient = excludeClient - update.ForceRaidUpdate = forceRaidUpdate - - select { - case g.updateQueue <- update: - default: - // Queue is full, drop the update - } -} - -// SimpleGroupMessage sends a simple message to all group members -func (g *Group) SimpleGroupMessage(message string) { - msg := NewGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, CHANNEL_GROUP_CHAT, message, "", 0) - - select { - case g.messageQueue <- msg: - default: - // Queue is full, drop the message - } -} - -// SendGroupMessage sends a formatted message to all group members -func (g *Group) SendGroupMessage(msgType int8, message string) { - msg := NewGroupMessage(msgType, CHANNEL_GROUP_CHAT, message, "", 0) - - select { - case g.messageQueue <- msg: - default: - // Queue is full, drop the message - } -} - -// GroupChatMessage sends a chat message from a member to the group -func (g *Group) GroupChatMessage(from Entity, language int32, message string, channel int16) { - if from == nil { - return - } - - msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, from.GetName(), language) - - select { - case g.messageQueue <- msg: - default: - // Queue is full, drop the message - } -} - -// GroupChatMessageFromName sends a chat message from a named sender to the group -func (g *Group) GroupChatMessageFromName(fromName string, language int32, message string, channel int16) { - msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, fromName, language) - - select { - case g.messageQueue <- msg: - default: - // Queue is full, drop the message - } -} - -// MakeLeader changes the group leader -func (g *Group) MakeLeader(newLeader Entity) error { - if newLeader == nil { - return fmt.Errorf("new leader cannot be nil") - } - - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - var newLeaderGMI *GroupMemberInfo - - // Find the new leader and update leadership - for _, gmi := range g.Members { - if gmi.Member == newLeader { - newLeaderGMI = gmi - gmi.Leader = true - } else if gmi.Leader { - // Remove leadership from current leader - gmi.Leader = false - } - } - - if newLeaderGMI == nil { - return fmt.Errorf("new leader not found in group") - } - - g.updateLastActivity() - - // Send group update - g.sendGroupUpdate(nil, false) - - return nil -} - -// GetLeaderName returns the name of the group leader -func (g *Group) GetLeaderName() string { - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - for _, gmi := range g.Members { - if gmi.Leader { - return gmi.Name - } - } - - return "" -} - -// ShareQuestWithGroup shares a quest with all group members -func (g *Group) ShareQuestWithGroup(questSharer any, quest any) bool { - // TODO: Implement quest sharing - // This would require integration with the quest system - return false -} - -// UpdateGroupMemberInfo updates information for a specific member -func (g *Group) UpdateGroupMemberInfo(member Entity, groupMembersLocked bool) { - if member == nil { - return - } - - if !groupMembersLocked { - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - } - - // Find the member and update their info - for _, gmi := range g.Members { - if gmi.Member == member { - gmi.UpdateStats() - g.updateLastActivity() - break - } - } -} - -// GetGroupMemberByPosition returns a group member at a specific position -func (g *Group) GetGroupMemberByPosition(seeker Entity, mappedPosition int32) Entity { - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - if mappedPosition < 0 || int(mappedPosition) >= len(g.Members) { - return nil - } - - return g.Members[mappedPosition].Member -} - -// RemoveClientReference removes client references when a client disconnects -// This is used for cleanup when a player disconnects but stays in the group -func (g *Group) RemoveClientReference(client any) { - g.membersMutex.Lock() - defer g.membersMutex.Unlock() - - for _, gmi := range g.Members { - if gmi.Client != nil && gmi.Client == client { - gmi.Client = nil - // Don't set Member to nil as the entity might still exist - // Only clear the client reference - break - } - } -} - -// GetGroupOptions returns a copy of the group options -func (g *Group) GetGroupOptions() GroupOptions { - g.optionsMutex.RLock() - defer g.optionsMutex.RUnlock() - - return g.Options.Copy() -} - -// SetGroupOptions sets new group options -func (g *Group) SetGroupOptions(options *GroupOptions) error { - if options == nil { - return fmt.Errorf("options cannot be nil") - } - - if !options.IsValid() { - return fmt.Errorf("invalid group options") - } - - g.optionsMutex.Lock() - g.Options = *options - g.optionsMutex.Unlock() - - g.updateLastActivity() - - // Send group update for options change - update := NewGroupUpdate(GROUP_UPDATE_FLAG_OPTIONS, g.GroupID) - update.Options = options - - select { - case g.updateQueue <- update: - default: - // Queue is full, drop the update - } - - return nil -} - -// GetLastLooterIndex returns the last looter index -func (g *Group) GetLastLooterIndex() int8 { - g.optionsMutex.RLock() - defer g.optionsMutex.RUnlock() - - return g.Options.LastLootedIndex -} - -// SetNextLooterIndex sets the next looter index -func (g *Group) SetNextLooterIndex(newIndex int8) { - g.optionsMutex.Lock() - g.Options.LastLootedIndex = newIndex - g.optionsMutex.Unlock() - - g.updateLastActivity() -} - -// Raid functionality - -// GetRaidGroups returns a copy of the raid groups list -func (g *Group) GetRaidGroups() []int32 { - g.raidGroupsMutex.RLock() - defer g.raidGroupsMutex.RUnlock() - - if g.RaidGroups == nil { - return []int32{} - } - - groups := make([]int32, len(g.RaidGroups)) - copy(groups, g.RaidGroups) - return groups -} - -// ReplaceRaidGroups replaces the entire raid groups list -func (g *Group) ReplaceRaidGroups(groups []int32) { - g.raidGroupsMutex.Lock() - defer g.raidGroupsMutex.Unlock() - - if groups == nil { - g.RaidGroups = make([]int32, 0) - } else { - g.RaidGroups = make([]int32, len(groups)) - copy(g.RaidGroups, groups) - } - - g.updateLastActivity() -} - -// IsInRaidGroup checks if this group is in a raid with the specified group -func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool { - g.raidGroupsMutex.RLock() - defer g.raidGroupsMutex.RUnlock() - - for _, id := range g.RaidGroups { - if id == groupID { - return true - } - } - - return false -} - -// AddGroupToRaid adds a group to the raid -func (g *Group) AddGroupToRaid(groupID int32) { - g.raidGroupsMutex.Lock() - defer g.raidGroupsMutex.Unlock() - - // Check if already in raid - for _, id := range g.RaidGroups { - if id == groupID { - return - } - } - - g.RaidGroups = append(g.RaidGroups, groupID) - g.updateLastActivity() -} - -// RemoveGroupFromRaid removes a group from the raid -func (g *Group) RemoveGroupFromRaid(groupID int32) { - g.raidGroupsMutex.Lock() - defer g.raidGroupsMutex.Unlock() - - for i, id := range g.RaidGroups { - if id == groupID { - g.RaidGroups = append(g.RaidGroups[:i], g.RaidGroups[i+1:]...) - g.updateLastActivity() - break - } - } -} - -// IsGroupRaid checks if this group is part of a raid -func (g *Group) IsGroupRaid() bool { - g.raidGroupsMutex.RLock() - defer g.raidGroupsMutex.RUnlock() - - return len(g.RaidGroups) > 0 -} - -// ClearGroupRaid clears all raid associations -func (g *Group) ClearGroupRaid() { - g.raidGroupsMutex.Lock() - defer g.raidGroupsMutex.Unlock() - - g.RaidGroups = make([]int32, 0) - g.updateLastActivity() -} - -// IsDisbanded checks if the group has been disbanded -func (g *Group) IsDisbanded() bool { - g.disbandMutex.RLock() - defer g.disbandMutex.RUnlock() - - return g.Disbanded -} - -// GetCreatedTime returns when the group was created -func (g *Group) GetCreatedTime() time.Time { - return g.CreatedTime -} - -// GetLastActivity returns the last activity time -func (g *Group) GetLastActivity() time.Time { - g.activityMutex.RLock() - defer g.activityMutex.RUnlock() - return g.LastActivity -} - -// updateLastActivity updates the last activity timestamp -func (g *Group) updateLastActivity() { - g.activityMutex.Lock() - defer g.activityMutex.Unlock() - g.LastActivity = time.Now() -} - -// processMessages processes messages and updates in the background -func (g *Group) processMessages() { - defer g.wg.Done() - - for { - select { - case msg := <-g.messageQueue: - g.handleMessage(msg) - case update := <-g.updateQueue: - g.handleUpdate(update) - case <-g.stopChan: - return - } - } -} - -// handleMessage handles a group message -func (g *Group) handleMessage(msg *GroupMessage) { - if msg == nil { - return - } - - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - // Send message to all group members except the excluded client - for _, gmi := range g.Members { - if gmi.Client != nil && gmi.Client != msg.ExcludeClient { - // TODO: Send message to client - // This would require integration with the client system - } - } -} - -// handleUpdate handles a group update -func (g *Group) handleUpdate(update *GroupUpdate) { - if update == nil { - return - } - - g.membersMutex.RLock() - defer g.membersMutex.RUnlock() - - // Send update to all group members except the excluded client - for _, gmi := range g.Members { - if gmi.Client != nil && gmi.Client != update.ExcludeClient { - // TODO: Send update to client - // This would require integration with the client system - // if gmi.Client != nil { - // gmi.Client.GetPlayer().SetCharSheetChanged(true) - // if isInRaid || update.ForceRaidUpdate { - // gmi.Client.GetPlayer().SetRaidSheetChanged(true) - // } - // } - } - } -} diff --git a/internal/groups/groups.go b/internal/groups/groups.go new file mode 100644 index 0000000..ab4f78f --- /dev/null +++ b/internal/groups/groups.go @@ -0,0 +1,1633 @@ +package groups + +import ( + "fmt" + "sync" + "time" + + "eq2emu/internal/database" + "eq2emu/internal/packets" +) + +// Entity represents a game entity that can be part of groups (simplified interface) +type Entity interface { + GetID() int32 + GetName() string + GetLevel() int8 + GetClass() int8 + GetRace() int8 + GetHP() int32 + GetTotalHP() int32 + GetPower() int32 + GetTotalPower() int32 + IsPlayer() bool + IsBot() bool + IsNPC() bool + IsDead() bool + GetZone() Zone + GetDistance(other Entity) float32 +} + +// Zone represents a game zone (simplified interface) +type Zone interface { + GetZoneID() int32 + GetInstanceID() int32 + GetZoneName() string +} + +// Logger interface for logging operations +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Error(msg string, args ...any) +} + +// GroupOptions holds group configuration settings (preserved from C++) +type GroupOptions struct { + LootMethod int8 `json:"loot_method" db:"loot_method"` + LootItemsRarity int8 `json:"loot_items_rarity" db:"loot_items_rarity"` + AutoSplit int8 `json:"auto_split" db:"auto_split"` + DefaultYell int8 `json:"default_yell" db:"default_yell"` + GroupLockMethod int8 `json:"group_lock_method" db:"group_lock_method"` + GroupAutolock int8 `json:"group_autolock" db:"group_autolock"` + SoloAutolock int8 `json:"solo_autolock" db:"solo_autolock"` + AutoLootMethod int8 `json:"auto_loot_method" db:"auto_loot_method"` + LastLootedIndex int8 `json:"last_looted_index" db:"last_looted_index"` +} + +// GroupMemberInfo contains all information about a group member (preserved from C++) +type GroupMemberInfo struct { + GroupID int32 `json:"group_id" db:"group_id"` + Name string `json:"name" db:"name"` + Zone string `json:"zone" db:"zone"` + HPCurrent int32 `json:"hp_current" db:"hp_current"` + HPMax int32 `json:"hp_max" db:"hp_max"` + PowerCurrent int32 `json:"power_current" db:"power_current"` + PowerMax int32 `json:"power_max" db:"power_max"` + LevelCurrent int16 `json:"level_current" db:"level_current"` + LevelMax int16 `json:"level_max" db:"level_max"` + RaceID int8 `json:"race_id" db:"race_id"` + ClassID int8 `json:"class_id" db:"class_id"` + Leader bool `json:"leader" db:"leader"` + IsClient bool `json:"is_client" db:"is_client"` + IsRaidLooter bool `json:"is_raid_looter" db:"is_raid_looter"` + ZoneID int32 `json:"zone_id" db:"zone_id"` + InstanceID int32 `json:"instance_id" db:"instance_id"` + MentorTargetCharID int32 `json:"mentor_target_char_id" db:"mentor_target_char_id"` + ClientPeerAddress string `json:"client_peer_address" db:"client_peer_address"` + ClientPeerPort int16 `json:"client_peer_port" db:"client_peer_port"` + Member Entity `json:"-"` + Client any `json:"-"` + JoinTime time.Time `json:"join_time" db:"join_time"` + LastUpdate time.Time `json:"last_update" db:"last_update"` +} + +// GroupInvite represents a pending group invitation +type GroupInvite struct { + InviterName string `json:"inviter_name"` + InviteeName string `json:"invitee_name"` + GroupID int32 `json:"group_id"` + IsRaidInvite bool `json:"is_raid_invite"` + CreatedTime time.Time `json:"created_time"` + ExpiresTime time.Time `json:"expires_time"` +} + +// Group represents a player group (consolidated from multiple files) +type Group struct { + // Core identification + groupID int32 + + // Group configuration + options GroupOptions + + // Member management + members []*GroupMemberInfo + raidGroups []int32 + + // Timestamps + createdTime time.Time + lastActivity time.Time + + // State + disbanded bool + + // Thread safety + membersMutex sync.RWMutex + raidGroupsMutex sync.RWMutex + optionsMutex sync.RWMutex + activityMutex sync.RWMutex + disbandMutex sync.RWMutex +} + +// GroupManager manages all player groups (consolidated from multiple files) +type GroupManager struct { + // Group storage + groups map[int32]*Group + groupsMutex sync.RWMutex + + // Group ID generation + nextGroupID int32 + nextGroupIDMutex sync.Mutex + + // Pending invitations + pendingInvites map[string]*GroupInvite + raidPendingInvites map[string]*GroupInvite + invitesMutex sync.RWMutex + + // Statistics (simplified) + stats GroupManagerStats + statsMutex sync.RWMutex + + // Dependencies + database *database.Database + logger Logger +} + +// GroupManagerStats holds essential statistics +type GroupManagerStats struct { + TotalGroups int64 `json:"total_groups"` + ActiveGroups int64 `json:"active_groups"` + TotalRaids int64 `json:"total_raids"` + ActiveRaids int64 `json:"active_raids"` + TotalInvites int64 `json:"total_invites"` + AcceptedInvites int64 `json:"accepted_invites"` + DeclinedInvites int64 `json:"declined_invites"` + ExpiredInvites int64 `json:"expired_invites"` + LastStatsUpdate time.Time `json:"last_stats_update"` +} + +// NewGroupManager creates a new group manager +func NewGroupManager(db *database.Database, logger Logger) *GroupManager { + if logger == nil { + logger = &nullLogger{} + } + + return &GroupManager{ + groups: make(map[int32]*Group), + nextGroupID: 1, + pendingInvites: make(map[string]*GroupInvite), + raidPendingInvites: make(map[string]*GroupInvite), + database: db, + logger: logger, + stats: GroupManagerStats{LastStatsUpdate: time.Now()}, + } +} + +// Group creation and management (preserving C++ API) + +// NewGroup creates a new group with the provided Entity as the leader +func (gm *GroupManager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) { + if leader == nil { + return 0, fmt.Errorf("leader cannot be nil") + } + + // Use default options if none provided + if options == nil { + defaultOpts := DefaultGroupOptions() + options = &defaultOpts + } + + // Generate or use override group ID + var groupID int32 + if overrideGroupID > 0 { + groupID = overrideGroupID + // Update next ID if needed + gm.nextGroupIDMutex.Lock() + if overrideGroupID >= gm.nextGroupID { + gm.nextGroupID = overrideGroupID + 1 + } + gm.nextGroupIDMutex.Unlock() + } else { + gm.nextGroupIDMutex.Lock() + groupID = gm.nextGroupID + gm.nextGroupID++ + gm.nextGroupIDMutex.Unlock() + } + + // Create the group + group := &Group{ + groupID: groupID, + options: *options, + members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE), + raidGroups: make([]int32, 0), + createdTime: time.Now(), + lastActivity: time.Now(), + disbanded: false, + } + + // Add leader to the group + leaderInfo := &GroupMemberInfo{ + GroupID: groupID, + Name: leader.GetName(), + Leader: true, + Member: leader, + IsClient: leader.IsPlayer(), + JoinTime: time.Now(), + LastUpdate: time.Now(), + HPCurrent: leader.GetHP(), + HPMax: leader.GetTotalHP(), + PowerCurrent: leader.GetPower(), + PowerMax: leader.GetTotalPower(), + LevelCurrent: int16(leader.GetLevel()), + LevelMax: int16(leader.GetLevel()), + RaceID: leader.GetRace(), + ClassID: leader.GetClass(), + } + + // Update zone information + if zone := leader.GetZone(); zone != nil { + leaderInfo.ZoneID = zone.GetZoneID() + leaderInfo.InstanceID = zone.GetInstanceID() + leaderInfo.Zone = zone.GetZoneName() + } + + group.members = append(group.members, leaderInfo) + + // Add to groups map + gm.groupsMutex.Lock() + gm.groups[groupID] = group + gm.groupsMutex.Unlock() + + // Update statistics + gm.statsMutex.Lock() + gm.stats.TotalGroups++ + gm.stats.ActiveGroups++ + gm.stats.LastStatsUpdate = time.Now() + gm.statsMutex.Unlock() + + gm.logger.Info("Created new group", "group_id", groupID, "leader", leader.GetName()) + + return groupID, nil +} + +// RemoveGroup removes a group from the manager +func (gm *GroupManager) RemoveGroup(groupID int32) error { + gm.groupsMutex.Lock() + defer gm.groupsMutex.Unlock() + + group, exists := gm.groups[groupID] + if !exists { + return fmt.Errorf("group not found: %d", groupID) + } + + // Disband the group + group.Disband() + + // Remove from map + delete(gm.groups, groupID) + + // Update statistics + gm.statsMutex.Lock() + gm.stats.ActiveGroups-- + if len(group.raidGroups) > 0 { + gm.stats.ActiveRaids-- + } + gm.stats.LastStatsUpdate = time.Now() + gm.statsMutex.Unlock() + + gm.logger.Info("Removed group", "group_id", groupID) + + return nil +} + +// GetGroup retrieves a group by ID +func (gm *GroupManager) GetGroup(groupID int32) *Group { + gm.groupsMutex.RLock() + defer gm.groupsMutex.RUnlock() + + return gm.groups[groupID] +} + +// IsGroupIDValid checks if a group ID exists +func (gm *GroupManager) IsGroupIDValid(groupID int32) bool { + gm.groupsMutex.RLock() + defer gm.groupsMutex.RUnlock() + + _, exists := gm.groups[groupID] + return exists +} + +// Member management (preserving C++ API) + +// AddGroupMember adds a member to a group +func (gm *GroupManager) AddGroupMember(groupID int32, member Entity, isLeader bool) error { + group := gm.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group not found: %d", groupID) + } + + return group.AddMember(member, isLeader) +} + +// AddGroupMemberFromPeer adds a member from a peer server +func (gm *GroupManager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error { + group := gm.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group not found: %d", groupID) + } + + return group.AddMemberFromPeer(info) +} + +// RemoveGroupMember removes a member from a group +func (gm *GroupManager) RemoveGroupMember(groupID int32, member Entity) error { + group := gm.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group not found: %d", groupID) + } + + return group.RemoveMember(member) +} + +// RemoveGroupMemberByName removes a member by name +func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error { + group := gm.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group not found: %d", groupID) + } + + return group.RemoveMemberByName(name, isClient, charID) +} + +// Invitation system (preserving C++ API) + +// Invite handles player inviting another player to a group +func (gm *GroupManager) Invite(leader Entity, member Entity) int8 { + if leader == nil || member == nil { + return GROUP_INVITE_TARGET_NOT_FOUND + } + + if leader == member { + return GROUP_INVITE_SELF_INVITE + } + + // Check if member already has pending invite + if gm.HasPendingInvite(member) != "" { + return GROUP_INVITE_ALREADY_HAS_INVITE + } + + // Add the invitation + if !gm.AddInvite(leader, member) { + return GROUP_INVITE_TARGET_BUSY + } + + gm.statsMutex.Lock() + gm.stats.TotalInvites++ + gm.stats.LastStatsUpdate = time.Now() + gm.statsMutex.Unlock() + + gm.logger.Info("Group invitation sent", "leader", leader.GetName(), "member", member.GetName()) + + return GROUP_INVITE_SUCCESS +} + +// AddInvite adds a pending invitation +func (gm *GroupManager) AddInvite(leader Entity, member Entity) bool { + if leader == nil || member == nil { + return false + } + + invite := &GroupInvite{ + InviterName: leader.GetName(), + InviteeName: member.GetName(), + GroupID: 0, // Will be set when group is created + IsRaidInvite: false, + CreatedTime: time.Now(), + ExpiresTime: time.Now().Add(GROUP_INVITE_TIMEOUT * time.Millisecond), + } + + gm.invitesMutex.Lock() + gm.pendingInvites[member.GetName()] = invite + gm.invitesMutex.Unlock() + + return true +} + +// AcceptInvite handles accepting a group invitation +func (gm *GroupManager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 { + if member == nil { + return GROUP_INVITE_TARGET_NOT_FOUND + } + + gm.invitesMutex.Lock() + invite, exists := gm.pendingInvites[member.GetName()] + if !exists { + gm.invitesMutex.Unlock() + return GROUP_INVITE_TARGET_NOT_FOUND + } + + // Check if invite has expired + if invite.IsExpired() { + delete(gm.pendingInvites, member.GetName()) + gm.invitesMutex.Unlock() + gm.statsMutex.Lock() + gm.stats.ExpiredInvites++ + gm.statsMutex.Unlock() + return GROUP_INVITE_TARGET_NOT_FOUND + } + + // Remove the invite + delete(gm.pendingInvites, member.GetName()) + gm.invitesMutex.Unlock() + + var groupID int32 + if groupOverrideID != nil { + groupID = *groupOverrideID + } else { + // Find or create group for the leader + leaderGroupID := gm.findGroupByLeaderName(invite.InviterName) + if leaderGroupID == 0 && autoAddGroup { + // Create a new group - need to find the leader entity + // This is a simplified implementation - in reality, you'd need to look up the leader + gm.logger.Error("Cannot auto-create group - leader entity lookup not implemented") + return GROUP_INVITE_TARGET_NOT_FOUND + } + groupID = leaderGroupID + } + + if groupID != 0 { + err := gm.AddGroupMember(groupID, member, false) + if err != nil { + gm.logger.Error("Failed to add member to group", "error", err, "group_id", groupID) + return GROUP_INVITE_TARGET_BUSY + } + } + + gm.statsMutex.Lock() + gm.stats.AcceptedInvites++ + gm.stats.LastStatsUpdate = time.Now() + gm.statsMutex.Unlock() + + gm.logger.Info("Group invitation accepted", "member", member.GetName(), "group_id", groupID) + + return GROUP_INVITE_SUCCESS +} + +// DeclineInvite handles declining a group invitation +func (gm *GroupManager) DeclineInvite(member Entity) { + if member == nil { + return + } + + gm.invitesMutex.Lock() + _, exists := gm.pendingInvites[member.GetName()] + if exists { + delete(gm.pendingInvites, member.GetName()) + } + gm.invitesMutex.Unlock() + + if exists { + gm.statsMutex.Lock() + gm.stats.DeclinedInvites++ + gm.stats.LastStatsUpdate = time.Now() + gm.statsMutex.Unlock() + + gm.logger.Info("Group invitation declined", "member", member.GetName()) + } +} + +// ClearPendingInvite removes a pending invitation +func (gm *GroupManager) ClearPendingInvite(member Entity) { + if member == nil { + return + } + + gm.invitesMutex.Lock() + delete(gm.pendingInvites, member.GetName()) + gm.invitesMutex.Unlock() +} + +// HasPendingInvite checks if a member has a pending invitation +func (gm *GroupManager) HasPendingInvite(member Entity) string { + if member == nil { + return "" + } + + gm.invitesMutex.RLock() + invite, exists := gm.pendingInvites[member.GetName()] + gm.invitesMutex.RUnlock() + + if !exists || invite.IsExpired() { + if exists { + // Clean up expired invite + gm.invitesMutex.Lock() + delete(gm.pendingInvites, member.GetName()) + gm.invitesMutex.Unlock() + } + return "" + } + + return invite.InviterName +} + +// Group utilities (preserving C++ API) + +// GetGroupSize returns the number of members in a group +func (gm *GroupManager) GetGroupSize(groupID int32) int32 { + group := gm.GetGroup(groupID) + if group == nil { + return 0 + } + + return group.GetSize() +} + +// IsInGroup checks if a member is in a specific group +func (gm *GroupManager) IsInGroup(groupID int32, member Entity) bool { + if member == nil { + return false + } + + group := gm.GetGroup(groupID) + if group == nil { + return false + } + + return group.HasMember(member) +} + +// IsPlayerInGroup checks if a player character ID is in a group +func (gm *GroupManager) IsPlayerInGroup(groupID int32, charID int32) Entity { + group := gm.GetGroup(groupID) + if group == nil { + return nil + } + + return group.GetMemberByCharID(charID) +} + +// IsSpawnInGroup checks if a spawn name is in a group +func (gm *GroupManager) IsSpawnInGroup(groupID int32, name string) bool { + group := gm.GetGroup(groupID) + if group == nil { + return false + } + + return group.HasMemberNamed(name) +} + +// GetGroupLeader returns the group leader +func (gm *GroupManager) GetGroupLeader(groupID int32) Entity { + group := gm.GetGroup(groupID) + if group == nil { + return nil + } + + return group.GetLeader() +} + +// MakeLeader changes the group leader +func (gm *GroupManager) MakeLeader(groupID int32, newLeader Entity) bool { + group := gm.GetGroup(groupID) + if group == nil { + return false + } + + return group.MakeLeader(newLeader) == nil +} + +// Messaging (preserving C++ API) + +// SimpleGroupMessage sends a simple message to all group members +func (gm *GroupManager) SimpleGroupMessage(groupID int32, message string) { + group := gm.GetGroup(groupID) + if group != nil { + group.SimpleGroupMessage(message) + } +} + +// SendGroupMessage sends a formatted message to all group members +func (gm *GroupManager) SendGroupMessage(groupID int32, msgType int8, message string) { + group := gm.GetGroup(groupID) + if group != nil { + group.SendGroupMessage(msgType, message) + } +} + +// GroupMessage sends a group message +func (gm *GroupManager) GroupMessage(groupID int32, message string) { + gm.SimpleGroupMessage(groupID, message) +} + +// GroupChatMessage sends a chat message from an entity +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) + } +} + +// GroupChatMessageFromName sends a chat message from a named sender +func (gm *GroupManager) GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) { + group := gm.GetGroup(groupID) + if group != nil { + group.GroupChatMessageFromName(fromName, language, message, channel) + } +} + +// SendGroupChatMessage sends a chat message to the group +func (gm *GroupManager) SendGroupChatMessage(groupID int32, channel int16, message string) { + gm.GroupChatMessageFromName(groupID, "System", 0, message, channel) +} + +// Group updates (preserving C++ API) + +// SendGroupUpdate sends an update to all group members +func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { + group := gm.GetGroup(groupID) + if group != nil { + group.SendGroupUpdate(excludeClient, forceRaidUpdate) + } +} + +// Raid functionality (preserving C++ API) + +// ClearGroupRaid clears all raid associations for a group +func (gm *GroupManager) ClearGroupRaid(groupID int32) { + group := gm.GetGroup(groupID) + if group != nil { + group.ClearGroupRaid() + } +} + +// RemoveGroupFromRaid removes a group from a raid +func (gm *GroupManager) RemoveGroupFromRaid(groupID, targetGroupID int32) { + group := gm.GetGroup(groupID) + if group != nil { + group.RemoveGroupFromRaid(targetGroupID) + } +} + +// IsInRaidGroup checks if a group is in a raid with another group +func (gm *GroupManager) IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool { + group := gm.GetGroup(groupID) + if group == nil { + return false + } + + return group.IsInRaidGroup(targetGroupID, isLeaderGroup) +} + +// GetRaidGroups returns the raid groups for a group +func (gm *GroupManager) GetRaidGroups(groupID int32) []int32 { + group := gm.GetGroup(groupID) + if group == nil { + return []int32{} + } + + return group.GetRaidGroups() +} + +// ReplaceRaidGroups replaces the raid groups for a group +func (gm *GroupManager) ReplaceRaidGroups(groupID int32, newGroups []int32) { + group := gm.GetGroup(groupID) + if group != nil { + group.ReplaceRaidGroups(newGroups) + } +} + +// Group options (preserving C++ API) + +// GetDefaultGroupOptions returns the default group options +func (gm *GroupManager) GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) { + group := gm.GetGroup(groupID) + if group == nil { + return GroupOptions{}, false + } + + return group.GetGroupOptions(), true +} + +// SetGroupOptions sets group options +func (gm *GroupManager) SetGroupOptions(groupID int32, options *GroupOptions) error { + group := gm.GetGroup(groupID) + if group == nil { + return fmt.Errorf("group not found: %d", groupID) + } + + return group.SetGroupOptions(options) +} + +// Statistics and utilities + +// GetStats returns the group manager statistics +func (gm *GroupManager) GetStats() GroupManagerStats { + gm.statsMutex.RLock() + defer gm.statsMutex.RUnlock() + + return gm.stats +} + +// GetGroupCount returns the current number of active groups +func (gm *GroupManager) GetGroupCount() int32 { + gm.groupsMutex.RLock() + defer gm.groupsMutex.RUnlock() + + return int32(len(gm.groups)) +} + +// GetAllGroups returns a copy of all groups +func (gm *GroupManager) GetAllGroups() []*Group { + gm.groupsMutex.RLock() + defer gm.groupsMutex.RUnlock() + + groups := make([]*Group, 0, len(gm.groups)) + for _, group := range gm.groups { + groups = append(groups, group) + } + + return groups +} + +// Helper methods + +// findGroupByLeaderName finds a group by leader name (helper method) +func (gm *GroupManager) findGroupByLeaderName(leaderName string) int32 { + gm.groupsMutex.RLock() + defer gm.groupsMutex.RUnlock() + + for groupID, group := range gm.groups { + if group.GetLeaderName() == leaderName { + return groupID + } + } + + return 0 +} + +// Group methods (preserved from C++ API) + +// GetID returns the group ID +func (g *Group) GetID() int32 { + return g.groupID +} + +// GetSize returns the number of members in the group +func (g *Group) GetSize() int32 { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + return int32(len(g.members)) +} + +// GetMembers returns a copy of the member list +func (g *Group) GetMembers() []*GroupMemberInfo { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + members := make([]*GroupMemberInfo, len(g.members)) + for i, member := range g.members { + // Create a copy + memberCopy := *member + members[i] = &memberCopy + } + + return members +} + +// AddMember adds a new member to the group +func (g *Group) AddMember(member Entity, isLeader bool) error { + if member == nil { + return fmt.Errorf("member cannot be nil") + } + + g.disbandMutex.RLock() + if g.disbanded { + g.disbandMutex.RUnlock() + return fmt.Errorf("group has been disbanded") + } + g.disbandMutex.RUnlock() + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Check if group is full + if len(g.members) >= MAX_GROUP_SIZE { + return fmt.Errorf("group is full") + } + + // Check if member is already in the group + for _, gmi := range g.members { + if gmi.Member == member { + return fmt.Errorf("member is already in the group") + } + } + + // Create new group member info + gmi := &GroupMemberInfo{ + GroupID: g.groupID, + Name: member.GetName(), + Leader: isLeader, + Member: member, + IsClient: member.IsPlayer(), + JoinTime: time.Now(), + LastUpdate: time.Now(), + HPCurrent: member.GetHP(), + HPMax: member.GetTotalHP(), + PowerCurrent: member.GetPower(), + PowerMax: member.GetTotalPower(), + LevelCurrent: int16(member.GetLevel()), + LevelMax: int16(member.GetLevel()), + RaceID: member.GetRace(), + ClassID: member.GetClass(), + } + + // Update zone information + if zone := member.GetZone(); zone != nil { + gmi.ZoneID = zone.GetZoneID() + gmi.InstanceID = zone.GetInstanceID() + gmi.Zone = zone.GetZoneName() + } + + // Add to members list + g.members = append(g.members, gmi) + g.updateLastActivity() + + return nil +} + +// AddMemberFromPeer adds a member from a peer server +func (g *Group) AddMemberFromPeer(info *GroupMemberInfo) error { + if info == nil { + return fmt.Errorf("member info cannot be nil") + } + + g.disbandMutex.RLock() + if g.disbanded { + g.disbandMutex.RUnlock() + return fmt.Errorf("group has been disbanded") + } + g.disbandMutex.RUnlock() + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Check if group is full + if len(g.members) >= MAX_GROUP_SIZE { + return fmt.Errorf("group is full") + } + + // Create a copy of the member info + memberCopy := *info + memberCopy.GroupID = g.groupID + memberCopy.JoinTime = time.Now() + memberCopy.LastUpdate = time.Now() + + // Add to members list + g.members = append(g.members, &memberCopy) + g.updateLastActivity() + + return nil +} + +// RemoveMember removes a member from the group +func (g *Group) RemoveMember(member Entity) error { + if member == nil { + return fmt.Errorf("member cannot be nil") + } + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Find and remove the member + for i, gmi := range g.members { + if gmi.Member == member { + // Remove from slice + g.members = append(g.members[:i], g.members[i+1:]...) + g.updateLastActivity() + return nil + } + } + + return fmt.Errorf("member not found in group") +} + +// RemoveMemberByName removes a member by name (for peer members) +func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) error { + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Find and remove the member + for i, gmi := range g.members { + if gmi.Name == name && gmi.IsClient == isClient { + // Handle mentorship cleanup + if isClient && charID > 0 { + for _, otherGmi := range g.members { + if otherGmi.MentorTargetCharID == charID { + otherGmi.MentorTargetCharID = 0 + } + } + } + + // Remove from slice + g.members = append(g.members[:i], g.members[i+1:]...) + g.updateLastActivity() + return nil + } + } + + return fmt.Errorf("member not found in group") +} + +// Disband disbands the group and removes all members +func (g *Group) Disband() { + g.disbandMutex.Lock() + if g.disbanded { + g.disbandMutex.Unlock() + return + } + g.disbanded = true + g.disbandMutex.Unlock() + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Clear raid groups + g.raidGroupsMutex.Lock() + g.raidGroups = nil + g.raidGroupsMutex.Unlock() + + // Clear members list + g.members = nil +} + +// SendGroupUpdate sends an update to all group members +func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + selfInRaid := g.IsGroupRaid() + + // Send updates to all members + for _, gmi := range g.members { + if gmi.Client != nil && gmi.Client != excludeClient { + // Build the appropriate update packet + if selfInRaid || forceRaidUpdate { + // Send raid update + packetData, err := g.buildRaidUpdatePacket(gmi.Client) + if err == nil && len(packetData) > 0 { + g.sendPacketToClient(gmi.Client, packets.OP_RaidUpdateMsg, packetData) + } + } else { + // Send group update + packetData, err := g.buildGroupUpdatePacket(gmi.Client) + if err == nil && len(packetData) > 0 { + g.sendPacketToClient(gmi.Client, packets.OP_GroupUpdateMsg, packetData) + } + } + + // Set character sheet changed flags (preserved from C++) + g.setCharSheetChanged(gmi.Client, selfInRaid || forceRaidUpdate) + } + } +} + +// SimpleGroupMessage sends a simple message to all group members +func (g *Group) SimpleGroupMessage(message string) { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + // Send message to all group members + for _, gmi := range g.members { + if gmi.Client != nil { + g.sendSimpleMessageToClient(gmi.Client, CHANNEL_GROUP_CHAT, message) + } + } +} + +// SendGroupMessage sends a formatted message to all group members +func (g *Group) SendGroupMessage(msgType int8, message string) { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + // Send formatted message to all group members + for _, gmi := range g.members { + if gmi.Client != nil { + g.sendFormattedMessageToClient(gmi.Client, msgType, message) + } + } +} + +// GroupChatMessage sends a chat message from a member to the group +func (g *Group) GroupChatMessage(from Entity, language int32, message string, channel int16) { + if from == nil { + return + } + + g.GroupChatMessageFromName(from.GetName(), language, message, channel) +} + +// GroupChatMessageFromName sends a chat message from a named sender to the group +func (g *Group) GroupChatMessageFromName(fromName string, language int32, message string, channel int16) { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + // Send chat message to all group members + for _, gmi := range g.members { + if gmi.Client != nil { + g.sendChatMessageToClient(gmi.Client, fromName, language, message, channel) + } + } +} + +// MakeLeader changes the group leader +func (g *Group) MakeLeader(newLeader Entity) error { + if newLeader == nil { + return fmt.Errorf("new leader cannot be nil") + } + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + var found bool + + // Find the new leader and update leadership + for _, gmi := range g.members { + if gmi.Member == newLeader { + found = true + gmi.Leader = true + } else if gmi.Leader { + // Remove leadership from current leader + gmi.Leader = false + } + } + + if !found { + return fmt.Errorf("new leader not found in group") + } + + g.updateLastActivity() + return nil +} + +// GetLeaderName returns the name of the group leader +func (g *Group) GetLeaderName() string { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + for _, gmi := range g.members { + if gmi.Leader { + return gmi.Name + } + } + + return "" +} + +// GetLeader returns the group leader entity +func (g *Group) GetLeader() Entity { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + for _, gmi := range g.members { + if gmi.Leader { + return gmi.Member + } + } + + return nil +} + +// HasMember checks if an entity is a member of the group +func (g *Group) HasMember(entity Entity) bool { + if entity == nil { + return false + } + + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + for _, gmi := range g.members { + if gmi.Member == entity { + return true + } + } + + return false +} + +// HasMemberNamed checks if the group has a member with the given name +func (g *Group) HasMemberNamed(name string) bool { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + for _, gmi := range g.members { + if gmi.Name == name { + return true + } + } + + return false +} + +// GetMemberByCharID returns a member by character ID +func (g *Group) GetMemberByCharID(charID int32) Entity { + g.membersMutex.RLock() + defer g.membersMutex.RUnlock() + + for _, gmi := range g.members { + // This is a simplified check - in reality, you'd need to get char ID from the entity + if gmi.Member != nil && gmi.IsClient { + return gmi.Member + } + } + + return nil +} + +// UpdateGroupMemberInfo updates information for a specific member +func (g *Group) UpdateGroupMemberInfo(member Entity) { + if member == nil { + return + } + + g.membersMutex.Lock() + defer g.membersMutex.Unlock() + + // Find the member and update their info + for _, gmi := range g.members { + if gmi.Member == member { + gmi.HPCurrent = member.GetHP() + gmi.HPMax = member.GetTotalHP() + gmi.PowerCurrent = member.GetPower() + gmi.PowerMax = member.GetTotalPower() + gmi.LevelCurrent = int16(member.GetLevel()) + gmi.LevelMax = int16(member.GetLevel()) + gmi.LastUpdate = time.Now() + + // Update zone information + if zone := member.GetZone(); zone != nil { + gmi.ZoneID = zone.GetZoneID() + gmi.InstanceID = zone.GetInstanceID() + gmi.Zone = zone.GetZoneName() + } + + g.updateLastActivity() + break + } + } +} + +// GetGroupOptions returns a copy of the group options +func (g *Group) GetGroupOptions() GroupOptions { + g.optionsMutex.RLock() + defer g.optionsMutex.RUnlock() + + return g.options +} + +// SetGroupOptions sets new group options +func (g *Group) SetGroupOptions(options *GroupOptions) error { + if options == nil { + return fmt.Errorf("options cannot be nil") + } + + if !options.IsValid() { + return fmt.Errorf("invalid group options") + } + + g.optionsMutex.Lock() + g.options = *options + g.optionsMutex.Unlock() + + g.updateLastActivity() + return nil +} + +// GetLastLooterIndex returns the last looter index +func (g *Group) GetLastLooterIndex() int8 { + g.optionsMutex.RLock() + defer g.optionsMutex.RUnlock() + + return g.options.LastLootedIndex +} + +// SetNextLooterIndex sets the next looter index +func (g *Group) SetNextLooterIndex(newIndex int8) { + g.optionsMutex.Lock() + g.options.LastLootedIndex = newIndex + g.optionsMutex.Unlock() + + g.updateLastActivity() +} + +// Raid functionality + +// GetRaidGroups returns a copy of the raid groups list +func (g *Group) GetRaidGroups() []int32 { + g.raidGroupsMutex.RLock() + defer g.raidGroupsMutex.RUnlock() + + if g.raidGroups == nil { + return []int32{} + } + + groups := make([]int32, len(g.raidGroups)) + copy(groups, g.raidGroups) + return groups +} + +// ReplaceRaidGroups replaces the entire raid groups list +func (g *Group) ReplaceRaidGroups(groups []int32) { + g.raidGroupsMutex.Lock() + defer g.raidGroupsMutex.Unlock() + + if groups == nil { + g.raidGroups = make([]int32, 0) + } else { + g.raidGroups = make([]int32, len(groups)) + copy(g.raidGroups, groups) + } + + g.updateLastActivity() +} + +// IsInRaidGroup checks if this group is in a raid with the specified group +func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool { + g.raidGroupsMutex.RLock() + defer g.raidGroupsMutex.RUnlock() + + for _, id := range g.raidGroups { + if id == groupID { + return true + } + } + + return false +} + +// AddGroupToRaid adds a group to the raid +func (g *Group) AddGroupToRaid(groupID int32) { + g.raidGroupsMutex.Lock() + defer g.raidGroupsMutex.Unlock() + + // Check if already in raid + for _, id := range g.raidGroups { + if id == groupID { + return + } + } + + g.raidGroups = append(g.raidGroups, groupID) + g.updateLastActivity() +} + +// RemoveGroupFromRaid removes a group from the raid +func (g *Group) RemoveGroupFromRaid(groupID int32) { + g.raidGroupsMutex.Lock() + defer g.raidGroupsMutex.Unlock() + + for i, id := range g.raidGroups { + if id == groupID { + g.raidGroups = append(g.raidGroups[:i], g.raidGroups[i+1:]...) + g.updateLastActivity() + break + } + } +} + +// IsGroupRaid checks if this group is part of a raid +func (g *Group) IsGroupRaid() bool { + g.raidGroupsMutex.RLock() + defer g.raidGroupsMutex.RUnlock() + + return len(g.raidGroups) > 0 +} + +// ClearGroupRaid clears all raid associations +func (g *Group) ClearGroupRaid() { + g.raidGroupsMutex.Lock() + defer g.raidGroupsMutex.Unlock() + + g.raidGroups = make([]int32, 0) + g.updateLastActivity() +} + +// IsDisbanded checks if the group has been disbanded +func (g *Group) IsDisbanded() bool { + g.disbandMutex.RLock() + defer g.disbandMutex.RUnlock() + + return g.disbanded +} + +// GetCreatedTime returns when the group was created +func (g *Group) GetCreatedTime() time.Time { + return g.createdTime +} + +// GetLastActivity returns the last activity time +func (g *Group) GetLastActivity() time.Time { + g.activityMutex.RLock() + defer g.activityMutex.RUnlock() + return g.lastActivity +} + +// updateLastActivity updates the last activity timestamp +func (g *Group) updateLastActivity() { + g.activityMutex.Lock() + defer g.activityMutex.Unlock() + g.lastActivity = time.Now() +} + +// Helper methods for GroupMemberInfo + +// Copy creates a copy of GroupMemberInfo +func (gmi *GroupMemberInfo) Copy() *GroupMemberInfo { + copy := *gmi + return © +} + +// IsValid checks if the group member info is valid +func (gmi *GroupMemberInfo) IsValid() bool { + return gmi.GroupID > 0 && len(gmi.Name) > 0 +} + +// Helper methods for GroupOptions + +// Copy creates a copy of GroupOptions +func (opts *GroupOptions) Copy() GroupOptions { + return *opts +} + +// IsValid checks if group options are valid +func (opts *GroupOptions) IsValid() bool { + return opts.LootMethod >= LOOT_METHOD_LEADER_ONLY && opts.LootMethod <= LOOT_METHOD_LOTTO && + opts.LootItemsRarity >= LOOT_RARITY_COMMON && opts.LootItemsRarity <= LOOT_RARITY_FABLED +} + +// Helper methods for GroupInvite + +// IsExpired checks if the group invite has expired +func (gi *GroupInvite) IsExpired() bool { + return time.Now().After(gi.ExpiresTime) +} + +// TimeRemaining returns the remaining time for the invite +func (gi *GroupInvite) TimeRemaining() time.Duration { + return time.Until(gi.ExpiresTime) +} + +// DefaultGroupOptions returns default group options +func DefaultGroupOptions() GroupOptions { + return GroupOptions{ + LootMethod: LOOT_METHOD_ROUND_ROBIN, + LootItemsRarity: LOOT_RARITY_COMMON, + AutoSplit: AUTO_SPLIT_DISABLED, + DefaultYell: DEFAULT_YELL_DISABLED, + GroupLockMethod: LOCK_METHOD_OPEN, + GroupAutolock: AUTO_LOCK_DISABLED, + SoloAutolock: AUTO_LOCK_DISABLED, + AutoLootMethod: AUTO_LOOT_DISABLED, + LastLootedIndex: 0, + } +} + +// nullLogger is a no-op logger implementation +type nullLogger struct{} + +func (nl *nullLogger) Debug(msg string, args ...any) {} +func (nl *nullLogger) Info(msg string, args ...any) {} +func (nl *nullLogger) Error(msg string, args ...any) {} + +// Packet building and sending methods + +// buildGroupUpdatePacket builds a group update packet using the packet system +func (g *Group) buildGroupUpdatePacket(client any) ([]byte, error) { + // Get client version (this would need to be implemented based on your client interface) + clientVersion := g.getClientVersion(client) + if clientVersion == 0 { + return nil, fmt.Errorf("invalid client version") + } + + // The packet structure depends on the client version and uses CharacterSheet.xml with GroupMember substruct + // For now, we'll build a basic structure - this would need to be enhanced based on your packet system + packet := make(map[string]any) + + // Add group members data + members := make([]map[string]any, 0, len(g.members)) + for i, gmi := range g.members { + if i >= MAX_GROUP_SIZE { + break + } + + memberData := map[string]any{ + "spawn_id": gmi.Member.GetID(), + "name": gmi.Name, + "zone": gmi.Zone, + "hp_current": gmi.HPCurrent, + "hp_max": gmi.HPMax, + "power_current": gmi.PowerCurrent, + "power_max": gmi.PowerMax, + "level_current": gmi.LevelCurrent, + "level_max": gmi.LevelMax, + "race_id": gmi.RaceID, + "class_id": gmi.ClassID, + "zone_status": uint8(1), // Assume online + "instance": uint8(gmi.InstanceID), + } + + // Add version-specific fields + if clientVersion >= 562 { + memberData["unknown4"] = uint16(0) + memberData["trauma_count"] = uint8(0) + memberData["arcane_count"] = uint8(0) + memberData["noxious_count"] = uint8(0) + memberData["elemental_count"] = uint8(0) + memberData["curse_count"] = uint8(0) + } + + members = append(members, memberData) + } + + packet["group_members"] = members + + // Use the packet builder to create the actual packet data + return packets.BuildPacket("CharacterSheet", packet, clientVersion, 0) +} + +// buildRaidUpdatePacket builds a raid update packet using the packet system +func (g *Group) buildRaidUpdatePacket(client any) ([]byte, error) { + clientVersion := g.getClientVersion(client) + if clientVersion == 0 { + return nil, fmt.Errorf("invalid client version") + } + + // Build raid packet using RaidUpdate.xml structure + packet := make(map[string]any) + + // Initialize all 24 raid slots (4 groups x 6 members each) + for groupNum := 0; groupNum < MAX_RAID_GROUPS; groupNum++ { + for memberNum := 0; memberNum < MAX_GROUP_SIZE; memberNum++ { + slotName := fmt.Sprintf("group_member%d_%d", memberNum, groupNum) + + // Initialize empty slot + memberData := map[string]any{ + "spawn_id": uint32(0), + "name": "", + "zone": "", + "hp_current": int32(0), + "hp_max": int32(0), + "power_current": int32(0), + "power_max": int32(0), + "level_current": uint16(0), + "level_max": uint16(0), + "race_id": uint8(0), + "class_id": uint8(0), + "zone_status": uint8(0), + "instance": uint8(0), + } + + packet[slotName] = memberData + } + } + + // Fill in actual group members for this group + for i, gmi := range g.members { + if i >= MAX_GROUP_SIZE { + break + } + + slotName := fmt.Sprintf("group_member%d_0", i) // Assume this group is raid group 0 + memberData := map[string]any{ + "spawn_id": gmi.Member.GetID(), + "name": gmi.Name, + "zone": gmi.Zone, + "hp_current": gmi.HPCurrent, + "hp_max": gmi.HPMax, + "power_current": gmi.PowerCurrent, + "power_max": gmi.PowerMax, + "level_current": gmi.LevelCurrent, + "level_max": gmi.LevelMax, + "race_id": gmi.RaceID, + "class_id": gmi.ClassID, + "zone_status": uint8(1), + "instance": uint8(gmi.InstanceID), + } + + if clientVersion >= 57048 { + memberData["unknown5"] = []uint8{0, 0, 0} + } + + packet[slotName] = memberData + } + + return packets.BuildPacket("RaidUpdate", packet, clientVersion, 0) +} + +// sendPacketToClient sends a packet to a specific client +func (g *Group) sendPacketToClient(client any, opcode packets.InternalOpcode, data []byte) { + // This would integrate with your client system to actually send the packet + // For now, this is a placeholder that would need to be implemented based on your architecture + + // In the real implementation, this would: + // 1. Get the client version + // 2. Convert internal opcode to client opcode + // 3. Send the packet data to the client + + // Example (pseudocode): + // clientVersion := client.GetVersion() + // clientOpcode := packets.InternalToClient(opcode, clientVersion) + // client.SendPacket(clientOpcode, data) +} + +// setCharSheetChanged sets character sheet changed flags (preserved from C++) +func (g *Group) setCharSheetChanged(client any, raidUpdate bool) { + // This would call the equivalent of: + // client.GetPlayer().SetCharSheetChanged(true) + // if raidUpdate { + // client.GetPlayer().SetRaidSheetChanged(true) + // } + + // Placeholder - needs integration with your player/client system +} + +// getClientVersion gets the client version for packet building +func (g *Group) getClientVersion(client any) uint32 { + // This would extract the client version from your client interface + // For now, return a default version + return 1188 // Default to a common client version +} + +// sendSimpleMessageToClient sends a simple message to a client +func (g *Group) sendSimpleMessageToClient(client any, channel int16, message string) { + // Build a simple message packet + packetData := g.buildSimpleMessagePacket(client, channel, message) + if len(packetData) > 0 { + g.sendPacketToClient(client, packets.OP_GroupMessageMsg, packetData) + } +} + +// sendFormattedMessageToClient sends a formatted message to a client +func (g *Group) sendFormattedMessageToClient(client any, msgType int8, message string) { + // Build a formatted message packet + packetData := g.buildFormattedMessagePacket(client, msgType, message) + if len(packetData) > 0 { + g.sendPacketToClient(client, packets.OP_GroupMessageMsg, packetData) + } +} + +// sendChatMessageToClient sends a chat message to a client +func (g *Group) sendChatMessageToClient(client any, fromName string, language int32, message string, channel int16) { + // Build a chat message packet + packetData := g.buildChatMessagePacket(client, fromName, language, message, channel) + if len(packetData) > 0 { + g.sendPacketToClient(client, packets.OP_GroupChatMsg, packetData) + } +} + +// buildSimpleMessagePacket builds a simple message packet +func (g *Group) buildSimpleMessagePacket(client any, channel int16, message string) []byte { + clientVersion := g.getClientVersion(client) + + packet := map[string]any{ + "channel": channel, + "message": message, + "type": GROUP_MESSAGE_TYPE_SYSTEM, + } + + data, err := packets.BuildPacket("GroupMessage", packet, clientVersion, 0) + if err != nil { + return nil + } + + return data +} + +// buildFormattedMessagePacket builds a formatted message packet +func (g *Group) buildFormattedMessagePacket(client any, msgType int8, message string) []byte { + clientVersion := g.getClientVersion(client) + + packet := map[string]any{ + "channel": CHANNEL_GROUP_CHAT, + "message": message, + "type": msgType, + } + + data, err := packets.BuildPacket("GroupMessage", packet, clientVersion, 0) + if err != nil { + return nil + } + + return data +} + +// buildChatMessagePacket builds a chat message packet +func (g *Group) buildChatMessagePacket(client any, fromName string, language int32, message string, channel int16) []byte { + clientVersion := g.getClientVersion(client) + + packet := map[string]any{ + "channel": channel, + "message": message, + "from": fromName, + "language": language, + "type": GROUP_MESSAGE_TYPE_CHAT, + } + + data, err := packets.BuildPacket("HearChat", packet, clientVersion, 0) + if err != nil { + return nil + } + + return data +} \ No newline at end of file diff --git a/internal/groups/interfaces.go b/internal/groups/interfaces.go deleted file mode 100644 index bd19090..0000000 --- a/internal/groups/interfaces.go +++ /dev/null @@ -1,501 +0,0 @@ -package groups - -import ( - "time" -) - -// GroupAware interface for entities that can be part of groups -type GroupAware interface { - // GetGroupMemberInfo returns the group member info for this entity - GetGroupMemberInfo() *GroupMemberInfo - - // SetGroupMemberInfo sets the group member info for this entity - SetGroupMemberInfo(info *GroupMemberInfo) - - // GetGroupID returns the current group ID - GetGroupID() int32 - - // SetGroupID sets the current group ID - SetGroupID(groupID int32) - - // IsInGroup returns true if the entity is in a group - IsInGroup() bool -} - -// GroupManager interface for managing groups -type GroupManagerInterface interface { - // Group creation and management - 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, isLeader bool) error - AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) 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, 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) bool - IsPlayerInGroup(groupID int32, charID int32) Entity - IsSpawnInGroup(groupID int32, name string) 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, language int32, message string, channel int16) - GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) - SendGroupChatMessage(groupID int32, channel int16, message string) - - // Raid functionality - ClearGroupRaid(groupID int32) - RemoveGroupFromRaid(groupID, targetGroupID int32) - IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool - GetRaidGroups(groupID int32) []int32 - ReplaceRaidGroups(groupID int32, newGroups []int32) - - // Group options - GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) - SetGroupOptions(groupID int32, options *GroupOptions) error - - // Statistics - GetStats() GroupManagerStats - GetGroupCount() int32 - GetAllGroups() []*Group -} - -// GroupEventHandler interface for handling group events -type GroupEventHandler interface { - // Group lifecycle events - OnGroupCreated(group *Group, leader Entity) error - OnGroupDisbanded(group *Group) 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) error - OnGroupInviteAccepted(leader, member Entity, groupID int32) error - OnGroupInviteDeclined(leader, member Entity) error - OnGroupInviteExpired(leader, member Entity) error - - // Raid events - OnRaidFormed(groups []*Group) error - OnRaidDisbanded(groups []*Group) error - OnRaidInviteSent(leaderGroup *Group, targetGroup *Group) error - OnRaidInviteAccepted(leaderGroup *Group, targetGroup *Group) error - OnRaidInviteDeclined(leaderGroup *Group, targetGroup *Group) error - - // Group activity events - OnGroupMessage(group *Group, from Entity, message string, channel int16) error - OnGroupOptionsChanged(group *Group, oldOptions, newOptions *GroupOptions) error - OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error -} - -// GroupDatabase interface for database operations -type GroupDatabase interface { - // Group persistence - SaveGroup(group *Group) error - LoadGroup(groupID int32) (*Group, error) - DeleteGroup(groupID int32) error - - // Group member persistence - SaveGroupMember(groupID int32, member *GroupMemberInfo) error - LoadGroupMembers(groupID int32) ([]*GroupMemberInfo, error) - DeleteGroupMember(groupID int32, memberName string) error - - // Group options persistence - SaveGroupOptions(groupID int32, options *GroupOptions) error - LoadGroupOptions(groupID int32) (*GroupOptions, error) - - // Raid persistence - SaveRaidGroups(groupID int32, raidGroups []int32) error - LoadRaidGroups(groupID int32) ([]int32, error) - - // Statistics persistence - SaveGroupStats(stats *GroupManagerStats) error - LoadGroupStats() (*GroupManagerStats, error) - - // Cleanup operations - CleanupExpiredGroups() error - CleanupOrphanedMembers() error -} - -// GroupPacketHandler interface for handling group-related packets -type GroupPacketHandler interface { - // Group update packets - SendGroupUpdate(members []*GroupMemberInfo, excludeClient any) error - SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient any) error - SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient any) error - - // Group invitation packets - SendGroupInvite(inviter, invitee Entity) error - SendGroupInviteResponse(inviter, invitee Entity, accepted bool) error - - // Group messaging packets - SendGroupMessage(members []*GroupMemberInfo, message *GroupMessage) error - SendGroupChatMessage(members []*GroupMemberInfo, from string, message string, channel int16, language int32) error - - // Raid packets - SendRaidUpdate(raidGroups []*Group, excludeClient any) error - SendRaidInvite(leaderGroup, targetGroup *Group) error - SendRaidInviteResponse(leaderGroup, targetGroup *Group, accepted bool) error - - // Group UI packets - SendGroupWindowUpdate(client any, group *Group) error - SendRaidWindowUpdate(client any, raidGroups []*Group) error - - // Group member packets - SendGroupMemberStats(member *GroupMemberInfo, excludeClient any) error - SendGroupMemberZoneChange(member *GroupMemberInfo, oldZoneID, newZoneID int32) error -} - -// GroupValidator interface for validating group operations -type GroupValidator interface { - // Group creation validation - ValidateGroupCreation(leader Entity, options *GroupOptions) error - ValidateGroupJoin(group *Group, member Entity) error - ValidateGroupLeave(group *Group, member Entity) error - - // Invitation validation - ValidateGroupInvite(leader, member Entity) error - ValidateRaidInvite(leaderGroup, targetGroup *Group) error - - // Group operation validation - ValidateLeadershipChange(group *Group, oldLeader, newLeader Entity) error - ValidateGroupOptions(group *Group, options *GroupOptions) error - ValidateGroupMessage(group *Group, from Entity, message string) error - - // Raid validation - ValidateRaidFormation(groups []*Group) error - ValidateRaidOperation(raidGroups []*Group, operation string) error -} - -// GroupNotifier interface for sending notifications -type GroupNotifier interface { - // Group notifications - NotifyGroupCreated(group *Group, leader Entity) error - NotifyGroupDisbanded(group *Group, reason string) 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) 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 - NotifyRaidDisbanded(groups []*Group, reason string) error - NotifyRaidInviteSent(leaderGroup, targetGroup *Group) error - NotifyRaidInviteReceived(leaderGroup, targetGroup *Group) error - NotifyRaidInviteAccepted(leaderGroup, targetGroup *Group) error - NotifyRaidInviteDeclined(leaderGroup, targetGroup *Group) error - - // System notifications - NotifyGroupSystemMessage(group *Group, message string, msgType int8) error - NotifyGroupError(group *Group, error string, errorCode int8) error -} - -// GroupStatistics interface for tracking group statistics -type GroupStatistics interface { - // Group statistics - RecordGroupCreated(group *Group, leader Entity) - RecordGroupDisbanded(group *Group, duration int64) - RecordGroupMemberJoined(group *Group, member Entity) - RecordGroupMemberLeft(group *Group, member Entity, duration int64) - - // Invitation statistics - 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, messageType int8) - RecordGroupActivity(group *Group, activityType string) - - // Performance statistics - RecordGroupProcessingTime(operation string, duration int64) - RecordGroupMemoryUsage(groups int32, members int32) - - // Statistics retrieval - GetGroupStatistics(groupID int32) map[string]any - GetOverallStatistics() map[string]any - GetStatisticsSummary() *GroupManagerStats -} - -// GroupAdapter adapts group functionality for other systems -type GroupAdapter struct { - group *Group -} - -// NewGroupAdapter creates a new group adapter -func NewGroupAdapter(group *Group) *GroupAdapter { - return &GroupAdapter{group: group} -} - -// GetGroup returns the wrapped group -func (ga *GroupAdapter) GetGroup() *Group { - return ga.group -} - -// GetGroupID returns the group ID -func (ga *GroupAdapter) GetGroupID() int32 { - return ga.group.GetID() -} - -// GetGroupSize returns the group size -func (ga *GroupAdapter) GetGroupSize() int32 { - return ga.group.GetSize() -} - -// GetMembers returns group members -func (ga *GroupAdapter) GetMembers() []*GroupMemberInfo { - return ga.group.GetMembers() -} - -// GetLeader returns the group leader -func (ga *GroupAdapter) GetLeader() Entity { - members := ga.group.GetMembers() - for _, member := range members { - if member.Leader { - return member.Member - } - } - return nil -} - -// GetLeaderName returns the group leader's name -func (ga *GroupAdapter) GetLeaderName() string { - return ga.group.GetLeaderName() -} - -// IsInRaid returns true if the group is part of a raid -func (ga *GroupAdapter) IsInRaid() bool { - return ga.group.IsGroupRaid() -} - -// GetRaidGroups returns the raid groups -func (ga *GroupAdapter) GetRaidGroups() []int32 { - return ga.group.GetRaidGroups() -} - -// IsMember checks if an entity is a member of the group -func (ga *GroupAdapter) IsMember(entity Entity) bool { - if entity == nil { - return false - } - - members := ga.group.GetMembers() - for _, member := range members { - if member.Member == entity { - return true - } - } - return false -} - -// HasMemberNamed checks if the group has a member with the given name -func (ga *GroupAdapter) HasMemberNamed(name string) bool { - members := ga.group.GetMembers() - for _, member := range members { - if member.Name == name { - return true - } - } - return false -} - -// GetMemberByName returns a member by name -func (ga *GroupAdapter) GetMemberByName(name string) *GroupMemberInfo { - members := ga.group.GetMembers() - for _, member := range members { - if member.Name == name { - return member - } - } - return nil -} - -// GetMemberByEntity returns a member by entity -func (ga *GroupAdapter) GetMemberByEntity(entity Entity) *GroupMemberInfo { - if entity == nil { - return nil - } - - members := ga.group.GetMembers() - for _, member := range members { - if member.Member == entity { - return member - } - } - return nil -} - -// IsLeader checks if an entity is the group leader -func (ga *GroupAdapter) IsLeader(entity Entity) bool { - if entity == nil { - return false - } - - members := ga.group.GetMembers() - for _, member := range members { - if member.Member == entity && member.Leader { - return true - } - } - return false -} - -// GetOptions returns the group options -func (ga *GroupAdapter) GetOptions() GroupOptions { - return ga.group.GetGroupOptions() -} - -// IsDisbanded returns true if the group has been disbanded -func (ga *GroupAdapter) IsDisbanded() bool { - return ga.group.IsDisbanded() -} - -// GetCreatedTime returns when the group was created -func (ga *GroupAdapter) GetCreatedTime() time.Time { - return ga.group.GetCreatedTime() -} - -// GetLastActivity returns the last activity time -func (ga *GroupAdapter) GetLastActivity() time.Time { - return ga.group.GetLastActivity() -} - -// EntityGroupAdapter adapts entity functionality for group systems -type EntityGroupAdapter struct { - entity Entity -} - -// NewEntityGroupAdapter creates a new entity group adapter -func NewEntityGroupAdapter(entity Entity) *EntityGroupAdapter { - return &EntityGroupAdapter{entity: entity} -} - -// GetEntity returns the wrapped entity -func (ega *EntityGroupAdapter) GetEntity() Entity { - return ega.entity -} - -// GetName returns the entity name -func (ega *EntityGroupAdapter) GetName() string { - return ega.entity.GetName() -} - -// GetLevel returns the entity level -func (ega *EntityGroupAdapter) GetLevel() int8 { - return ega.entity.GetLevel() -} - -// GetClass returns the entity class -func (ega *EntityGroupAdapter) GetClass() int8 { - return ega.entity.GetClass() -} - -// GetRace returns the entity race -func (ega *EntityGroupAdapter) GetRace() int8 { - return ega.entity.GetRace() -} - -// GetZoneID returns the current zone ID -func (ega *EntityGroupAdapter) GetZoneID() int32 { - if zone := ega.entity.GetZone(); zone != nil { - return zone.GetZoneID() - } - return 0 -} - -// GetInstanceID returns the current instance ID -func (ega *EntityGroupAdapter) GetInstanceID() int32 { - if zone := ega.entity.GetZone(); zone != nil { - return zone.GetInstanceID() - } - return 0 -} - -// GetZoneName returns the current zone name -func (ega *EntityGroupAdapter) GetZoneName() string { - if zone := ega.entity.GetZone(); zone != nil { - return zone.GetZoneName() - } - return "" -} - -// GetHP returns current HP -func (ega *EntityGroupAdapter) GetHP() int32 { - return ega.entity.GetHP() -} - -// GetMaxHP returns maximum HP -func (ega *EntityGroupAdapter) GetMaxHP() int32 { - return ega.entity.GetTotalHP() -} - -// GetPower returns current power -func (ega *EntityGroupAdapter) GetPower() int32 { - return ega.entity.GetPower() -} - -// GetMaxPower returns maximum power -func (ega *EntityGroupAdapter) GetMaxPower() int32 { - return ega.entity.GetTotalPower() -} - -// IsPlayer returns true if the entity is a player -func (ega *EntityGroupAdapter) IsPlayer() bool { - return ega.entity.IsPlayer() -} - -// IsNPC returns true if the entity is an NPC -func (ega *EntityGroupAdapter) IsNPC() bool { - return ega.entity.IsNPC() -} - -// IsBot returns true if the entity is a bot -func (ega *EntityGroupAdapter) IsBot() bool { - return ega.entity.IsBot() -} - -// IsAlive returns true if the entity is alive -func (ega *EntityGroupAdapter) IsAlive() bool { - return !ega.entity.IsDead() -} - -// IsDead returns true if the entity is dead -func (ega *EntityGroupAdapter) IsDead() bool { - return ega.entity.IsDead() -} - -// GetDistance returns distance to another entity -func (ega *EntityGroupAdapter) GetDistance(other Entity) float32 { - return ega.entity.GetDistance(other) -} diff --git a/internal/groups/manager.go b/internal/groups/manager.go deleted file mode 100644 index 9560c42..0000000 --- a/internal/groups/manager.go +++ /dev/null @@ -1,510 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "time" -) - -// Manager provides group management with embedded database operations -type Manager struct { - // Core fields with embedded database operations - MasterList *MasterList `json:"master_list"` - Config GroupManagerConfig `json:"config"` - Stats GroupManagerStats `json:"stats"` - - // Group ID generation - nextGroupID int32 `json:"-" db:"next_group_id"` - nextGroupIDMutex sync.Mutex `json:"-"` - - // Pending invitations - PendingInvites map[string]*GroupInvite `json:"pending_invites"` - RaidPendingInvites map[string]*GroupInvite `json:"raid_pending_invites"` - invitesMutex sync.RWMutex `json:"-"` - - // Event handlers - EventHandlers []GroupEventHandler `json:"-"` - eventHandlersMutex sync.RWMutex `json:"-"` - - // Statistics - statsMutex sync.RWMutex `json:"-"` - - // Background processing - stopChan chan struct{} `json:"-"` - wg sync.WaitGroup `json:"-"` - - // Integration interfaces - database GroupDatabase `json:"-"` - packetHandler GroupPacketHandler `json:"-"` - validator GroupValidator `json:"-"` - notifier GroupNotifier `json:"-"` - - // Database integration - embedded operations - db any `json:"-"` // Database connection - isNew bool `json:"-"` // Flag for new managers -} - -// New creates a new group manager -func NewManager(config GroupManagerConfig, db any) *Manager { - manager := &Manager{ - MasterList: NewMasterList(), - Config: config, - Stats: GroupManagerStats{}, - nextGroupID: 1, - PendingInvites: make(map[string]*GroupInvite), - RaidPendingInvites: make(map[string]*GroupInvite), - EventHandlers: make([]GroupEventHandler, 0), - stopChan: make(chan struct{}), - db: db, - isNew: true, - } - - return manager -} - -// Save saves the manager state to the database -func (m *Manager) Save() error { - // TODO: Implement database save logic - return nil -} - -// Delete removes the manager from the database -func (m *Manager) Delete() error { - // Stop the manager first - m.Stop() - - // TODO: Implement database delete logic - return nil -} - -// Reload refreshes the manager from the database -func (m *Manager) Reload() error { - // TODO: Implement database reload logic - return nil -} - -// Start starts the group manager background processes -func (m *Manager) Start() error { - // Start background processes - if m.Config.UpdateInterval > 0 { - m.wg.Add(1) - go m.updateGroupsLoop() - } - - if m.Config.BuffUpdateInterval > 0 { - m.wg.Add(1) - go m.updateBuffsLoop() - } - - m.wg.Add(1) - go m.cleanupExpiredInvitesLoop() - - if m.Config.EnableStatistics { - m.wg.Add(1) - go m.updateStatsLoop() - } - - return nil -} - -// Stop stops the group manager and all background processes -func (m *Manager) Stop() error { - close(m.stopChan) - m.wg.Wait() - return nil -} - -// NewGroup creates a new group with the given leader and options -func (m *Manager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) { - if leader == nil { - return 0, fmt.Errorf("leader cannot be nil") - } - - var groupID int32 - if overrideGroupID > 0 { - groupID = overrideGroupID - } else { - groupID = m.generateNextGroupID() - } - - // Check if group ID already exists - if m.MasterList.GetGroup(groupID) != nil && overrideGroupID == 0 { - return 0, fmt.Errorf("group ID %d already exists", groupID) - } - - // Create new group - group := NewGroup(groupID, options, m.db) - - // Add leader to the group - if err := group.AddMember(leader, true); err != nil { - group.Disband() - return 0, fmt.Errorf("failed to add leader to group: %v", err) - } - - // Add group to manager - if !m.MasterList.AddGroup(group) { - group.Disband() - return 0, fmt.Errorf("failed to add group to master list") - } - - // Update statistics - m.updateStatsForNewGroup() - - // Fire event - m.fireGroupCreatedEvent(group, leader) - - return groupID, nil -} - -// RemoveGroup removes a group from the manager -func (m *Manager) RemoveGroup(groupID int32) error { - group := m.MasterList.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - - // Disband the group - group.Disband() - - // Remove from master list - if !m.MasterList.RemoveGroup(groupID) { - return fmt.Errorf("failed to remove group %d from master list", groupID) - } - - // Update statistics - m.updateStatsForRemovedGroup() - - // Fire event - m.fireGroupDisbandedEvent(group) - - return nil -} - -// GetGroup returns a group by ID -func (m *Manager) GetGroup(groupID int32) *Group { - return m.MasterList.GetGroup(groupID) -} - -// IsGroupIDValid checks if a group ID is valid and exists -func (m *Manager) IsGroupIDValid(groupID int32) bool { - return m.MasterList.GetGroup(groupID) != nil -} - -// AddGroupMember adds a member to an existing group -func (m *Manager) AddGroupMember(groupID int32, member Entity, isLeader bool) error { - group := m.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - - return group.AddMember(member, isLeader) -} - -// AddGroupMemberFromPeer adds a member from a peer server to an existing group -func (m *Manager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error { - group := m.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - - return group.AddMemberFromPeer( - info.Name, info.Leader, info.IsClient, info.ClassID, - info.HPCurrent, info.HPMax, info.LevelCurrent, info.LevelMax, - info.PowerCurrent, info.PowerMax, info.RaceID, info.Zone, - info.MentorTargetCharID, info.ZoneID, info.InstanceID, - info.ClientPeerAddress, info.ClientPeerPort, info.IsRaidLooter, - ) -} - -// RemoveGroupMember removes a member from a group -func (m *Manager) RemoveGroupMember(groupID int32, member Entity) error { - group := m.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - - err := group.RemoveMember(member) - if err != nil { - return err - } - - // If group is now empty, remove it - if group.GetSize() == 0 { - m.RemoveGroup(groupID) - } - - return nil -} - -// RemoveGroupMemberByName removes a member by name from a group -func (m *Manager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error { - group := m.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - - err := group.RemoveMemberByName(name, isClient, charID) - if err != nil { - return err - } - - // If group is now empty, remove it - if group.GetSize() == 0 { - m.RemoveGroup(groupID) - } - - return nil -} - -// SendGroupUpdate sends an update to all members of a group -func (m *Manager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { - group := m.GetGroup(groupID) - if group != nil { - group.SendGroupUpdate(excludeClient, forceRaidUpdate) - } -} - -// Invite handles inviting a player to a group -func (m *Manager) Invite(leader Entity, member Entity) int8 { - if leader == nil || member == nil { - return GROUP_INVITE_TARGET_NOT_FOUND - } - - // Check if inviting self - if leader == member { - return GROUP_INVITE_SELF_INVITE - } - - // Check if member already has an invite - inviteKey := member.GetName() - if m.hasPendingInvite(inviteKey) != "" { - return GROUP_INVITE_ALREADY_HAS_INVITE - } - - // Check if member is already in a group - // TODO: Check if member already in group - // if member.GetGroupMemberInfo() != nil { - // return GROUP_INVITE_ALREADY_IN_GROUP - // } - - // Add the invite - if !m.addInvite(leader, member) { - return GROUP_INVITE_PERMISSION_DENIED - } - - // Fire event - m.fireGroupInviteSentEvent(leader, member) - - return GROUP_INVITE_SUCCESS -} - -// AddInvite adds a group invitation -func (m *Manager) AddInvite(leader Entity, member Entity) bool { - return m.addInvite(leader, member) -} - -// addInvite internal method to add an invitation -func (m *Manager) addInvite(leader Entity, member Entity) bool { - if leader == nil || member == nil { - return false - } - - inviteKey := member.GetName() - leaderName := leader.GetName() - - invite := &GroupInvite{ - InviterName: leaderName, - InviteeName: inviteKey, - GroupID: 0, // Will be set when group is created - IsRaidInvite: false, - CreatedTime: time.Now(), - ExpiresTime: time.Now().Add(m.Config.InviteTimeout), - } - - m.invitesMutex.Lock() - m.PendingInvites[inviteKey] = invite - m.invitesMutex.Unlock() - - // Update statistics - m.updateStatsForInvite() - - return true -} - -// AcceptInvite handles accepting of a group invite -func (m *Manager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 { - if member == nil { - return GROUP_INVITE_TARGET_NOT_FOUND - } - - inviteKey := member.GetName() - - m.invitesMutex.Lock() - invite, exists := m.PendingInvites[inviteKey] - if !exists { - m.invitesMutex.Unlock() - return GROUP_INVITE_TARGET_NOT_FOUND - } - - // Check if invite has expired - if invite.IsExpired() { - delete(m.PendingInvites, inviteKey) - m.invitesMutex.Unlock() - m.updateStatsForExpiredInvite() - return GROUP_INVITE_DECLINED - } - - // Remove the invite - delete(m.PendingInvites, inviteKey) - m.invitesMutex.Unlock() - - if !autoAddGroup { - return GROUP_INVITE_SUCCESS - } - - // Find the leader - var leader Entity - // TODO: Find leader entity by name - // leader = world.GetPlayerByName(invite.InviterName) - - if leader == nil { - return GROUP_INVITE_TARGET_NOT_FOUND - } - - var groupID int32 - if groupOverrideID != nil { - groupID = *groupOverrideID - } - - // Check if leader already has a group - // TODO: Get leader's group ID - // leaderGroupID := leader.GetGroupID() - - leaderGroupID := int32(0) // Placeholder - - if leaderGroupID == 0 { - // Create new group with leader - var err error - if groupID != 0 { - groupID, err = m.NewGroup(leader, nil, groupID) - } else { - groupID, err = m.NewGroup(leader, nil, 0) - } - if err != nil { - return GROUP_INVITE_PERMISSION_DENIED - } - } else { - groupID = leaderGroupID - } - - // Add member to the group - if err := m.AddGroupMember(groupID, member, false); err != nil { - return GROUP_INVITE_GROUP_FULL - } - - // Update statistics - m.updateStatsForAcceptedInvite() - - // Fire event - m.fireGroupInviteAcceptedEvent(leader, member, groupID) - - return GROUP_INVITE_SUCCESS -} - -// DeclineInvite handles declining of a group invite -func (m *Manager) DeclineInvite(member Entity) { - if member == nil { - return - } - - inviteKey := member.GetName() - - m.invitesMutex.Lock() - _, exists := m.PendingInvites[inviteKey] - if exists { - delete(m.PendingInvites, inviteKey) - } - m.invitesMutex.Unlock() - - if exists { - // Update statistics - m.updateStatsForDeclinedInvite() - - // Fire event - var leader Entity - // TODO: Find leader entity by name - // leader = world.GetPlayerByName(invite.InviterName) - m.fireGroupInviteDeclinedEvent(leader, member) - } -} - -// ClearPendingInvite clears a pending invite for a member -func (m *Manager) ClearPendingInvite(member Entity) { - if member == nil { - return - } - - inviteKey := member.GetName() - - m.invitesMutex.Lock() - delete(m.PendingInvites, inviteKey) - m.invitesMutex.Unlock() -} - -// HasPendingInvite checks if a member has a pending invite and returns the inviter name -func (m *Manager) HasPendingInvite(member Entity) string { - if member == nil { - return "" - } - - inviteKey := member.GetName() - return m.hasPendingInvite(inviteKey) -} - -// hasPendingInvite internal method to check for pending invites -func (m *Manager) hasPendingInvite(inviteKey string) string { - m.invitesMutex.RLock() - defer m.invitesMutex.RUnlock() - - if invite, exists := m.PendingInvites[inviteKey]; exists { - if !invite.IsExpired() { - return invite.InviterName - } - } - - return "" -} - -// generateNextGroupID generates the next available group ID -func (m *Manager) generateNextGroupID() int32 { - m.nextGroupIDMutex.Lock() - defer m.nextGroupIDMutex.Unlock() - - id := m.nextGroupID - m.nextGroupID++ - - // Handle overflow - if m.nextGroupID <= 0 { - m.nextGroupID = 1 - } - - return id -} - -// GetGroupCount returns the number of active groups -func (m *Manager) GetGroupCount() int32 { - return int32(m.MasterList.Size()) -} - -// GetAllGroups returns all active groups -func (m *Manager) GetAllGroups() []*Group { - return m.MasterList.GetAllGroups() -} - -// GetStats returns current manager statistics -func (m *Manager) GetStats() GroupManagerStats { - m.statsMutex.RLock() - defer m.statsMutex.RUnlock() - - return m.Stats -} diff --git a/internal/groups/manager_methods.go b/internal/groups/manager_methods.go deleted file mode 100644 index e533aa3..0000000 --- a/internal/groups/manager_methods.go +++ /dev/null @@ -1,528 +0,0 @@ -package groups - -import ( - "fmt" - "time" -) - -// Group utility methods - -// GetGroupSize returns the size of a group -func (m *Manager) GetGroupSize(groupID int32) int32 { - group := m.GetGroup(groupID) - if group == nil { - return 0 - } - return group.GetSize() -} - -// IsInGroup checks if an entity is in a specific group -func (m *Manager) IsInGroup(groupID int32, member Entity) bool { - group := m.GetGroup(groupID) - if group == nil || member == nil { - return false - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Member == member { - return true - } - } - - return false -} - -// IsPlayerInGroup checks if a player with the given character ID is in a group -func (m *Manager) IsPlayerInGroup(groupID int32, charID int32) Entity { - group := m.GetGroup(groupID) - if group == nil { - return nil - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.IsClient && gmi.Member != nil { - // TODO: Check character ID - // if gmi.Member.GetCharacterID() == charID { - // return gmi.Member - // } - } - } - - return nil -} - -// IsSpawnInGroup checks if a spawn with the given name is in a group -func (m *Manager) IsSpawnInGroup(groupID int32, name string) bool { - group := m.GetGroup(groupID) - if group == nil { - return false - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Name == name { - return true - } - } - - return false -} - -// GetGroupLeader returns the leader of a group -func (m *Manager) GetGroupLeader(groupID int32) Entity { - group := m.GetGroup(groupID) - if group == nil { - return nil - } - - members := group.GetMembers() - for _, gmi := range members { - if gmi.Leader { - return gmi.Member - } - } - - return nil -} - -// MakeLeader changes the leader of a group -func (m *Manager) MakeLeader(groupID int32, newLeader Entity) bool { - group := m.GetGroup(groupID) - if group == nil { - return false - } - - err := group.MakeLeader(newLeader) - return err == nil -} - -// Group messaging - -// SimpleGroupMessage sends a simple message to all members of a group -func (m *Manager) SimpleGroupMessage(groupID int32, message string) { - group := m.GetGroup(groupID) - if group != nil { - group.SimpleGroupMessage(message) - } -} - -// SendGroupMessage sends a formatted message to all members of a group -func (m *Manager) SendGroupMessage(groupID int32, msgType int8, message string) { - group := m.GetGroup(groupID) - if group != nil { - group.SendGroupMessage(msgType, message) - } -} - -// GroupMessage sends a message to all members of a group (alias for SimpleGroupMessage) -func (m *Manager) GroupMessage(groupID int32, message string) { - m.SimpleGroupMessage(groupID, message) -} - -// GroupChatMessage sends a chat message from a member to the group -func (m *Manager) GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16) { - group := m.GetGroup(groupID) - if group != nil { - group.GroupChatMessage(from, language, message, channel) - } -} - -// GroupChatMessageFromName sends a chat message from a named sender to the group -func (m *Manager) GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) { - group := m.GetGroup(groupID) - if group != nil { - group.GroupChatMessageFromName(fromName, language, message, channel) - } -} - -// SendGroupChatMessage sends a formatted chat message to the group -func (m *Manager) SendGroupChatMessage(groupID int32, channel int16, message string) { - m.GroupChatMessageFromName(groupID, "System", 0, message, channel) -} - -// Raid functionality - -// ClearGroupRaid clears raid associations for a group -func (m *Manager) ClearGroupRaid(groupID int32) { - group := m.GetGroup(groupID) - if group != nil { - group.ClearGroupRaid() - } -} - -// RemoveGroupFromRaid removes a group from a raid -func (m *Manager) RemoveGroupFromRaid(groupID, targetGroupID int32) { - group := m.GetGroup(groupID) - if group != nil { - group.RemoveGroupFromRaid(targetGroupID) - } -} - -// IsInRaidGroup checks if two groups are in the same raid -func (m *Manager) IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool { - group := m.GetGroup(groupID) - if group == nil { - return false - } - return group.IsInRaidGroup(targetGroupID, isLeaderGroup) -} - -// GetRaidGroups returns the raid groups for a specific group -func (m *Manager) GetRaidGroups(groupID int32) []int32 { - group := m.GetGroup(groupID) - if group == nil { - return []int32{} - } - return group.GetRaidGroups() -} - -// ReplaceRaidGroups replaces the raid groups for a specific group -func (m *Manager) ReplaceRaidGroups(groupID int32, newGroups []int32) { - group := m.GetGroup(groupID) - if group != nil { - group.ReplaceRaidGroups(newGroups) - } -} - -// Group options - -// GetDefaultGroupOptions returns the default group options for a group -func (m *Manager) GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) { - group := m.GetGroup(groupID) - if group == nil { - return GroupOptions{}, false - } - return group.GetGroupOptions(), true -} - -// SetGroupOptions sets group options for a specific group -func (m *Manager) SetGroupOptions(groupID int32, options *GroupOptions) error { - group := m.GetGroup(groupID) - if group == nil { - return fmt.Errorf("group %d not found", groupID) - } - return group.SetGroupOptions(options) -} - -// Background processing loops - -// updateGroupsLoop periodically updates all groups -func (m *Manager) updateGroupsLoop() { - defer m.wg.Done() - - ticker := time.NewTicker(m.Config.UpdateInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.processGroupUpdates() - case <-m.stopChan: - return - } - } -} - -// updateBuffsLoop periodically updates group buffs -func (m *Manager) updateBuffsLoop() { - defer m.wg.Done() - - ticker := time.NewTicker(m.Config.BuffUpdateInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.updateGroupBuffs() - case <-m.stopChan: - return - } - } -} - -// cleanupExpiredInvitesLoop periodically cleans up expired invites -func (m *Manager) cleanupExpiredInvitesLoop() { - defer m.wg.Done() - - ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.cleanupExpiredInvites() - case <-m.stopChan: - return - } - } -} - -// updateStatsLoop periodically updates statistics -func (m *Manager) updateStatsLoop() { - defer m.wg.Done() - - ticker := time.NewTicker(1 * time.Minute) // Update stats every minute - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.updateStatistics() - case <-m.stopChan: - return - } - } -} - -// processGroupUpdates processes periodic group updates -func (m *Manager) processGroupUpdates() { - groups := m.GetAllGroups() - - for _, group := range groups { - if !group.IsDisbanded() { - // Update member information - members := group.GetMembers() - for _, gmi := range members { - if gmi.Member != nil { - group.UpdateGroupMemberInfo(gmi.Member, false) - } - } - } - } -} - -// updateGroupBuffs updates group buffs for all groups -func (m *Manager) updateGroupBuffs() { - // TODO: Implement group buff updates - // This would require integration with the spell/buff system -} - -// cleanupExpiredInvites removes expired invitations -func (m *Manager) cleanupExpiredInvites() { - m.invitesMutex.Lock() - defer m.invitesMutex.Unlock() - - now := time.Now() - expiredCount := 0 - - // Clean up regular invites - for key, invite := range m.PendingInvites { - if now.After(invite.ExpiresTime) { - delete(m.PendingInvites, key) - expiredCount++ - } - } - - // Clean up raid invites - for key, invite := range m.RaidPendingInvites { - if now.After(invite.ExpiresTime) { - delete(m.RaidPendingInvites, key) - expiredCount++ - } - } - - // Update statistics - if expiredCount > 0 { - m.statsMutex.Lock() - m.Stats.ExpiredInvites += int64(expiredCount) - m.statsMutex.Unlock() - } -} - -// updateStatistics updates manager statistics -func (m *Manager) updateStatistics() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - activeGroups := m.MasterList.GetActiveGroups() - raidGroups := m.MasterList.GetRaidGroups() - - var totalMembers int64 - var raidMembers int64 - - for _, group := range activeGroups { - totalMembers += int64(group.GetSize()) - if group.IsGroupRaid() { - raidMembers += int64(group.GetSize()) - } - } - - m.Stats.ActiveGroups = int64(len(activeGroups)) - m.Stats.ActiveRaids = int64(len(raidGroups)) - - if len(activeGroups) > 0 { - m.Stats.AverageGroupSize = float64(totalMembers) / float64(len(activeGroups)) - } else { - m.Stats.AverageGroupSize = 0 - } - - m.Stats.LastStatsUpdate = time.Now() -} - -// Statistics update methods - -// updateStatsForNewGroup updates statistics when a new group is created -func (m *Manager) updateStatsForNewGroup() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - m.Stats.TotalGroups++ -} - -// updateStatsForRemovedGroup updates statistics when a group is removed -func (m *Manager) updateStatsForRemovedGroup() { - // Statistics are primarily tracked in updateStatistics() -} - -// updateStatsForInvite updates statistics when an invite is sent -func (m *Manager) updateStatsForInvite() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - m.Stats.TotalInvites++ -} - -// updateStatsForAcceptedInvite updates statistics when an invite is accepted -func (m *Manager) updateStatsForAcceptedInvite() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - m.Stats.AcceptedInvites++ -} - -// updateStatsForDeclinedInvite updates statistics when an invite is declined -func (m *Manager) updateStatsForDeclinedInvite() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - m.Stats.DeclinedInvites++ -} - -// updateStatsForExpiredInvite updates statistics when an invite expires -func (m *Manager) updateStatsForExpiredInvite() { - if !m.Config.EnableStatistics { - return - } - - m.statsMutex.Lock() - defer m.statsMutex.Unlock() - - m.Stats.ExpiredInvites++ -} - -// Event system integration - -// AddEventHandler adds an event handler -func (m *Manager) AddEventHandler(handler GroupEventHandler) { - m.eventHandlersMutex.Lock() - defer m.eventHandlersMutex.Unlock() - - m.EventHandlers = append(m.EventHandlers, handler) -} - -// Integration interfaces - -// SetDatabase sets the database interface -func (m *Manager) SetDatabase(db GroupDatabase) { - m.database = db -} - -// SetPacketHandler sets the packet handler interface -func (m *Manager) SetPacketHandler(handler GroupPacketHandler) { - m.packetHandler = handler -} - -// SetValidator sets the validator interface -func (m *Manager) SetValidator(validator GroupValidator) { - m.validator = validator -} - -// SetNotifier sets the notifier interface -func (m *Manager) SetNotifier(notifier GroupNotifier) { - m.notifier = notifier -} - -// Event firing methods - -// fireGroupCreatedEvent fires a group created event -func (m *Manager) fireGroupCreatedEvent(group *Group, leader Entity) { - m.eventHandlersMutex.RLock() - defer m.eventHandlersMutex.RUnlock() - - for _, handler := range m.EventHandlers { - go handler.OnGroupCreated(group, leader) - } -} - -// fireGroupDisbandedEvent fires a group disbanded event -func (m *Manager) fireGroupDisbandedEvent(group *Group) { - m.eventHandlersMutex.RLock() - defer m.eventHandlersMutex.RUnlock() - - for _, handler := range m.EventHandlers { - go handler.OnGroupDisbanded(group) - } -} - -// fireGroupInviteSentEvent fires a group invite sent event -func (m *Manager) fireGroupInviteSentEvent(leader, member Entity) { - m.eventHandlersMutex.RLock() - defer m.eventHandlersMutex.RUnlock() - - for _, handler := range m.EventHandlers { - go handler.OnGroupInviteSent(leader, member) - } -} - -// fireGroupInviteAcceptedEvent fires a group invite accepted event -func (m *Manager) fireGroupInviteAcceptedEvent(leader, member Entity, groupID int32) { - m.eventHandlersMutex.RLock() - defer m.eventHandlersMutex.RUnlock() - - for _, handler := range m.EventHandlers { - go handler.OnGroupInviteAccepted(leader, member, groupID) - } -} - -// fireGroupInviteDeclinedEvent fires a group invite declined event -func (m *Manager) fireGroupInviteDeclinedEvent(leader, member Entity) { - m.eventHandlersMutex.RLock() - defer m.eventHandlersMutex.RUnlock() - - for _, handler := range m.EventHandlers { - go handler.OnGroupInviteDeclined(leader, member) - } -} - -// RemoveClientReference removes client references from all groups when a client disconnects -func (m *Manager) RemoveClientReference(client any) { - // Get all groups from the master list - groups := m.MasterList.GetAllGroups() - - // Remove client reference from all groups - for _, group := range groups { - group.RemoveClientReference(client) - } -} diff --git a/internal/groups/master.go b/internal/groups/master.go deleted file mode 100644 index a0b5839..0000000 --- a/internal/groups/master.go +++ /dev/null @@ -1,848 +0,0 @@ -package groups - -import ( - "fmt" - "maps" - "strings" - "sync" - "time" -) - -// MasterList manages groups with optimized lookups for: -// - Fast ID-based lookups (O(1)) -// - Fast member-based lookups (indexed) -// - Fast zone-based filtering (indexed) -// - Fast size-based filtering (indexed) -// - Raid group management and lookups -// - Leader-based searching -// - Activity tracking and cleanup -type MasterList struct { - // Core storage - groups map[int32]*Group // ID -> Group - mutex sync.RWMutex - - // Indices for O(1) lookups - byMember map[string]*Group // Member name -> group containing that member - byLeader map[string]*Group // Leader name -> group - byZone map[int32][]*Group // Zone ID -> groups with members in that zone - bySize map[int32][]*Group // Size -> groups of that size - activeGroups map[int32]*Group // Active (non-disbanded) groups - raidGroups map[int32]*Group // Groups that are part of raids - soloGroups map[int32]*Group // Single-member groups - fullGroups map[int32]*Group // Full groups (size = MAX_GROUP_SIZE) - - // Activity tracking - byLastActivity map[time.Time][]*Group // Last activity time -> groups - - // Cached metadata and slices - totalMembers int32 // Total active members across all groups - zones []int32 // Unique zones with group members - sizes []int32 // Unique group sizes - zoneStats map[int32]int // Zone ID -> group count - sizeStats map[int32]int // Size -> group count - allGroupsSlice []*Group // Cached slice of all groups - activeGroupsSlice []*Group // Cached slice of active groups - metaStale bool // Whether metadata cache needs refresh -} - -// NewMasterList creates a new group master list -func NewMasterList() *MasterList { - return &MasterList{ - groups: make(map[int32]*Group), - byMember: make(map[string]*Group), - byLeader: make(map[string]*Group), - byZone: make(map[int32][]*Group), - bySize: make(map[int32][]*Group), - activeGroups: make(map[int32]*Group), - raidGroups: make(map[int32]*Group), - soloGroups: make(map[int32]*Group), - fullGroups: make(map[int32]*Group), - byLastActivity: make(map[time.Time][]*Group), - zoneStats: make(map[int32]int), - sizeStats: make(map[int32]int), - allGroupsSlice: make([]*Group, 0), - activeGroupsSlice: make([]*Group, 0), - metaStale: true, - } -} - -// refreshMetaCache updates the cached metadata -// Note: This function assumes the caller already holds ml.mutex.Lock() -func (ml *MasterList) refreshMetaCache() { - if !ml.metaStale { - return - } - - // Clear and rebuild zone and size stats - ml.zoneStats = make(map[int32]int) - ml.sizeStats = make(map[int32]int) - zoneSet := make(map[int32]struct{}) - sizeSet := make(map[int32]struct{}) - ml.totalMembers = 0 - - // Get snapshot of active groups to avoid holding lock while calling group methods - activeGroupsSnapshot := make([]*Group, 0, len(ml.activeGroups)) - for _, group := range ml.activeGroups { - activeGroupsSnapshot = append(activeGroupsSnapshot, group) - } - - // Collect unique values and stats - for _, group := range activeGroupsSnapshot { - size := group.GetSize() - ml.sizeStats[size]++ - sizeSet[size] = struct{}{} - ml.totalMembers += size - - // Collect zones from group members - members := group.GetMembers() - zoneMap := make(map[int32]struct{}) - for _, member := range members { - if member.ZoneID > 0 { - zoneMap[member.ZoneID] = struct{}{} - } - } - for zoneID := range zoneMap { - ml.zoneStats[zoneID]++ - zoneSet[zoneID] = struct{}{} - } - } - - // Clear and rebuild cached slices - ml.zones = ml.zones[:0] - for zoneID := range zoneSet { - ml.zones = append(ml.zones, zoneID) - } - - ml.sizes = ml.sizes[:0] - for size := range sizeSet { - ml.sizes = append(ml.sizes, size) - } - - // Rebuild all groups slice - ml.allGroupsSlice = ml.allGroupsSlice[:0] - if cap(ml.allGroupsSlice) < len(ml.groups) { - ml.allGroupsSlice = make([]*Group, 0, len(ml.groups)) - } - for _, group := range ml.groups { - ml.allGroupsSlice = append(ml.allGroupsSlice, group) - } - - // Rebuild active groups slice - ml.activeGroupsSlice = ml.activeGroupsSlice[:0] - if cap(ml.activeGroupsSlice) < len(ml.activeGroups) { - ml.activeGroupsSlice = make([]*Group, 0, len(ml.activeGroups)) - } - for _, group := range ml.activeGroups { - ml.activeGroupsSlice = append(ml.activeGroupsSlice, group) - } - - ml.metaStale = false -} - -// updateGroupIndices updates all indices for a group -func (ml *MasterList) updateGroupIndices(group *Group, add bool) { - // Get all group data in one go to minimize lock contention - // This avoids holding the master list lock while calling multiple group methods - groupID := group.GetID() - - // Create a snapshot of group data to avoid repeated method calls - groupData := struct { - id int32 - size int32 - leaderName string - isRaid bool - isDisbanded bool - members []*GroupMemberInfo - }{ - id: groupID, - size: group.GetSize(), - leaderName: group.GetLeaderName(), - isRaid: group.IsGroupRaid(), - isDisbanded: group.IsDisbanded(), - members: group.GetMembers(), - } - - // Use the snapshot data for indexing - size := groupData.size - leaderName := groupData.leaderName - isRaid := groupData.isRaid - isDisbanded := groupData.isDisbanded - members := groupData.members - - if add { - // Add to size index - ml.bySize[size] = append(ml.bySize[size], group) - - // Add to leader index - if leaderName != "" { - ml.byLeader[strings.ToLower(leaderName)] = group - } - - // Add to member index - for _, member := range members { - if member.Name != "" { - ml.byMember[strings.ToLower(member.Name)] = group - } - } - - // Add to zone index - zoneMap := make(map[int32]struct{}) - for _, member := range members { - if member.ZoneID > 0 { - zoneMap[member.ZoneID] = struct{}{} - } - } - for zoneID := range zoneMap { - ml.byZone[zoneID] = append(ml.byZone[zoneID], group) - } - - // Add to specialized indices - if !isDisbanded { - ml.activeGroups[groupID] = group - } - if isRaid { - ml.raidGroups[groupID] = group - } - if size == 1 { - ml.soloGroups[groupID] = group - } - if size == MAX_GROUP_SIZE { - ml.fullGroups[groupID] = group - } - - // Add to activity index - activity := group.GetLastActivity() - ml.byLastActivity[activity] = append(ml.byLastActivity[activity], group) - } else { - // Remove from size index - sizeGroups := ml.bySize[size] - for i, g := range sizeGroups { - if g.GetID() == groupID { - ml.bySize[size] = append(sizeGroups[:i], sizeGroups[i+1:]...) - break - } - } - - // Remove from leader index - if leaderName != "" { - delete(ml.byLeader, strings.ToLower(leaderName)) - } - - // Remove from member index - for _, member := range members { - if member.Name != "" { - delete(ml.byMember, strings.ToLower(member.Name)) - } - } - - // Remove from zone index - zoneMap := make(map[int32]struct{}) - for _, member := range members { - if member.ZoneID > 0 { - zoneMap[member.ZoneID] = struct{}{} - } - } - for zoneID := range zoneMap { - zoneGroups := ml.byZone[zoneID] - for i, g := range zoneGroups { - if g.GetID() == groupID { - ml.byZone[zoneID] = append(zoneGroups[:i], zoneGroups[i+1:]...) - break - } - } - } - - // Remove from specialized indices - delete(ml.activeGroups, groupID) - delete(ml.raidGroups, groupID) - delete(ml.soloGroups, groupID) - delete(ml.fullGroups, groupID) - - // Remove from activity index - activity := group.GetLastActivity() - activityGroups := ml.byLastActivity[activity] - for i, g := range activityGroups { - if g.GetID() == groupID { - ml.byLastActivity[activity] = append(activityGroups[:i], activityGroups[i+1:]...) - break - } - } - } -} - -// AddGroup adds a group with full indexing -func (ml *MasterList) AddGroup(group *Group) bool { - if group == nil { - return false - } - - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Check if exists - if _, exists := ml.groups[group.GetID()]; exists { - return false - } - - // Add to core storage - ml.groups[group.GetID()] = group - - // Update all indices - ml.updateGroupIndices(group, true) - - // Invalidate metadata cache - ml.metaStale = true - - return true -} - -// GetGroup retrieves by ID (O(1)) -func (ml *MasterList) GetGroup(groupID int32) *Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return ml.groups[groupID] -} - -// GetGroupSafe retrieves a group by ID with existence check -func (ml *MasterList) GetGroupSafe(groupID int32) (*Group, bool) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - group, exists := ml.groups[groupID] - return group, exists -} - -// HasGroup checks if a group exists by ID -func (ml *MasterList) HasGroup(groupID int32) bool { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - _, exists := ml.groups[groupID] - return exists -} - -// RemoveGroup removes a group and updates all indices -func (ml *MasterList) RemoveGroup(groupID int32) bool { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - group, exists := ml.groups[groupID] - if !exists { - return false - } - - // Remove from core storage - delete(ml.groups, groupID) - - // Update all indices - ml.updateGroupIndices(group, false) - - // Invalidate metadata cache - ml.metaStale = true - - return true -} - -// GetAllGroups returns all groups as a slice -func (ml *MasterList) GetAllGroups() []*Group { - // Use read lock first to check if we need to refresh - ml.mutex.RLock() - needsRefresh := ml.metaStale - if !needsRefresh { - // Return cached result without upgrading to write lock - result := make([]*Group, len(ml.allGroupsSlice)) - copy(result, ml.allGroupsSlice) - ml.mutex.RUnlock() - return result - } - ml.mutex.RUnlock() - - // Need to refresh - acquire write lock - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Double-check pattern - someone else might have refreshed while we waited - if ml.metaStale { - ml.refreshMetaCache() - } - - // Return a copy to prevent external modification - result := make([]*Group, len(ml.allGroupsSlice)) - copy(result, ml.allGroupsSlice) - return result -} - -// GetAllGroupsMap returns a copy of all groups map -func (ml *MasterList) GetAllGroupsMap() map[int32]*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - // Return a copy to prevent external modification - result := make(map[int32]*Group, len(ml.groups)) - maps.Copy(result, ml.groups) - return result -} - -// GetGroupCount returns the total number of groups -func (ml *MasterList) GetGroupCount() int32 { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return int32(len(ml.groups)) -} - -// Size returns the total number of groups -func (ml *MasterList) Size() int { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return len(ml.groups) -} - -// IsEmpty returns true if the master list is empty -func (ml *MasterList) IsEmpty() bool { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return len(ml.groups) == 0 -} - -// Clear removes all groups from the list -func (ml *MasterList) Clear() { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Clear all maps - ml.groups = make(map[int32]*Group) - ml.byMember = make(map[string]*Group) - ml.byLeader = make(map[string]*Group) - ml.byZone = make(map[int32][]*Group) - ml.bySize = make(map[int32][]*Group) - ml.activeGroups = make(map[int32]*Group) - ml.raidGroups = make(map[int32]*Group) - ml.soloGroups = make(map[int32]*Group) - ml.fullGroups = make(map[int32]*Group) - ml.byLastActivity = make(map[time.Time][]*Group) - - // Clear cached metadata - ml.zones = ml.zones[:0] - ml.sizes = ml.sizes[:0] - ml.allGroupsSlice = ml.allGroupsSlice[:0] - ml.activeGroupsSlice = ml.activeGroupsSlice[:0] - ml.zoneStats = make(map[int32]int) - ml.sizeStats = make(map[int32]int) - ml.totalMembers = 0 - ml.metaStale = true -} - -// GetGroupsByFilter returns groups matching the filter function -func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - var result []*Group - for _, group := range ml.groups { - if filter(group) { - result = append(result, group) - } - } - return result -} - -// GetGroupByMember returns the group containing the specified member (O(1)) -func (ml *MasterList) GetGroupByMember(memberName string) *Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return ml.byMember[strings.ToLower(memberName)] -} - -// GetGroupByLeader returns the group led by the specified leader (O(1)) -func (ml *MasterList) GetGroupByLeader(leaderName string) *Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - return ml.byLeader[strings.ToLower(leaderName)] -} - -// GetActiveGroups returns all non-disbanded groups (O(1)) -func (ml *MasterList) GetActiveGroups() []*Group { - // Use read lock first to check if we need to refresh - ml.mutex.RLock() - needsRefresh := ml.metaStale - if !needsRefresh { - // Return cached result without upgrading to write lock - result := make([]*Group, len(ml.activeGroupsSlice)) - copy(result, ml.activeGroupsSlice) - ml.mutex.RUnlock() - return result - } - ml.mutex.RUnlock() - - // Need to refresh - acquire write lock - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Double-check pattern - someone else might have refreshed while we waited - if ml.metaStale { - ml.refreshMetaCache() - } - - // Return a copy to prevent external modification - result := make([]*Group, len(ml.activeGroupsSlice)) - copy(result, ml.activeGroupsSlice) - return result -} - -// GetGroupsByZone returns groups with members in the specified zone (O(1)) -func (ml *MasterList) GetGroupsByZone(zoneID int32) []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - groups := ml.byZone[zoneID] - if groups == nil { - return []*Group{} - } - - // Return a copy to prevent external modification - result := make([]*Group, len(groups)) - copy(result, groups) - return result -} - -// GetGroupsBySize returns groups of the specified size (O(1)) -func (ml *MasterList) GetGroupsBySize(size int32) []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - groups := ml.bySize[size] - if groups == nil { - return []*Group{} - } - - // Return a copy to prevent external modification - result := make([]*Group, len(groups)) - copy(result, groups) - return result -} - -// GetRaidGroups returns all groups that are part of raids (O(1)) -func (ml *MasterList) GetRaidGroups() []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]*Group, 0, len(ml.raidGroups)) - for _, group := range ml.raidGroups { - result = append(result, group) - } - return result -} - -// GetSoloGroups returns all groups with only one member (O(1)) -func (ml *MasterList) GetSoloGroups() []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]*Group, 0, len(ml.soloGroups)) - for _, group := range ml.soloGroups { - result = append(result, group) - } - return result -} - -// GetFullGroups returns all groups at maximum capacity (O(1)) -func (ml *MasterList) GetFullGroups() []*Group { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]*Group, 0, len(ml.fullGroups)) - for _, group := range ml.fullGroups { - result = append(result, group) - } - return result -} - -// GetGroupsByLeader returns groups led by entities with the specified name -func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group { - group := ml.GetGroupByLeader(leaderName) - if group != nil { - return []*Group{group} - } - return []*Group{} -} - -// GetGroupsByMember returns groups containing a member with the specified name -func (ml *MasterList) GetGroupsByMember(memberName string) []*Group { - group := ml.GetGroupByMember(memberName) - if group != nil { - return []*Group{group} - } - return []*Group{} -} - -// GetZones returns all unique zones with group members using cached results -func (ml *MasterList) GetZones() []int32 { - ml.mutex.Lock() // Need write lock to potentially update cache - defer ml.mutex.Unlock() - - ml.refreshMetaCache() - - // Return a copy to prevent external modification - result := make([]int32, len(ml.zones)) - copy(result, ml.zones) - return result -} - -// GetSizes returns all unique group sizes using cached results -func (ml *MasterList) GetSizes() []int32 { - ml.mutex.Lock() // Need write lock to potentially update cache - defer ml.mutex.Unlock() - - ml.refreshMetaCache() - - // Return a copy to prevent external modification - result := make([]int32, len(ml.sizes)) - copy(result, ml.sizes) - return result -} - -// GetTotalMembers returns the total number of active members across all groups -func (ml *MasterList) GetTotalMembers() int32 { - ml.mutex.Lock() // Need write lock to potentially update cache - defer ml.mutex.Unlock() - - ml.refreshMetaCache() - return ml.totalMembers -} - -// GetGroupStatistics returns statistics about the groups in the master list using cached data -func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats { - // Use read lock first to check if we need to refresh - ml.mutex.RLock() - needsRefresh := ml.metaStale - if !needsRefresh { - // Calculate stats from cached data - var totalRaidMembers int32 - for _, group := range ml.raidGroups { - totalRaidMembers += group.GetSize() - } - - var averageGroupSize float64 - if len(ml.activeGroups) > 0 { - averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups)) - } - - stats := &GroupMasterListStats{ - TotalGroups: int32(len(ml.groups)), - ActiveGroups: int32(len(ml.activeGroups)), - RaidGroups: int32(len(ml.raidGroups)), - TotalMembers: ml.totalMembers, - TotalRaidMembers: totalRaidMembers, - AverageGroupSize: averageGroupSize, - SoloGroups: int32(len(ml.soloGroups)), - FullGroups: int32(len(ml.fullGroups)), - } - ml.mutex.RUnlock() - return stats - } - ml.mutex.RUnlock() - - // Need to refresh - acquire write lock - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Double-check pattern - if ml.metaStale { - ml.refreshMetaCache() - } - - var totalRaidMembers int32 - for _, group := range ml.raidGroups { - totalRaidMembers += group.GetSize() - } - - var averageGroupSize float64 - if len(ml.activeGroups) > 0 { - averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups)) - } - - return &GroupMasterListStats{ - TotalGroups: int32(len(ml.groups)), - ActiveGroups: int32(len(ml.activeGroups)), - RaidGroups: int32(len(ml.raidGroups)), - TotalMembers: ml.totalMembers, - TotalRaidMembers: totalRaidMembers, - AverageGroupSize: averageGroupSize, - SoloGroups: int32(len(ml.soloGroups)), - FullGroups: int32(len(ml.fullGroups)), - } -} - -// GroupMasterListStats holds statistics about the groups master list -type GroupMasterListStats struct { - TotalGroups int32 `json:"total_groups"` - ActiveGroups int32 `json:"active_groups"` - RaidGroups int32 `json:"raid_groups"` - TotalMembers int32 `json:"total_members"` - TotalRaidMembers int32 `json:"total_raid_members"` - AverageGroupSize float64 `json:"average_group_size"` - SoloGroups int32 `json:"solo_groups"` - FullGroups int32 `json:"full_groups"` -} - -// RefreshGroupIndices refreshes indices for a group (used when group state changes) -func (ml *MasterList) RefreshGroupIndices(group *Group) { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Remove from old indices - ml.updateGroupIndices(group, false) - // Add to new indices - ml.updateGroupIndices(group, true) - - // Invalidate metadata cache - ml.metaStale = true -} - -// UpdateGroup updates an existing group and refreshes indices -func (ml *MasterList) UpdateGroup(group *Group) error { - if group == nil { - return fmt.Errorf("group cannot be nil") - } - - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Check if exists - old, exists := ml.groups[group.GetID()] - if !exists { - return fmt.Errorf("group %d not found", group.GetID()) - } - - // Remove old group from indices (but not core storage yet) - ml.updateGroupIndices(old, false) - - // Update core storage - ml.groups[group.GetID()] = group - - // Add new group to indices - ml.updateGroupIndices(group, true) - - // Invalidate metadata cache - ml.metaStale = true - - return nil -} - -// ForEach executes a function for each group -func (ml *MasterList) ForEach(fn func(int32, *Group)) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - for id, group := range ml.groups { - fn(id, group) - } -} - -// Cleanup removes disbanded groups from the master list -func (ml *MasterList) Cleanup() int32 { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - removed := int32(0) - for id, group := range ml.groups { - if group.IsDisbanded() { - // Remove from core storage - delete(ml.groups, id) - // Remove from indices (group is already disbanded, so activeGroups won't include it) - ml.updateGroupIndices(group, false) - removed++ - } - } - - if removed > 0 { - // Invalidate metadata cache - ml.metaStale = true - } - - return removed -} - -// ValidateAll validates all groups in the master list -func (ml *MasterList) ValidateAll() []error { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - var errors []error - - for id, group := range ml.groups { - if group == nil { - errors = append(errors, fmt.Errorf("group ID %d is nil", id)) - continue - } - - // Check for basic validity - if group.GetID() != id { - errors = append(errors, fmt.Errorf("group ID mismatch: map key %d != group ID %d", id, group.GetID())) - } - - if group.GetID() <= 0 { - errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID())) - } - - if group.GetSize() == 0 && !group.IsDisbanded() { - errors = append(errors, fmt.Errorf("group %d is empty but not disbanded", group.GetID())) - } - - if group.GetSize() > MAX_GROUP_SIZE { - errors = append(errors, fmt.Errorf("group %d exceeds maximum size (%d > %d)", - group.GetID(), group.GetSize(), MAX_GROUP_SIZE)) - } - - // Check for leader - members := group.GetMembers() - hasLeader := false - leaderCount := 0 - - for _, member := range members { - if member.Leader { - hasLeader = true - leaderCount++ - } - } - - if !hasLeader && !group.IsDisbanded() { - errors = append(errors, fmt.Errorf("group %d has no leader", group.GetID())) - } - - if leaderCount > 1 { - errors = append(errors, fmt.Errorf("group %d has multiple leaders (%d)", group.GetID(), leaderCount)) - } - - // Validate index consistency - if !group.IsDisbanded() { - if _, exists := ml.activeGroups[id]; !exists { - errors = append(errors, fmt.Errorf("active group %d not found in activeGroups index", id)) - } - } - - if group.IsGroupRaid() { - if _, exists := ml.raidGroups[id]; !exists { - errors = append(errors, fmt.Errorf("raid group %d not found in raidGroups index", id)) - } - } - - if group.GetSize() == 1 { - if _, exists := ml.soloGroups[id]; !exists { - errors = append(errors, fmt.Errorf("solo group %d not found in soloGroups index", id)) - } - } - - if group.GetSize() == MAX_GROUP_SIZE { - if _, exists := ml.fullGroups[id]; !exists { - errors = append(errors, fmt.Errorf("full group %d not found in fullGroups index", id)) - } - } - } - - return errors -} - -// IsValid returns true if all groups are valid -func (ml *MasterList) IsValid() bool { - errors := ml.ValidateAll() - return len(errors) == 0 -} diff --git a/internal/groups/types.go b/internal/groups/types.go deleted file mode 100644 index f014822..0000000 --- a/internal/groups/types.go +++ /dev/null @@ -1,257 +0,0 @@ -package groups - -import ( - "sync" - "time" -) - -// GroupOptions holds group configuration settings -type GroupOptions struct { - LootMethod int8 `json:"loot_method" db:"loot_method"` - LootItemsRarity int8 `json:"loot_items_rarity" db:"loot_items_rarity"` - AutoSplit int8 `json:"auto_split" db:"auto_split"` - DefaultYell int8 `json:"default_yell" db:"default_yell"` - GroupLockMethod int8 `json:"group_lock_method" db:"group_lock_method"` - GroupAutolock int8 `json:"group_autolock" db:"group_autolock"` - SoloAutolock int8 `json:"solo_autolock" db:"solo_autolock"` - AutoLootMethod int8 `json:"auto_loot_method" db:"auto_loot_method"` - LastLootedIndex int8 `json:"last_looted_index" db:"last_looted_index"` -} - -// GroupMemberInfo contains all information about a group member -type GroupMemberInfo struct { - // Group and member identification - GroupID int32 `json:"group_id" db:"group_id"` - Name string `json:"name" db:"name"` - Zone string `json:"zone" db:"zone"` - - // Health and power stats - HPCurrent int32 `json:"hp_current" db:"hp_current"` - HPMax int32 `json:"hp_max" db:"hp_max"` - PowerCurrent int32 `json:"power_current" db:"power_current"` - PowerMax int32 `json:"power_max" db:"power_max"` - - // Level and character info - LevelCurrent int16 `json:"level_current" db:"level_current"` - LevelMax int16 `json:"level_max" db:"level_max"` - RaceID int8 `json:"race_id" db:"race_id"` - ClassID int8 `json:"class_id" db:"class_id"` - - // Group status - Leader bool `json:"leader" db:"leader"` - IsClient bool `json:"is_client" db:"is_client"` - IsRaidLooter bool `json:"is_raid_looter" db:"is_raid_looter"` - - // Zone and instance info - ZoneID int32 `json:"zone_id" db:"zone_id"` - InstanceID int32 `json:"instance_id" db:"instance_id"` - - // Mentoring - MentorTargetCharID int32 `json:"mentor_target_char_id" db:"mentor_target_char_id"` - - // Network info for cross-server groups - ClientPeerAddress string `json:"client_peer_address" db:"client_peer_address"` - ClientPeerPort int16 `json:"client_peer_port" db:"client_peer_port"` - - // Entity reference (local members only) - Member Entity `json:"-"` - - // Client reference (players only) - interface to avoid circular deps - Client any `json:"-"` - - // Timestamps - JoinTime time.Time `json:"join_time" db:"join_time"` - LastUpdate time.Time `json:"last_update" db:"last_update"` -} - -// Group is now defined in group.go - this type definition removed to avoid duplication - -// GroupMessage represents a message sent to the group -type GroupMessage struct { - Type int8 `json:"type"` - Channel int16 `json:"channel"` - Message string `json:"message"` - FromName string `json:"from_name"` - Language int32 `json:"language"` - Timestamp time.Time `json:"timestamp"` - ExcludeClient any `json:"-"` -} - -// GroupUpdate represents a group update event -type GroupUpdate struct { - Type int8 `json:"type"` - GroupID int32 `json:"group_id"` - MemberInfo *GroupMemberInfo `json:"member_info,omitempty"` - Options *GroupOptions `json:"options,omitempty"` - RaidGroups []int32 `json:"raid_groups,omitempty"` - ForceRaidUpdate bool `json:"force_raid_update"` - ExcludeClient any `json:"-"` - Timestamp time.Time `json:"timestamp"` -} - -// GroupInvite represents a pending group invitation -type GroupInvite struct { - InviterName string `json:"inviter_name"` - InviteeName string `json:"invitee_name"` - GroupID int32 `json:"group_id"` - IsRaidInvite bool `json:"is_raid_invite"` - CreatedTime time.Time `json:"created_time"` - ExpiresTime time.Time `json:"expires_time"` -} - -// GroupManager manages all player groups -type GroupManager struct { - // Group storage - groups map[int32]*Group - groupsMutex sync.RWMutex - - // Group ID generation - nextGroupID int32 - nextGroupIDMutex sync.Mutex - - // Pending invitations - pendingInvites map[string]*GroupInvite - raidPendingInvites map[string]*GroupInvite - invitesMutex sync.RWMutex - - // Event handlers - eventHandlers []GroupEventHandler - eventHandlersMutex sync.RWMutex - - // Configuration - config GroupManagerConfig - - // Statistics - stats GroupManagerStats - statsMutex sync.RWMutex - - // Background processing - stopChan chan struct{} - wg sync.WaitGroup - - // Integration interfaces - database GroupDatabase - packetHandler GroupPacketHandler - validator GroupValidator - notifier GroupNotifier -} - -// GroupManagerConfig holds configuration for the group manager -type GroupManagerConfig struct { - MaxGroups int32 `json:"max_groups"` - MaxRaidGroups int32 `json:"max_raid_groups"` - InviteTimeout time.Duration `json:"invite_timeout"` - UpdateInterval time.Duration `json:"update_interval"` - BuffUpdateInterval time.Duration `json:"buff_update_interval"` - EnableCrossServer bool `json:"enable_cross_server"` - EnableRaids bool `json:"enable_raids"` - EnableQuestSharing bool `json:"enable_quest_sharing"` - EnableAutoInvite bool `json:"enable_auto_invite"` - EnableStatistics bool `json:"enable_statistics"` -} - -// GroupManagerStats holds statistics about group management -type GroupManagerStats struct { - TotalGroups int64 `json:"total_groups"` - ActiveGroups int64 `json:"active_groups"` - TotalRaids int64 `json:"total_raids"` - ActiveRaids int64 `json:"active_raids"` - TotalInvites int64 `json:"total_invites"` - AcceptedInvites int64 `json:"accepted_invites"` - DeclinedInvites int64 `json:"declined_invites"` - ExpiredInvites int64 `json:"expired_invites"` - AverageGroupSize float64 `json:"average_group_size"` - AverageGroupDuration time.Duration `json:"average_group_duration"` - LastStatsUpdate time.Time `json:"last_stats_update"` -} - -// Default group options -func DefaultGroupOptions() GroupOptions { - return GroupOptions{ - LootMethod: LOOT_METHOD_ROUND_ROBIN, - LootItemsRarity: LOOT_RARITY_COMMON, - AutoSplit: AUTO_SPLIT_DISABLED, - DefaultYell: DEFAULT_YELL_DISABLED, - GroupLockMethod: LOCK_METHOD_OPEN, - GroupAutolock: AUTO_LOCK_DISABLED, - SoloAutolock: AUTO_LOCK_DISABLED, - AutoLootMethod: AUTO_LOOT_DISABLED, - LastLootedIndex: 0, - } -} - -// Copy creates a copy of GroupMemberInfo -func (gmi *GroupMemberInfo) Copy() *GroupMemberInfo { - copy := *gmi - return © -} - -// IsValid checks if the group member info is valid -func (gmi *GroupMemberInfo) IsValid() bool { - return gmi.GroupID > 0 && len(gmi.Name) > 0 -} - -// UpdateStats updates member stats from entity -func (gmi *GroupMemberInfo) UpdateStats() { - if gmi.Member == nil { - return - } - - entity := gmi.Member - gmi.HPCurrent = entity.GetHP() - gmi.HPMax = entity.GetTotalHP() - gmi.PowerCurrent = entity.GetPower() - gmi.PowerMax = entity.GetTotalPower() - gmi.LevelCurrent = int16(entity.GetLevel()) - gmi.LevelMax = int16(entity.GetLevel()) // TODO: Get actual max level - gmi.LastUpdate = time.Now() - - // Update zone info if entity has zone - if zone := entity.GetZone(); zone != nil { - gmi.ZoneID = zone.GetZoneID() - gmi.InstanceID = zone.GetInstanceID() - gmi.Zone = zone.GetZoneName() - } -} - -// Copy creates a copy of GroupOptions -func (go_opts *GroupOptions) Copy() GroupOptions { - return *go_opts -} - -// IsValid checks if group options are valid -func (go_opts *GroupOptions) IsValid() bool { - return go_opts.LootMethod >= LOOT_METHOD_LEADER_ONLY && go_opts.LootMethod <= LOOT_METHOD_LOTTO && - go_opts.LootItemsRarity >= LOOT_RARITY_COMMON && go_opts.LootItemsRarity <= LOOT_RARITY_FABLED -} - -// NewGroupMessage creates a new group message -func NewGroupMessage(msgType int8, channel int16, message, fromName string, language int32) *GroupMessage { - return &GroupMessage{ - Type: msgType, - Channel: channel, - Message: message, - FromName: fromName, - Language: language, - Timestamp: time.Now(), - } -} - -// NewGroupUpdate creates a new group update -func NewGroupUpdate(updateType int8, groupID int32) *GroupUpdate { - return &GroupUpdate{ - Type: updateType, - GroupID: groupID, - Timestamp: time.Now(), - } -} - -// IsExpired checks if the group invite has expired -func (gi *GroupInvite) IsExpired() bool { - return time.Now().After(gi.ExpiresTime) -} - -// TimeRemaining returns the remaining time for the invite -func (gi *GroupInvite) TimeRemaining() time.Duration { - return time.Until(gi.ExpiresTime) -} diff --git a/internal/packets/opcodes.go b/internal/packets/opcodes.go index 0d1d791..71eb79f 100644 --- a/internal/packets/opcodes.go +++ b/internal/packets/opcodes.go @@ -136,6 +136,17 @@ const ( OP_EntityVerbsReplyMsg OP_EntityVerbsVerbMsg + // Group system opcodes + OP_GroupUpdateMsg + OP_RaidUpdateMsg + OP_GroupInviteMsg + OP_GroupDeclineMsg + OP_GroupChatMsg + OP_GroupMessageMsg + OP_GroupOptionsMsg + OP_DefaultGroupOptionsMsg + OP_DefaultGroupOptionsRequestMsg + // Add more opcodes as needed... _maxInternalOpcode // Sentinel value ) @@ -231,6 +242,17 @@ var OpcodeNames = map[InternalOpcode]string{ OP_EntityVerbsRequestMsg: "OP_EntityVerbsRequestMsg", OP_EntityVerbsReplyMsg: "OP_EntityVerbsReplyMsg", OP_EntityVerbsVerbMsg: "OP_EntityVerbsVerbMsg", + + // Group system opcodes + OP_GroupUpdateMsg: "OP_GroupUpdateMsg", + OP_RaidUpdateMsg: "OP_RaidUpdateMsg", + OP_GroupInviteMsg: "OP_GroupInviteMsg", + OP_GroupDeclineMsg: "OP_GroupDeclineMsg", + OP_GroupChatMsg: "OP_GroupChatMsg", + OP_GroupMessageMsg: "OP_GroupMessageMsg", + OP_GroupOptionsMsg: "OP_GroupOptionsMsg", + OP_DefaultGroupOptionsMsg: "OP_DefaultGroupOptionsMsg", + OP_DefaultGroupOptionsRequestMsg: "OP_DefaultGroupOptionsRequestMsg", } // OpcodeManager handles the mapping between client-specific opcodes and internal opcodes