package groups import ( "fmt" "time" "eq2emu/internal/entity" ) // NewGroupManager creates a new group manager with the given configuration func NewGroupManager(config GroupManagerConfig) *GroupManager { manager := &GroupManager{ groups: make(map[int32]*Group), nextGroupID: 1, pendingInvites: make(map[string]*GroupInvite), raidPendingInvites: make(map[string]*GroupInvite), eventHandlers: make([]GroupEventHandler, 0), config: config, stopChan: make(chan struct{}), } return manager } // Start starts the group manager background processes func (gm *GroupManager) Start() error { // Start background processes if gm.config.UpdateInterval > 0 { gm.wg.Add(1) go gm.updateGroupsLoop() } if gm.config.BuffUpdateInterval > 0 { gm.wg.Add(1) go gm.updateBuffsLoop() } gm.wg.Add(1) go gm.cleanupExpiredInvitesLoop() if gm.config.EnableStatistics { gm.wg.Add(1) go gm.updateStatsLoop() } return nil } // Stop stops the group manager and all background processes func (gm *GroupManager) Stop() error { close(gm.stopChan) gm.wg.Wait() return nil } // NewGroup creates a new group with the given leader and options func (gm *GroupManager) NewGroup(leader entity.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 = gm.generateNextGroupID() } // Check if group ID already exists gm.groupsMutex.RLock() if _, exists := gm.groups[groupID]; exists && overrideGroupID == 0 { gm.groupsMutex.RUnlock() return 0, fmt.Errorf("group ID %d already exists", groupID) } gm.groupsMutex.RUnlock() // Create new group group := NewGroup(groupID, options) // 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 gm.groupsMutex.Lock() gm.groups[groupID] = group gm.groupsMutex.Unlock() // Update statistics gm.updateStatsForNewGroup() // Fire event gm.fireGroupCreatedEvent(group, leader) return groupID, nil } // RemoveGroup removes a group from the manager func (gm *GroupManager) RemoveGroup(groupID int32) error { gm.groupsMutex.Lock() group, exists := gm.groups[groupID] if !exists { gm.groupsMutex.Unlock() return fmt.Errorf("group %d not found", groupID) } delete(gm.groups, groupID) gm.groupsMutex.Unlock() // Disband the group group.Disband() // Update statistics gm.updateStatsForRemovedGroup() // Fire event gm.fireGroupDisbandedEvent(group) return nil } // GetGroup returns 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 is valid and exists func (gm *GroupManager) IsGroupIDValid(groupID int32) bool { gm.groupsMutex.RLock() defer gm.groupsMutex.RUnlock() _, exists := gm.groups[groupID] return exists } // AddGroupMember adds a member to an existing group func (gm *GroupManager) AddGroupMember(groupID int32, member entity.Entity, isLeader bool) error { group := gm.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 (gm *GroupManager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error { group := gm.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 (gm *GroupManager) RemoveGroupMember(groupID int32, member entity.Entity) error { group := gm.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 { gm.RemoveGroup(groupID) } return nil } // RemoveGroupMemberByName removes a member by name from a group func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error { group := gm.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 { gm.RemoveGroup(groupID) } return nil } // SendGroupUpdate sends an update to all members of a group func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { group := gm.GetGroup(groupID) if group != nil { group.SendGroupUpdate(excludeClient, forceRaidUpdate) } } // Group invitation handling // Invite handles inviting a player to a group func (gm *GroupManager) Invite(leader entity.Entity, member entity.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 gm.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 !gm.addInvite(leader, member) { return GROUP_INVITE_PERMISSION_DENIED } // Fire event gm.fireGroupInviteSentEvent(leader, member) return GROUP_INVITE_SUCCESS } // AddInvite adds a group invitation func (gm *GroupManager) AddInvite(leader entity.Entity, member entity.Entity) bool { return gm.addInvite(leader, member) } // addInvite internal method to add an invitation func (gm *GroupManager) addInvite(leader entity.Entity, member entity.Entity) bool { 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(gm.config.InviteTimeout), } gm.invitesMutex.Lock() gm.pendingInvites[inviteKey] = invite gm.invitesMutex.Unlock() // Update statistics gm.updateStatsForInvite() return true } // AcceptInvite handles accepting of a group invite func (gm *GroupManager) AcceptInvite(member entity.Entity, groupOverrideID *int32, autoAddGroup bool) int8 { if member == nil { return GROUP_INVITE_TARGET_NOT_FOUND } inviteKey := member.GetName() gm.invitesMutex.Lock() invite, exists := gm.pendingInvites[inviteKey] if !exists { gm.invitesMutex.Unlock() return GROUP_INVITE_TARGET_NOT_FOUND } // Check if invite has expired if invite.IsExpired() { delete(gm.pendingInvites, inviteKey) gm.invitesMutex.Unlock() gm.updateStatsForExpiredInvite() return GROUP_INVITE_DECLINED } // Remove the invite delete(gm.pendingInvites, inviteKey) gm.invitesMutex.Unlock() if !autoAddGroup { return GROUP_INVITE_SUCCESS } // Find the leader var leader entity.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 = gm.NewGroup(leader, nil, groupID) } else { groupID, err = gm.NewGroup(leader, nil, 0) } if err != nil { return GROUP_INVITE_PERMISSION_DENIED } } else { groupID = leaderGroupID } // Add member to the group if err := gm.AddGroupMember(groupID, member, false); err != nil { return GROUP_INVITE_GROUP_FULL } // Update statistics gm.updateStatsForAcceptedInvite() // Fire event gm.fireGroupInviteAcceptedEvent(leader, member, groupID) return GROUP_INVITE_SUCCESS } // DeclineInvite handles declining of a group invite func (gm *GroupManager) DeclineInvite(member entity.Entity) { if member == nil { return } inviteKey := member.GetName() gm.invitesMutex.Lock() invite, exists := gm.pendingInvites[inviteKey] if exists { delete(gm.pendingInvites, inviteKey) } gm.invitesMutex.Unlock() if exists { // Update statistics gm.updateStatsForDeclinedInvite() // Fire event var leader entity.Entity // TODO: Find leader entity by name // leader = world.GetPlayerByName(invite.InviterName) gm.fireGroupInviteDeclinedEvent(leader, member) } } // ClearPendingInvite clears a pending invite for a member func (gm *GroupManager) ClearPendingInvite(member entity.Entity) { if member == nil { return } inviteKey := member.GetName() gm.invitesMutex.Lock() delete(gm.pendingInvites, inviteKey) gm.invitesMutex.Unlock() } // HasPendingInvite checks if a member has a pending invite and returns the inviter name func (gm *GroupManager) HasPendingInvite(member entity.Entity) string { if member == nil { return "" } inviteKey := member.GetName() return gm.hasPendingInvite(inviteKey) } // hasPendingInvite internal method to check for pending invites func (gm *GroupManager) hasPendingInvite(inviteKey string) string { gm.invitesMutex.RLock() defer gm.invitesMutex.RUnlock() if invite, exists := gm.pendingInvites[inviteKey]; exists { if !invite.IsExpired() { return invite.InviterName } } return "" } // Group utility methods // GetGroupSize returns the size of a group func (gm *GroupManager) GetGroupSize(groupID int32) int32 { group := gm.GetGroup(groupID) if group == nil { return 0 } return group.GetSize() } // IsInGroup checks if an entity is in a specific group func (gm *GroupManager) IsInGroup(groupID int32, member entity.Entity) bool { group := gm.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 (gm *GroupManager) IsPlayerInGroup(groupID int32, charID int32) entity.Entity { group := gm.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 (gm *GroupManager) IsSpawnInGroup(groupID int32, name string) bool { group := gm.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 (gm *GroupManager) GetGroupLeader(groupID int32) entity.Entity { group := gm.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 (gm *GroupManager) MakeLeader(groupID int32, newLeader entity.Entity) bool { group := gm.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 (gm *GroupManager) SimpleGroupMessage(groupID int32, message string) { group := gm.GetGroup(groupID) if group != nil { group.SimpleGroupMessage(message) } } // SendGroupMessage sends a formatted message to all members of a group func (gm *GroupManager) SendGroupMessage(groupID int32, msgType int8, message string) { group := gm.GetGroup(groupID) if group != nil { group.SendGroupMessage(msgType, message) } } // GroupMessage sends a message to all members of a group (alias for SimpleGroupMessage) func (gm *GroupManager) GroupMessage(groupID int32, message string) { gm.SimpleGroupMessage(groupID, message) } // GroupChatMessage sends a chat message from a member to the group func (gm *GroupManager) GroupChatMessage(groupID int32, from entity.Entity, language int32, message string, channel int16) { group := gm.GetGroup(groupID) if group != nil { group.GroupChatMessage(from, language, message, channel) } } // GroupChatMessageFromName sends a chat message from a named sender to the group 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 formatted chat message to the group func (gm *GroupManager) SendGroupChatMessage(groupID int32, channel int16, message string) { gm.GroupChatMessageFromName(groupID, "System", 0, message, channel) } // Raid functionality // ClearGroupRaid clears 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 two groups are in the same raid 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 specific 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 specific group func (gm *GroupManager) ReplaceRaidGroups(groupID int32, newGroups []int32) { group := gm.GetGroup(groupID) if group != nil { group.ReplaceRaidGroups(newGroups) } } // Group options // GetDefaultGroupOptions returns the default group options for a group 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 for a specific group func (gm *GroupManager) SetGroupOptions(groupID int32, options *GroupOptions) error { group := gm.GetGroup(groupID) if group == nil { return fmt.Errorf("group %d not found", groupID) } return group.SetGroupOptions(options) } // Utility methods // generateNextGroupID generates the next available group ID func (gm *GroupManager) generateNextGroupID() int32 { gm.nextGroupIDMutex.Lock() defer gm.nextGroupIDMutex.Unlock() id := gm.nextGroupID gm.nextGroupID++ // Handle overflow if gm.nextGroupID <= 0 { gm.nextGroupID = 1 } return id } // GetGroupCount returns the number of active groups func (gm *GroupManager) GetGroupCount() int32 { gm.groupsMutex.RLock() defer gm.groupsMutex.RUnlock() return int32(len(gm.groups)) } // GetAllGroups returns all active 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 } // Background processing loops // updateGroupsLoop periodically updates all groups func (gm *GroupManager) updateGroupsLoop() { defer gm.wg.Done() ticker := time.NewTicker(gm.config.UpdateInterval) defer ticker.Stop() for { select { case <-ticker.C: gm.processGroupUpdates() case <-gm.stopChan: return } } } // updateBuffsLoop periodically updates group buffs func (gm *GroupManager) updateBuffsLoop() { defer gm.wg.Done() ticker := time.NewTicker(gm.config.BuffUpdateInterval) defer ticker.Stop() for { select { case <-ticker.C: gm.updateGroupBuffs() case <-gm.stopChan: return } } } // cleanupExpiredInvitesLoop periodically cleans up expired invites func (gm *GroupManager) cleanupExpiredInvitesLoop() { defer gm.wg.Done() ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds defer ticker.Stop() for { select { case <-ticker.C: gm.cleanupExpiredInvites() case <-gm.stopChan: return } } } // updateStatsLoop periodically updates statistics func (gm *GroupManager) updateStatsLoop() { defer gm.wg.Done() ticker := time.NewTicker(1 * time.Minute) // Update stats every minute defer ticker.Stop() for { select { case <-ticker.C: gm.updateStatistics() case <-gm.stopChan: return } } } // processGroupUpdates processes periodic group updates func (gm *GroupManager) processGroupUpdates() { groups := gm.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 (gm *GroupManager) updateGroupBuffs() { // TODO: Implement group buff updates // This would require integration with the spell/buff system } // cleanupExpiredInvites removes expired invitations func (gm *GroupManager) cleanupExpiredInvites() { gm.invitesMutex.Lock() defer gm.invitesMutex.Unlock() now := time.Now() expiredCount := 0 // Clean up regular invites for key, invite := range gm.pendingInvites { if now.After(invite.ExpiresTime) { delete(gm.pendingInvites, key) expiredCount++ } } // Clean up raid invites for key, invite := range gm.raidPendingInvites { if now.After(invite.ExpiresTime) { delete(gm.raidPendingInvites, key) expiredCount++ } } // Update statistics if expiredCount > 0 { gm.statsMutex.Lock() gm.stats.ExpiredInvites += int64(expiredCount) gm.statsMutex.Unlock() } } // updateStatistics updates manager statistics func (gm *GroupManager) updateStatistics() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.groupsMutex.RLock() activeGroups := int64(len(gm.groups)) var totalMembers int64 var raidCount int64 for _, group := range gm.groups { totalMembers += int64(group.GetSize()) if group.IsGroupRaid() { raidCount++ } } gm.groupsMutex.RUnlock() gm.stats.ActiveGroups = activeGroups gm.stats.ActiveRaids = raidCount if activeGroups > 0 { gm.stats.AverageGroupSize = float64(totalMembers) / float64(activeGroups) } else { gm.stats.AverageGroupSize = 0 } gm.stats.LastStatsUpdate = time.Now() } // Statistics update methods // updateStatsForNewGroup updates statistics when a new group is created func (gm *GroupManager) updateStatsForNewGroup() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.stats.TotalGroups++ } // updateStatsForRemovedGroup updates statistics when a group is removed func (gm *GroupManager) updateStatsForRemovedGroup() { // Statistics are primarily tracked in updateStatistics() } // updateStatsForInvite updates statistics when an invite is sent func (gm *GroupManager) updateStatsForInvite() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.stats.TotalInvites++ } // updateStatsForAcceptedInvite updates statistics when an invite is accepted func (gm *GroupManager) updateStatsForAcceptedInvite() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.stats.AcceptedInvites++ } // updateStatsForDeclinedInvite updates statistics when an invite is declined func (gm *GroupManager) updateStatsForDeclinedInvite() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.stats.DeclinedInvites++ } // updateStatsForExpiredInvite updates statistics when an invite expires func (gm *GroupManager) updateStatsForExpiredInvite() { if !gm.config.EnableStatistics { return } gm.statsMutex.Lock() defer gm.statsMutex.Unlock() gm.stats.ExpiredInvites++ } // GetStats returns current manager statistics func (gm *GroupManager) GetStats() GroupManagerStats { gm.statsMutex.RLock() defer gm.statsMutex.RUnlock() return gm.stats } // Event system integration // AddEventHandler adds an event handler func (gm *GroupManager) AddEventHandler(handler GroupEventHandler) { gm.eventHandlersMutex.Lock() defer gm.eventHandlersMutex.Unlock() gm.eventHandlers = append(gm.eventHandlers, handler) } // Integration interfaces // SetDatabase sets the database interface func (gm *GroupManager) SetDatabase(db GroupDatabase) { gm.database = db } // SetPacketHandler sets the packet handler interface func (gm *GroupManager) SetPacketHandler(handler GroupPacketHandler) { gm.packetHandler = handler } // SetValidator sets the validator interface func (gm *GroupManager) SetValidator(validator GroupValidator) { gm.validator = validator } // SetNotifier sets the notifier interface func (gm *GroupManager) SetNotifier(notifier GroupNotifier) { gm.notifier = notifier } // Event firing methods // fireGroupCreatedEvent fires a group created event func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader entity.Entity) { gm.eventHandlersMutex.RLock() defer gm.eventHandlersMutex.RUnlock() for _, handler := range gm.eventHandlers { go handler.OnGroupCreated(group, leader) } } // fireGroupDisbandedEvent fires a group disbanded event func (gm *GroupManager) fireGroupDisbandedEvent(group *Group) { gm.eventHandlersMutex.RLock() defer gm.eventHandlersMutex.RUnlock() for _, handler := range gm.eventHandlers { go handler.OnGroupDisbanded(group) } } // fireGroupInviteSentEvent fires a group invite sent event func (gm *GroupManager) fireGroupInviteSentEvent(leader, member entity.Entity) { gm.eventHandlersMutex.RLock() defer gm.eventHandlersMutex.RUnlock() for _, handler := range gm.eventHandlers { go handler.OnGroupInviteSent(leader, member) } } // fireGroupInviteAcceptedEvent fires a group invite accepted event func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member entity.Entity, groupID int32) { gm.eventHandlersMutex.RLock() defer gm.eventHandlersMutex.RUnlock() for _, handler := range gm.eventHandlers { go handler.OnGroupInviteAccepted(leader, member, groupID) } } // fireGroupInviteDeclinedEvent fires a group invite declined event func (gm *GroupManager) fireGroupInviteDeclinedEvent(leader, member entity.Entity) { gm.eventHandlersMutex.RLock() defer gm.eventHandlersMutex.RUnlock() for _, handler := range gm.eventHandlers { go handler.OnGroupInviteDeclined(leader, member) } }