eq2go/internal/groups/groups.go
2025-08-29 14:02:21 -05:00

1633 lines
42 KiB
Go

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 &copy
}
// 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
}