1633 lines
42 KiB
Go
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 ©
|
|
}
|
|
|
|
// 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
|
|
} |