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 }