simplify groups

This commit is contained in:
Sky Johnson 2025-08-29 14:02:21 -05:00
parent 88fd7bed4d
commit 575f94f3c1
9 changed files with 1655 additions and 3475 deletions

View File

@ -1,34 +0,0 @@
package groups
// Entity represents a game entity that can be part of a group
type Entity interface {
// Basic entity information
GetID() int32
GetName() string
GetLevel() int8
GetClass() int8
GetRace() int8
// Health and power
GetHP() int32
GetTotalHP() int32
GetPower() int32
GetTotalPower() int32
// Entity types
IsPlayer() bool
IsBot() bool
IsNPC() bool
IsDead() bool
// World positioning
GetZone() Zone
GetDistance(other Entity) float32
}
// Zone represents a game zone
type Zone interface {
GetZoneID() int32
GetInstanceID() int32
GetZoneName() string
}

View File

@ -1,797 +0,0 @@
package groups
import (
"fmt"
"sync"
"time"
)
// Group represents a player group with embedded database operations
type Group struct {
// Core fields
GroupID int32 `json:"group_id" db:"group_id"`
Options GroupOptions `json:"options"`
Members []*GroupMemberInfo `json:"members"`
RaidGroups []int32 `json:"raid_groups"`
CreatedTime time.Time `json:"created_time" db:"created_time"`
LastActivity time.Time `json:"last_activity" db:"last_activity"`
Disbanded bool `json:"disbanded" db:"disbanded"`
// Internal fields
membersMutex sync.RWMutex `json:"-"`
raidGroupsMutex sync.RWMutex `json:"-"`
optionsMutex sync.RWMutex `json:"-"`
activityMutex sync.RWMutex `json:"-"`
disbandMutex sync.RWMutex `json:"-"`
// Communication channels
messageQueue chan *GroupMessage `json:"-"`
updateQueue chan *GroupUpdate `json:"-"`
// Background processing
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
// Database integration - embedded operations
db any `json:"-"` // Database connection
isNew bool `json:"-"` // Flag for new groups
}
// New creates a new group
func New(db any) *Group {
group := &Group{
GroupID: 0, // Will be set when saved
Options: DefaultGroupOptions(),
Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE),
RaidGroups: make([]int32, 0),
CreatedTime: time.Now(),
LastActivity: time.Now(),
Disbanded: false,
messageQueue: make(chan *GroupMessage, 100),
updateQueue: make(chan *GroupUpdate, 100),
stopChan: make(chan struct{}),
db: db,
isNew: true,
}
// Start background processing
group.wg.Add(1)
go group.processMessages()
return group
}
// NewGroup creates a new group with specified ID and options
func NewGroup(id int32, options *GroupOptions, db any) *Group {
if options == nil {
defaultOpts := DefaultGroupOptions()
options = &defaultOpts
}
group := &Group{
GroupID: id,
Options: *options,
Members: make([]*GroupMemberInfo, 0, MAX_GROUP_SIZE),
RaidGroups: make([]int32, 0),
CreatedTime: time.Now(),
LastActivity: time.Now(),
Disbanded: false,
messageQueue: make(chan *GroupMessage, 100),
updateQueue: make(chan *GroupUpdate, 100),
stopChan: make(chan struct{}),
db: db,
isNew: false,
}
// Start background processing
group.wg.Add(1)
go group.processMessages()
return group
}
// GetID returns the group ID (implements Identifiable interface)
func (g *Group) GetID() int32 {
return g.GroupID
}
// Save saves the group to the database
func (g *Group) Save() error {
// TODO: Implement database save logic
// This would require integration with the actual database system
return nil
}
// Delete removes the group from the database
func (g *Group) Delete() error {
// Disband the group first
g.Disband()
// TODO: Implement database delete logic
// This would require integration with the actual database system
return nil
}
// Reload refreshes the group from the database
func (g *Group) Reload() error {
// TODO: Implement database reload logic
// This would require integration with the actual database system
return nil
}
// GetSize returns the number of members in the group
func (g *Group) GetSize() int32 {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
return int32(len(g.Members))
}
// GetMembers returns a copy of the member list
func (g *Group) GetMembers() []*GroupMemberInfo {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
members := make([]*GroupMemberInfo, len(g.Members))
for i, member := range g.Members {
members[i] = member.Copy()
}
return members
}
// AddMember adds a new member to the group
func (g *Group) AddMember(member Entity, isLeader bool) error {
if member == nil {
return fmt.Errorf("member cannot be nil")
}
g.disbandMutex.RLock()
if g.Disbanded {
g.disbandMutex.RUnlock()
return fmt.Errorf("group has been disbanded")
}
g.disbandMutex.RUnlock()
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Check if group is full
if len(g.Members) >= MAX_GROUP_SIZE {
return fmt.Errorf("group is full")
}
// Check if member is already in the group
for _, gmi := range g.Members {
if gmi.Member == member {
return fmt.Errorf("member is already in the group")
}
}
// Create new group member info
gmi := &GroupMemberInfo{
GroupID: g.GroupID,
Name: member.GetName(),
Leader: isLeader,
Member: member,
IsClient: member.IsPlayer(),
JoinTime: time.Now(),
LastUpdate: time.Now(),
}
// Update member stats from entity
gmi.UpdateStats()
// Set client reference if it's a player
if member.IsPlayer() {
// TODO: Get client reference from player
// gmi.Client = member.GetClient()
}
// Update zone information
if zone := member.GetZone(); zone != nil {
gmi.ZoneID = zone.GetZoneID()
gmi.InstanceID = zone.GetInstanceID()
gmi.Zone = zone.GetZoneName()
}
// Add to members list
g.Members = append(g.Members, gmi)
g.updateLastActivity()
// Set group reference on the entity
// TODO: Set group member info on entity
// member.SetGroupMemberInfo(gmi)
// Send group update
g.sendGroupUpdate(nil, false)
return nil
}
// AddMemberFromPeer adds a member from a peer server
func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID int8,
hpCur, hpMax int32, levelCur, levelMax int16, powerCur, powerMax int32,
raceID int8, zoneName string, mentorTargetCharID int32,
zoneID, instanceID int32, peerAddress string, peerPort int16, isRaidLooter bool) error {
g.disbandMutex.RLock()
if g.Disbanded {
g.disbandMutex.RUnlock()
return fmt.Errorf("group has been disbanded")
}
g.disbandMutex.RUnlock()
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Check if group is full
if len(g.Members) >= MAX_GROUP_SIZE {
return fmt.Errorf("group is full")
}
// Create new group member info for peer member
gmi := &GroupMemberInfo{
GroupID: g.GroupID,
Name: name,
Zone: zoneName,
HPCurrent: hpCur,
HPMax: hpMax,
PowerCurrent: powerCur,
PowerMax: powerMax,
LevelCurrent: levelCur,
LevelMax: levelMax,
RaceID: raceID,
ClassID: classID,
Leader: isLeader,
IsClient: isClient,
ZoneID: zoneID,
InstanceID: instanceID,
MentorTargetCharID: mentorTargetCharID,
ClientPeerAddress: peerAddress,
ClientPeerPort: peerPort,
IsRaidLooter: isRaidLooter,
Member: nil, // No local entity reference for peer members
Client: nil, // No local client reference for peer members
JoinTime: time.Now(),
LastUpdate: time.Now(),
}
// Add to members list
g.Members = append(g.Members, gmi)
g.updateLastActivity()
// Send group update
g.sendGroupUpdate(nil, false)
return nil
}
// RemoveMember removes a member from the group
func (g *Group) RemoveMember(member Entity) error {
if member == nil {
return fmt.Errorf("member cannot be nil")
}
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Find and remove the member
for i, gmi := range g.Members {
if gmi.Member == member {
// Clear group reference on entity
// TODO: Clear group member info on entity
// member.SetGroupMemberInfo(nil)
// Remove from slice
g.Members = append(g.Members[:i], g.Members[i+1:]...)
g.updateLastActivity()
// If this was a bot, camp it
// TODO: Handle bot camping
// if member.IsBot() {
// member.Camp()
// }
// Send group update
g.sendGroupUpdate(nil, false)
return nil
}
}
return fmt.Errorf("member not found in group")
}
// RemoveMemberByName removes a member by name (for peer members)
func (g *Group) RemoveMemberByName(name string, isClient bool, charID int32) error {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Find and remove the member
for i, gmi := range g.Members {
if gmi.Name == name && gmi.IsClient == isClient {
// Handle mentorship cleanup
if isClient && charID > 0 {
for _, otherGmi := range g.Members {
if otherGmi.MentorTargetCharID == charID {
otherGmi.MentorTargetCharID = 0
// TODO: Enable reset mentorship on client
// if otherGmi.Client != nil {
// otherGmi.Client.GetPlayer().EnableResetMentorship()
// }
}
}
}
// Remove from slice
g.Members = append(g.Members[:i], g.Members[i+1:]...)
g.updateLastActivity()
// Send group update
g.sendGroupUpdate(nil, false)
return nil
}
}
return fmt.Errorf("member not found in group")
}
// Disband disbands the group and removes all members
func (g *Group) Disband() {
g.disbandMutex.Lock()
if g.Disbanded {
g.disbandMutex.Unlock()
return
}
g.Disbanded = true
g.disbandMutex.Unlock()
// Stop background processing first to avoid deadlock
close(g.stopChan)
g.wg.Wait()
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Clear raid groups
g.raidGroupsMutex.Lock()
g.RaidGroups = nil
g.raidGroupsMutex.Unlock()
// Remove all members
for _, gmi := range g.Members {
if gmi.Member != nil {
// Clear group reference on entity
// TODO: Clear group member info on entity
// gmi.Member.SetGroupMemberInfo(nil)
// Handle bot camping
// TODO: Handle bot camping
// if gmi.Member.IsBot() {
// gmi.Member.Camp()
// }
}
// Handle mentorship cleanup
if gmi.MentorTargetCharID > 0 {
// TODO: Enable reset mentorship on client
// if gmi.Client != nil {
// gmi.Client.GetPlayer().EnableResetMentorship()
// }
}
// TODO: Set character/raid sheet changed flags
// if gmi.Client != nil {
// gmi.Client.GetPlayer().SetCharSheetChanged(true)
// if isInRaid {
// gmi.Client.GetPlayer().SetRaidSheetChanged(true)
// }
// }
}
// Clear members list
g.Members = nil
}
// SendGroupUpdate sends an update to all group members
func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) {
g.sendGroupUpdate(excludeClient, forceRaidUpdate)
}
// sendGroupUpdate internal method to send group updates
func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) {
update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.GroupID)
update.ExcludeClient = excludeClient
update.ForceRaidUpdate = forceRaidUpdate
select {
case g.updateQueue <- update:
default:
// Queue is full, drop the update
}
}
// SimpleGroupMessage sends a simple message to all group members
func (g *Group) SimpleGroupMessage(message string) {
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, CHANNEL_GROUP_CHAT, message, "", 0)
select {
case g.messageQueue <- msg:
default:
// Queue is full, drop the message
}
}
// SendGroupMessage sends a formatted message to all group members
func (g *Group) SendGroupMessage(msgType int8, message string) {
msg := NewGroupMessage(msgType, CHANNEL_GROUP_CHAT, message, "", 0)
select {
case g.messageQueue <- msg:
default:
// Queue is full, drop the message
}
}
// GroupChatMessage sends a chat message from a member to the group
func (g *Group) GroupChatMessage(from Entity, language int32, message string, channel int16) {
if from == nil {
return
}
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, from.GetName(), language)
select {
case g.messageQueue <- msg:
default:
// Queue is full, drop the message
}
}
// GroupChatMessageFromName sends a chat message from a named sender to the group
func (g *Group) GroupChatMessageFromName(fromName string, language int32, message string, channel int16) {
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, fromName, language)
select {
case g.messageQueue <- msg:
default:
// Queue is full, drop the message
}
}
// MakeLeader changes the group leader
func (g *Group) MakeLeader(newLeader Entity) error {
if newLeader == nil {
return fmt.Errorf("new leader cannot be nil")
}
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
var newLeaderGMI *GroupMemberInfo
// Find the new leader and update leadership
for _, gmi := range g.Members {
if gmi.Member == newLeader {
newLeaderGMI = gmi
gmi.Leader = true
} else if gmi.Leader {
// Remove leadership from current leader
gmi.Leader = false
}
}
if newLeaderGMI == nil {
return fmt.Errorf("new leader not found in group")
}
g.updateLastActivity()
// Send group update
g.sendGroupUpdate(nil, false)
return nil
}
// GetLeaderName returns the name of the group leader
func (g *Group) GetLeaderName() string {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
for _, gmi := range g.Members {
if gmi.Leader {
return gmi.Name
}
}
return ""
}
// ShareQuestWithGroup shares a quest with all group members
func (g *Group) ShareQuestWithGroup(questSharer any, quest any) bool {
// TODO: Implement quest sharing
// This would require integration with the quest system
return false
}
// UpdateGroupMemberInfo updates information for a specific member
func (g *Group) UpdateGroupMemberInfo(member Entity, groupMembersLocked bool) {
if member == nil {
return
}
if !groupMembersLocked {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
}
// Find the member and update their info
for _, gmi := range g.Members {
if gmi.Member == member {
gmi.UpdateStats()
g.updateLastActivity()
break
}
}
}
// GetGroupMemberByPosition returns a group member at a specific position
func (g *Group) GetGroupMemberByPosition(seeker Entity, mappedPosition int32) Entity {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
if mappedPosition < 0 || int(mappedPosition) >= len(g.Members) {
return nil
}
return g.Members[mappedPosition].Member
}
// RemoveClientReference removes client references when a client disconnects
// This is used for cleanup when a player disconnects but stays in the group
func (g *Group) RemoveClientReference(client any) {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
for _, gmi := range g.Members {
if gmi.Client != nil && gmi.Client == client {
gmi.Client = nil
// Don't set Member to nil as the entity might still exist
// Only clear the client reference
break
}
}
}
// GetGroupOptions returns a copy of the group options
func (g *Group) GetGroupOptions() GroupOptions {
g.optionsMutex.RLock()
defer g.optionsMutex.RUnlock()
return g.Options.Copy()
}
// SetGroupOptions sets new group options
func (g *Group) SetGroupOptions(options *GroupOptions) error {
if options == nil {
return fmt.Errorf("options cannot be nil")
}
if !options.IsValid() {
return fmt.Errorf("invalid group options")
}
g.optionsMutex.Lock()
g.Options = *options
g.optionsMutex.Unlock()
g.updateLastActivity()
// Send group update for options change
update := NewGroupUpdate(GROUP_UPDATE_FLAG_OPTIONS, g.GroupID)
update.Options = options
select {
case g.updateQueue <- update:
default:
// Queue is full, drop the update
}
return nil
}
// GetLastLooterIndex returns the last looter index
func (g *Group) GetLastLooterIndex() int8 {
g.optionsMutex.RLock()
defer g.optionsMutex.RUnlock()
return g.Options.LastLootedIndex
}
// SetNextLooterIndex sets the next looter index
func (g *Group) SetNextLooterIndex(newIndex int8) {
g.optionsMutex.Lock()
g.Options.LastLootedIndex = newIndex
g.optionsMutex.Unlock()
g.updateLastActivity()
}
// Raid functionality
// GetRaidGroups returns a copy of the raid groups list
func (g *Group) GetRaidGroups() []int32 {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
if g.RaidGroups == nil {
return []int32{}
}
groups := make([]int32, len(g.RaidGroups))
copy(groups, g.RaidGroups)
return groups
}
// ReplaceRaidGroups replaces the entire raid groups list
func (g *Group) ReplaceRaidGroups(groups []int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
if groups == nil {
g.RaidGroups = make([]int32, 0)
} else {
g.RaidGroups = make([]int32, len(groups))
copy(g.RaidGroups, groups)
}
g.updateLastActivity()
}
// IsInRaidGroup checks if this group is in a raid with the specified group
func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
for _, id := range g.RaidGroups {
if id == groupID {
return true
}
}
return false
}
// AddGroupToRaid adds a group to the raid
func (g *Group) AddGroupToRaid(groupID int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
// Check if already in raid
for _, id := range g.RaidGroups {
if id == groupID {
return
}
}
g.RaidGroups = append(g.RaidGroups, groupID)
g.updateLastActivity()
}
// RemoveGroupFromRaid removes a group from the raid
func (g *Group) RemoveGroupFromRaid(groupID int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
for i, id := range g.RaidGroups {
if id == groupID {
g.RaidGroups = append(g.RaidGroups[:i], g.RaidGroups[i+1:]...)
g.updateLastActivity()
break
}
}
}
// IsGroupRaid checks if this group is part of a raid
func (g *Group) IsGroupRaid() bool {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
return len(g.RaidGroups) > 0
}
// ClearGroupRaid clears all raid associations
func (g *Group) ClearGroupRaid() {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
g.RaidGroups = make([]int32, 0)
g.updateLastActivity()
}
// IsDisbanded checks if the group has been disbanded
func (g *Group) IsDisbanded() bool {
g.disbandMutex.RLock()
defer g.disbandMutex.RUnlock()
return g.Disbanded
}
// GetCreatedTime returns when the group was created
func (g *Group) GetCreatedTime() time.Time {
return g.CreatedTime
}
// GetLastActivity returns the last activity time
func (g *Group) GetLastActivity() time.Time {
g.activityMutex.RLock()
defer g.activityMutex.RUnlock()
return g.LastActivity
}
// updateLastActivity updates the last activity timestamp
func (g *Group) updateLastActivity() {
g.activityMutex.Lock()
defer g.activityMutex.Unlock()
g.LastActivity = time.Now()
}
// processMessages processes messages and updates in the background
func (g *Group) processMessages() {
defer g.wg.Done()
for {
select {
case msg := <-g.messageQueue:
g.handleMessage(msg)
case update := <-g.updateQueue:
g.handleUpdate(update)
case <-g.stopChan:
return
}
}
}
// handleMessage handles a group message
func (g *Group) handleMessage(msg *GroupMessage) {
if msg == nil {
return
}
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
// Send message to all group members except the excluded client
for _, gmi := range g.Members {
if gmi.Client != nil && gmi.Client != msg.ExcludeClient {
// TODO: Send message to client
// This would require integration with the client system
}
}
}
// handleUpdate handles a group update
func (g *Group) handleUpdate(update *GroupUpdate) {
if update == nil {
return
}
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
// Send update to all group members except the excluded client
for _, gmi := range g.Members {
if gmi.Client != nil && gmi.Client != update.ExcludeClient {
// TODO: Send update to client
// This would require integration with the client system
// if gmi.Client != nil {
// gmi.Client.GetPlayer().SetCharSheetChanged(true)
// if isInRaid || update.ForceRaidUpdate {
// gmi.Client.GetPlayer().SetRaidSheetChanged(true)
// }
// }
}
}
}

1633
internal/groups/groups.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,501 +0,0 @@
package groups
import (
"time"
)
// GroupAware interface for entities that can be part of groups
type GroupAware interface {
// GetGroupMemberInfo returns the group member info for this entity
GetGroupMemberInfo() *GroupMemberInfo
// SetGroupMemberInfo sets the group member info for this entity
SetGroupMemberInfo(info *GroupMemberInfo)
// GetGroupID returns the current group ID
GetGroupID() int32
// SetGroupID sets the current group ID
SetGroupID(groupID int32)
// IsInGroup returns true if the entity is in a group
IsInGroup() bool
}
// GroupManager interface for managing groups
type GroupManagerInterface interface {
// Group creation and management
NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error)
RemoveGroup(groupID int32) error
GetGroup(groupID int32) *Group
IsGroupIDValid(groupID int32) bool
// Member management
AddGroupMember(groupID int32, member Entity, isLeader bool) error
AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error
RemoveGroupMember(groupID int32, member Entity) error
RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error
// Group updates
SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool)
// Invitations
Invite(leader Entity, member Entity) int8
AddInvite(leader Entity, member Entity) bool
AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8
DeclineInvite(member Entity)
ClearPendingInvite(member Entity)
HasPendingInvite(member Entity) string
// Group utilities
GetGroupSize(groupID int32) int32
IsInGroup(groupID int32, member Entity) bool
IsPlayerInGroup(groupID int32, charID int32) Entity
IsSpawnInGroup(groupID int32, name string) bool
GetGroupLeader(groupID int32) Entity
MakeLeader(groupID int32, newLeader Entity) bool
// Messaging
SimpleGroupMessage(groupID int32, message string)
SendGroupMessage(groupID int32, msgType int8, message string)
GroupMessage(groupID int32, message string)
GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16)
GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16)
SendGroupChatMessage(groupID int32, channel int16, message string)
// Raid functionality
ClearGroupRaid(groupID int32)
RemoveGroupFromRaid(groupID, targetGroupID int32)
IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool
GetRaidGroups(groupID int32) []int32
ReplaceRaidGroups(groupID int32, newGroups []int32)
// Group options
GetDefaultGroupOptions(groupID int32) (GroupOptions, bool)
SetGroupOptions(groupID int32, options *GroupOptions) error
// Statistics
GetStats() GroupManagerStats
GetGroupCount() int32
GetAllGroups() []*Group
}
// GroupEventHandler interface for handling group events
type GroupEventHandler interface {
// Group lifecycle events
OnGroupCreated(group *Group, leader Entity) error
OnGroupDisbanded(group *Group) error
OnGroupMemberJoined(group *Group, member Entity) error
OnGroupMemberLeft(group *Group, member Entity) error
OnGroupLeaderChanged(group *Group, oldLeader, newLeader Entity) error
// Invitation events
OnGroupInviteSent(leader, member Entity) error
OnGroupInviteAccepted(leader, member Entity, groupID int32) error
OnGroupInviteDeclined(leader, member Entity) error
OnGroupInviteExpired(leader, member Entity) error
// Raid events
OnRaidFormed(groups []*Group) error
OnRaidDisbanded(groups []*Group) error
OnRaidInviteSent(leaderGroup *Group, targetGroup *Group) error
OnRaidInviteAccepted(leaderGroup *Group, targetGroup *Group) error
OnRaidInviteDeclined(leaderGroup *Group, targetGroup *Group) error
// Group activity events
OnGroupMessage(group *Group, from Entity, message string, channel int16) error
OnGroupOptionsChanged(group *Group, oldOptions, newOptions *GroupOptions) error
OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error
}
// GroupDatabase interface for database operations
type GroupDatabase interface {
// Group persistence
SaveGroup(group *Group) error
LoadGroup(groupID int32) (*Group, error)
DeleteGroup(groupID int32) error
// Group member persistence
SaveGroupMember(groupID int32, member *GroupMemberInfo) error
LoadGroupMembers(groupID int32) ([]*GroupMemberInfo, error)
DeleteGroupMember(groupID int32, memberName string) error
// Group options persistence
SaveGroupOptions(groupID int32, options *GroupOptions) error
LoadGroupOptions(groupID int32) (*GroupOptions, error)
// Raid persistence
SaveRaidGroups(groupID int32, raidGroups []int32) error
LoadRaidGroups(groupID int32) ([]int32, error)
// Statistics persistence
SaveGroupStats(stats *GroupManagerStats) error
LoadGroupStats() (*GroupManagerStats, error)
// Cleanup operations
CleanupExpiredGroups() error
CleanupOrphanedMembers() error
}
// GroupPacketHandler interface for handling group-related packets
type GroupPacketHandler interface {
// Group update packets
SendGroupUpdate(members []*GroupMemberInfo, excludeClient any) error
SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient any) error
SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient any) error
// Group invitation packets
SendGroupInvite(inviter, invitee Entity) error
SendGroupInviteResponse(inviter, invitee Entity, accepted bool) error
// Group messaging packets
SendGroupMessage(members []*GroupMemberInfo, message *GroupMessage) error
SendGroupChatMessage(members []*GroupMemberInfo, from string, message string, channel int16, language int32) error
// Raid packets
SendRaidUpdate(raidGroups []*Group, excludeClient any) error
SendRaidInvite(leaderGroup, targetGroup *Group) error
SendRaidInviteResponse(leaderGroup, targetGroup *Group, accepted bool) error
// Group UI packets
SendGroupWindowUpdate(client any, group *Group) error
SendRaidWindowUpdate(client any, raidGroups []*Group) error
// Group member packets
SendGroupMemberStats(member *GroupMemberInfo, excludeClient any) error
SendGroupMemberZoneChange(member *GroupMemberInfo, oldZoneID, newZoneID int32) error
}
// GroupValidator interface for validating group operations
type GroupValidator interface {
// Group creation validation
ValidateGroupCreation(leader Entity, options *GroupOptions) error
ValidateGroupJoin(group *Group, member Entity) error
ValidateGroupLeave(group *Group, member Entity) error
// Invitation validation
ValidateGroupInvite(leader, member Entity) error
ValidateRaidInvite(leaderGroup, targetGroup *Group) error
// Group operation validation
ValidateLeadershipChange(group *Group, oldLeader, newLeader Entity) error
ValidateGroupOptions(group *Group, options *GroupOptions) error
ValidateGroupMessage(group *Group, from Entity, message string) error
// Raid validation
ValidateRaidFormation(groups []*Group) error
ValidateRaidOperation(raidGroups []*Group, operation string) error
}
// GroupNotifier interface for sending notifications
type GroupNotifier interface {
// Group notifications
NotifyGroupCreated(group *Group, leader Entity) error
NotifyGroupDisbanded(group *Group, reason string) error
NotifyGroupMemberJoined(group *Group, member Entity) error
NotifyGroupMemberLeft(group *Group, member Entity, reason string) error
NotifyGroupLeaderChanged(group *Group, oldLeader, newLeader Entity) error
// Invitation notifications
NotifyGroupInviteSent(leader, member Entity) error
NotifyGroupInviteReceived(leader, member Entity) error
NotifyGroupInviteAccepted(leader, member Entity, groupID int32) error
NotifyGroupInviteDeclined(leader, member Entity) error
NotifyGroupInviteExpired(leader, member Entity) error
// Raid notifications
NotifyRaidFormed(groups []*Group) error
NotifyRaidDisbanded(groups []*Group, reason string) error
NotifyRaidInviteSent(leaderGroup, targetGroup *Group) error
NotifyRaidInviteReceived(leaderGroup, targetGroup *Group) error
NotifyRaidInviteAccepted(leaderGroup, targetGroup *Group) error
NotifyRaidInviteDeclined(leaderGroup, targetGroup *Group) error
// System notifications
NotifyGroupSystemMessage(group *Group, message string, msgType int8) error
NotifyGroupError(group *Group, error string, errorCode int8) error
}
// GroupStatistics interface for tracking group statistics
type GroupStatistics interface {
// Group statistics
RecordGroupCreated(group *Group, leader Entity)
RecordGroupDisbanded(group *Group, duration int64)
RecordGroupMemberJoined(group *Group, member Entity)
RecordGroupMemberLeft(group *Group, member Entity, duration int64)
// Invitation statistics
RecordInviteSent(leader, member Entity)
RecordInviteAccepted(leader, member Entity, responseTime int64)
RecordInviteDeclined(leader, member Entity, responseTime int64)
RecordInviteExpired(leader, member Entity)
// Raid statistics
RecordRaidFormed(groups []*Group)
RecordRaidDisbanded(groups []*Group, duration int64)
// Activity statistics
RecordGroupMessage(group *Group, from Entity, messageType int8)
RecordGroupActivity(group *Group, activityType string)
// Performance statistics
RecordGroupProcessingTime(operation string, duration int64)
RecordGroupMemoryUsage(groups int32, members int32)
// Statistics retrieval
GetGroupStatistics(groupID int32) map[string]any
GetOverallStatistics() map[string]any
GetStatisticsSummary() *GroupManagerStats
}
// GroupAdapter adapts group functionality for other systems
type GroupAdapter struct {
group *Group
}
// NewGroupAdapter creates a new group adapter
func NewGroupAdapter(group *Group) *GroupAdapter {
return &GroupAdapter{group: group}
}
// GetGroup returns the wrapped group
func (ga *GroupAdapter) GetGroup() *Group {
return ga.group
}
// GetGroupID returns the group ID
func (ga *GroupAdapter) GetGroupID() int32 {
return ga.group.GetID()
}
// GetGroupSize returns the group size
func (ga *GroupAdapter) GetGroupSize() int32 {
return ga.group.GetSize()
}
// GetMembers returns group members
func (ga *GroupAdapter) GetMembers() []*GroupMemberInfo {
return ga.group.GetMembers()
}
// GetLeader returns the group leader
func (ga *GroupAdapter) GetLeader() Entity {
members := ga.group.GetMembers()
for _, member := range members {
if member.Leader {
return member.Member
}
}
return nil
}
// GetLeaderName returns the group leader's name
func (ga *GroupAdapter) GetLeaderName() string {
return ga.group.GetLeaderName()
}
// IsInRaid returns true if the group is part of a raid
func (ga *GroupAdapter) IsInRaid() bool {
return ga.group.IsGroupRaid()
}
// GetRaidGroups returns the raid groups
func (ga *GroupAdapter) GetRaidGroups() []int32 {
return ga.group.GetRaidGroups()
}
// IsMember checks if an entity is a member of the group
func (ga *GroupAdapter) IsMember(entity Entity) bool {
if entity == nil {
return false
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity {
return true
}
}
return false
}
// HasMemberNamed checks if the group has a member with the given name
func (ga *GroupAdapter) HasMemberNamed(name string) bool {
members := ga.group.GetMembers()
for _, member := range members {
if member.Name == name {
return true
}
}
return false
}
// GetMemberByName returns a member by name
func (ga *GroupAdapter) GetMemberByName(name string) *GroupMemberInfo {
members := ga.group.GetMembers()
for _, member := range members {
if member.Name == name {
return member
}
}
return nil
}
// GetMemberByEntity returns a member by entity
func (ga *GroupAdapter) GetMemberByEntity(entity Entity) *GroupMemberInfo {
if entity == nil {
return nil
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity {
return member
}
}
return nil
}
// IsLeader checks if an entity is the group leader
func (ga *GroupAdapter) IsLeader(entity Entity) bool {
if entity == nil {
return false
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity && member.Leader {
return true
}
}
return false
}
// GetOptions returns the group options
func (ga *GroupAdapter) GetOptions() GroupOptions {
return ga.group.GetGroupOptions()
}
// IsDisbanded returns true if the group has been disbanded
func (ga *GroupAdapter) IsDisbanded() bool {
return ga.group.IsDisbanded()
}
// GetCreatedTime returns when the group was created
func (ga *GroupAdapter) GetCreatedTime() time.Time {
return ga.group.GetCreatedTime()
}
// GetLastActivity returns the last activity time
func (ga *GroupAdapter) GetLastActivity() time.Time {
return ga.group.GetLastActivity()
}
// EntityGroupAdapter adapts entity functionality for group systems
type EntityGroupAdapter struct {
entity Entity
}
// NewEntityGroupAdapter creates a new entity group adapter
func NewEntityGroupAdapter(entity Entity) *EntityGroupAdapter {
return &EntityGroupAdapter{entity: entity}
}
// GetEntity returns the wrapped entity
func (ega *EntityGroupAdapter) GetEntity() Entity {
return ega.entity
}
// GetName returns the entity name
func (ega *EntityGroupAdapter) GetName() string {
return ega.entity.GetName()
}
// GetLevel returns the entity level
func (ega *EntityGroupAdapter) GetLevel() int8 {
return ega.entity.GetLevel()
}
// GetClass returns the entity class
func (ega *EntityGroupAdapter) GetClass() int8 {
return ega.entity.GetClass()
}
// GetRace returns the entity race
func (ega *EntityGroupAdapter) GetRace() int8 {
return ega.entity.GetRace()
}
// GetZoneID returns the current zone ID
func (ega *EntityGroupAdapter) GetZoneID() int32 {
if zone := ega.entity.GetZone(); zone != nil {
return zone.GetZoneID()
}
return 0
}
// GetInstanceID returns the current instance ID
func (ega *EntityGroupAdapter) GetInstanceID() int32 {
if zone := ega.entity.GetZone(); zone != nil {
return zone.GetInstanceID()
}
return 0
}
// GetZoneName returns the current zone name
func (ega *EntityGroupAdapter) GetZoneName() string {
if zone := ega.entity.GetZone(); zone != nil {
return zone.GetZoneName()
}
return ""
}
// GetHP returns current HP
func (ega *EntityGroupAdapter) GetHP() int32 {
return ega.entity.GetHP()
}
// GetMaxHP returns maximum HP
func (ega *EntityGroupAdapter) GetMaxHP() int32 {
return ega.entity.GetTotalHP()
}
// GetPower returns current power
func (ega *EntityGroupAdapter) GetPower() int32 {
return ega.entity.GetPower()
}
// GetMaxPower returns maximum power
func (ega *EntityGroupAdapter) GetMaxPower() int32 {
return ega.entity.GetTotalPower()
}
// IsPlayer returns true if the entity is a player
func (ega *EntityGroupAdapter) IsPlayer() bool {
return ega.entity.IsPlayer()
}
// IsNPC returns true if the entity is an NPC
func (ega *EntityGroupAdapter) IsNPC() bool {
return ega.entity.IsNPC()
}
// IsBot returns true if the entity is a bot
func (ega *EntityGroupAdapter) IsBot() bool {
return ega.entity.IsBot()
}
// IsAlive returns true if the entity is alive
func (ega *EntityGroupAdapter) IsAlive() bool {
return !ega.entity.IsDead()
}
// IsDead returns true if the entity is dead
func (ega *EntityGroupAdapter) IsDead() bool {
return ega.entity.IsDead()
}
// GetDistance returns distance to another entity
func (ega *EntityGroupAdapter) GetDistance(other Entity) float32 {
return ega.entity.GetDistance(other)
}

View File

@ -1,510 +0,0 @@
package groups
import (
"fmt"
"sync"
"time"
)
// Manager provides group management with embedded database operations
type Manager struct {
// Core fields with embedded database operations
MasterList *MasterList `json:"master_list"`
Config GroupManagerConfig `json:"config"`
Stats GroupManagerStats `json:"stats"`
// Group ID generation
nextGroupID int32 `json:"-" db:"next_group_id"`
nextGroupIDMutex sync.Mutex `json:"-"`
// Pending invitations
PendingInvites map[string]*GroupInvite `json:"pending_invites"`
RaidPendingInvites map[string]*GroupInvite `json:"raid_pending_invites"`
invitesMutex sync.RWMutex `json:"-"`
// Event handlers
EventHandlers []GroupEventHandler `json:"-"`
eventHandlersMutex sync.RWMutex `json:"-"`
// Statistics
statsMutex sync.RWMutex `json:"-"`
// Background processing
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
// Integration interfaces
database GroupDatabase `json:"-"`
packetHandler GroupPacketHandler `json:"-"`
validator GroupValidator `json:"-"`
notifier GroupNotifier `json:"-"`
// Database integration - embedded operations
db any `json:"-"` // Database connection
isNew bool `json:"-"` // Flag for new managers
}
// New creates a new group manager
func NewManager(config GroupManagerConfig, db any) *Manager {
manager := &Manager{
MasterList: NewMasterList(),
Config: config,
Stats: GroupManagerStats{},
nextGroupID: 1,
PendingInvites: make(map[string]*GroupInvite),
RaidPendingInvites: make(map[string]*GroupInvite),
EventHandlers: make([]GroupEventHandler, 0),
stopChan: make(chan struct{}),
db: db,
isNew: true,
}
return manager
}
// Save saves the manager state to the database
func (m *Manager) Save() error {
// TODO: Implement database save logic
return nil
}
// Delete removes the manager from the database
func (m *Manager) Delete() error {
// Stop the manager first
m.Stop()
// TODO: Implement database delete logic
return nil
}
// Reload refreshes the manager from the database
func (m *Manager) Reload() error {
// TODO: Implement database reload logic
return nil
}
// Start starts the group manager background processes
func (m *Manager) Start() error {
// Start background processes
if m.Config.UpdateInterval > 0 {
m.wg.Add(1)
go m.updateGroupsLoop()
}
if m.Config.BuffUpdateInterval > 0 {
m.wg.Add(1)
go m.updateBuffsLoop()
}
m.wg.Add(1)
go m.cleanupExpiredInvitesLoop()
if m.Config.EnableStatistics {
m.wg.Add(1)
go m.updateStatsLoop()
}
return nil
}
// Stop stops the group manager and all background processes
func (m *Manager) Stop() error {
close(m.stopChan)
m.wg.Wait()
return nil
}
// NewGroup creates a new group with the given leader and options
func (m *Manager) NewGroup(leader Entity, options *GroupOptions, overrideGroupID int32) (int32, error) {
if leader == nil {
return 0, fmt.Errorf("leader cannot be nil")
}
var groupID int32
if overrideGroupID > 0 {
groupID = overrideGroupID
} else {
groupID = m.generateNextGroupID()
}
// Check if group ID already exists
if m.MasterList.GetGroup(groupID) != nil && overrideGroupID == 0 {
return 0, fmt.Errorf("group ID %d already exists", groupID)
}
// Create new group
group := NewGroup(groupID, options, m.db)
// Add leader to the group
if err := group.AddMember(leader, true); err != nil {
group.Disband()
return 0, fmt.Errorf("failed to add leader to group: %v", err)
}
// Add group to manager
if !m.MasterList.AddGroup(group) {
group.Disband()
return 0, fmt.Errorf("failed to add group to master list")
}
// Update statistics
m.updateStatsForNewGroup()
// Fire event
m.fireGroupCreatedEvent(group, leader)
return groupID, nil
}
// RemoveGroup removes a group from the manager
func (m *Manager) RemoveGroup(groupID int32) error {
group := m.MasterList.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
// Disband the group
group.Disband()
// Remove from master list
if !m.MasterList.RemoveGroup(groupID) {
return fmt.Errorf("failed to remove group %d from master list", groupID)
}
// Update statistics
m.updateStatsForRemovedGroup()
// Fire event
m.fireGroupDisbandedEvent(group)
return nil
}
// GetGroup returns a group by ID
func (m *Manager) GetGroup(groupID int32) *Group {
return m.MasterList.GetGroup(groupID)
}
// IsGroupIDValid checks if a group ID is valid and exists
func (m *Manager) IsGroupIDValid(groupID int32) bool {
return m.MasterList.GetGroup(groupID) != nil
}
// AddGroupMember adds a member to an existing group
func (m *Manager) AddGroupMember(groupID int32, member Entity, isLeader bool) error {
group := m.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
return group.AddMember(member, isLeader)
}
// AddGroupMemberFromPeer adds a member from a peer server to an existing group
func (m *Manager) AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error {
group := m.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
return group.AddMemberFromPeer(
info.Name, info.Leader, info.IsClient, info.ClassID,
info.HPCurrent, info.HPMax, info.LevelCurrent, info.LevelMax,
info.PowerCurrent, info.PowerMax, info.RaceID, info.Zone,
info.MentorTargetCharID, info.ZoneID, info.InstanceID,
info.ClientPeerAddress, info.ClientPeerPort, info.IsRaidLooter,
)
}
// RemoveGroupMember removes a member from a group
func (m *Manager) RemoveGroupMember(groupID int32, member Entity) error {
group := m.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
err := group.RemoveMember(member)
if err != nil {
return err
}
// If group is now empty, remove it
if group.GetSize() == 0 {
m.RemoveGroup(groupID)
}
return nil
}
// RemoveGroupMemberByName removes a member by name from a group
func (m *Manager) RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error {
group := m.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
err := group.RemoveMemberByName(name, isClient, charID)
if err != nil {
return err
}
// If group is now empty, remove it
if group.GetSize() == 0 {
m.RemoveGroup(groupID)
}
return nil
}
// SendGroupUpdate sends an update to all members of a group
func (m *Manager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) {
group := m.GetGroup(groupID)
if group != nil {
group.SendGroupUpdate(excludeClient, forceRaidUpdate)
}
}
// Invite handles inviting a player to a group
func (m *Manager) Invite(leader Entity, member Entity) int8 {
if leader == nil || member == nil {
return GROUP_INVITE_TARGET_NOT_FOUND
}
// Check if inviting self
if leader == member {
return GROUP_INVITE_SELF_INVITE
}
// Check if member already has an invite
inviteKey := member.GetName()
if m.hasPendingInvite(inviteKey) != "" {
return GROUP_INVITE_ALREADY_HAS_INVITE
}
// Check if member is already in a group
// TODO: Check if member already in group
// if member.GetGroupMemberInfo() != nil {
// return GROUP_INVITE_ALREADY_IN_GROUP
// }
// Add the invite
if !m.addInvite(leader, member) {
return GROUP_INVITE_PERMISSION_DENIED
}
// Fire event
m.fireGroupInviteSentEvent(leader, member)
return GROUP_INVITE_SUCCESS
}
// AddInvite adds a group invitation
func (m *Manager) AddInvite(leader Entity, member Entity) bool {
return m.addInvite(leader, member)
}
// addInvite internal method to add an invitation
func (m *Manager) addInvite(leader Entity, member Entity) bool {
if leader == nil || member == nil {
return false
}
inviteKey := member.GetName()
leaderName := leader.GetName()
invite := &GroupInvite{
InviterName: leaderName,
InviteeName: inviteKey,
GroupID: 0, // Will be set when group is created
IsRaidInvite: false,
CreatedTime: time.Now(),
ExpiresTime: time.Now().Add(m.Config.InviteTimeout),
}
m.invitesMutex.Lock()
m.PendingInvites[inviteKey] = invite
m.invitesMutex.Unlock()
// Update statistics
m.updateStatsForInvite()
return true
}
// AcceptInvite handles accepting of a group invite
func (m *Manager) AcceptInvite(member Entity, groupOverrideID *int32, autoAddGroup bool) int8 {
if member == nil {
return GROUP_INVITE_TARGET_NOT_FOUND
}
inviteKey := member.GetName()
m.invitesMutex.Lock()
invite, exists := m.PendingInvites[inviteKey]
if !exists {
m.invitesMutex.Unlock()
return GROUP_INVITE_TARGET_NOT_FOUND
}
// Check if invite has expired
if invite.IsExpired() {
delete(m.PendingInvites, inviteKey)
m.invitesMutex.Unlock()
m.updateStatsForExpiredInvite()
return GROUP_INVITE_DECLINED
}
// Remove the invite
delete(m.PendingInvites, inviteKey)
m.invitesMutex.Unlock()
if !autoAddGroup {
return GROUP_INVITE_SUCCESS
}
// Find the leader
var leader Entity
// TODO: Find leader entity by name
// leader = world.GetPlayerByName(invite.InviterName)
if leader == nil {
return GROUP_INVITE_TARGET_NOT_FOUND
}
var groupID int32
if groupOverrideID != nil {
groupID = *groupOverrideID
}
// Check if leader already has a group
// TODO: Get leader's group ID
// leaderGroupID := leader.GetGroupID()
leaderGroupID := int32(0) // Placeholder
if leaderGroupID == 0 {
// Create new group with leader
var err error
if groupID != 0 {
groupID, err = m.NewGroup(leader, nil, groupID)
} else {
groupID, err = m.NewGroup(leader, nil, 0)
}
if err != nil {
return GROUP_INVITE_PERMISSION_DENIED
}
} else {
groupID = leaderGroupID
}
// Add member to the group
if err := m.AddGroupMember(groupID, member, false); err != nil {
return GROUP_INVITE_GROUP_FULL
}
// Update statistics
m.updateStatsForAcceptedInvite()
// Fire event
m.fireGroupInviteAcceptedEvent(leader, member, groupID)
return GROUP_INVITE_SUCCESS
}
// DeclineInvite handles declining of a group invite
func (m *Manager) DeclineInvite(member Entity) {
if member == nil {
return
}
inviteKey := member.GetName()
m.invitesMutex.Lock()
_, exists := m.PendingInvites[inviteKey]
if exists {
delete(m.PendingInvites, inviteKey)
}
m.invitesMutex.Unlock()
if exists {
// Update statistics
m.updateStatsForDeclinedInvite()
// Fire event
var leader Entity
// TODO: Find leader entity by name
// leader = world.GetPlayerByName(invite.InviterName)
m.fireGroupInviteDeclinedEvent(leader, member)
}
}
// ClearPendingInvite clears a pending invite for a member
func (m *Manager) ClearPendingInvite(member Entity) {
if member == nil {
return
}
inviteKey := member.GetName()
m.invitesMutex.Lock()
delete(m.PendingInvites, inviteKey)
m.invitesMutex.Unlock()
}
// HasPendingInvite checks if a member has a pending invite and returns the inviter name
func (m *Manager) HasPendingInvite(member Entity) string {
if member == nil {
return ""
}
inviteKey := member.GetName()
return m.hasPendingInvite(inviteKey)
}
// hasPendingInvite internal method to check for pending invites
func (m *Manager) hasPendingInvite(inviteKey string) string {
m.invitesMutex.RLock()
defer m.invitesMutex.RUnlock()
if invite, exists := m.PendingInvites[inviteKey]; exists {
if !invite.IsExpired() {
return invite.InviterName
}
}
return ""
}
// generateNextGroupID generates the next available group ID
func (m *Manager) generateNextGroupID() int32 {
m.nextGroupIDMutex.Lock()
defer m.nextGroupIDMutex.Unlock()
id := m.nextGroupID
m.nextGroupID++
// Handle overflow
if m.nextGroupID <= 0 {
m.nextGroupID = 1
}
return id
}
// GetGroupCount returns the number of active groups
func (m *Manager) GetGroupCount() int32 {
return int32(m.MasterList.Size())
}
// GetAllGroups returns all active groups
func (m *Manager) GetAllGroups() []*Group {
return m.MasterList.GetAllGroups()
}
// GetStats returns current manager statistics
func (m *Manager) GetStats() GroupManagerStats {
m.statsMutex.RLock()
defer m.statsMutex.RUnlock()
return m.Stats
}

View File

@ -1,528 +0,0 @@
package groups
import (
"fmt"
"time"
)
// Group utility methods
// GetGroupSize returns the size of a group
func (m *Manager) GetGroupSize(groupID int32) int32 {
group := m.GetGroup(groupID)
if group == nil {
return 0
}
return group.GetSize()
}
// IsInGroup checks if an entity is in a specific group
func (m *Manager) IsInGroup(groupID int32, member Entity) bool {
group := m.GetGroup(groupID)
if group == nil || member == nil {
return false
}
members := group.GetMembers()
for _, gmi := range members {
if gmi.Member == member {
return true
}
}
return false
}
// IsPlayerInGroup checks if a player with the given character ID is in a group
func (m *Manager) IsPlayerInGroup(groupID int32, charID int32) Entity {
group := m.GetGroup(groupID)
if group == nil {
return nil
}
members := group.GetMembers()
for _, gmi := range members {
if gmi.IsClient && gmi.Member != nil {
// TODO: Check character ID
// if gmi.Member.GetCharacterID() == charID {
// return gmi.Member
// }
}
}
return nil
}
// IsSpawnInGroup checks if a spawn with the given name is in a group
func (m *Manager) IsSpawnInGroup(groupID int32, name string) bool {
group := m.GetGroup(groupID)
if group == nil {
return false
}
members := group.GetMembers()
for _, gmi := range members {
if gmi.Name == name {
return true
}
}
return false
}
// GetGroupLeader returns the leader of a group
func (m *Manager) GetGroupLeader(groupID int32) Entity {
group := m.GetGroup(groupID)
if group == nil {
return nil
}
members := group.GetMembers()
for _, gmi := range members {
if gmi.Leader {
return gmi.Member
}
}
return nil
}
// MakeLeader changes the leader of a group
func (m *Manager) MakeLeader(groupID int32, newLeader Entity) bool {
group := m.GetGroup(groupID)
if group == nil {
return false
}
err := group.MakeLeader(newLeader)
return err == nil
}
// Group messaging
// SimpleGroupMessage sends a simple message to all members of a group
func (m *Manager) SimpleGroupMessage(groupID int32, message string) {
group := m.GetGroup(groupID)
if group != nil {
group.SimpleGroupMessage(message)
}
}
// SendGroupMessage sends a formatted message to all members of a group
func (m *Manager) SendGroupMessage(groupID int32, msgType int8, message string) {
group := m.GetGroup(groupID)
if group != nil {
group.SendGroupMessage(msgType, message)
}
}
// GroupMessage sends a message to all members of a group (alias for SimpleGroupMessage)
func (m *Manager) GroupMessage(groupID int32, message string) {
m.SimpleGroupMessage(groupID, message)
}
// GroupChatMessage sends a chat message from a member to the group
func (m *Manager) GroupChatMessage(groupID int32, from Entity, language int32, message string, channel int16) {
group := m.GetGroup(groupID)
if group != nil {
group.GroupChatMessage(from, language, message, channel)
}
}
// GroupChatMessageFromName sends a chat message from a named sender to the group
func (m *Manager) GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16) {
group := m.GetGroup(groupID)
if group != nil {
group.GroupChatMessageFromName(fromName, language, message, channel)
}
}
// SendGroupChatMessage sends a formatted chat message to the group
func (m *Manager) SendGroupChatMessage(groupID int32, channel int16, message string) {
m.GroupChatMessageFromName(groupID, "System", 0, message, channel)
}
// Raid functionality
// ClearGroupRaid clears raid associations for a group
func (m *Manager) ClearGroupRaid(groupID int32) {
group := m.GetGroup(groupID)
if group != nil {
group.ClearGroupRaid()
}
}
// RemoveGroupFromRaid removes a group from a raid
func (m *Manager) RemoveGroupFromRaid(groupID, targetGroupID int32) {
group := m.GetGroup(groupID)
if group != nil {
group.RemoveGroupFromRaid(targetGroupID)
}
}
// IsInRaidGroup checks if two groups are in the same raid
func (m *Manager) IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool {
group := m.GetGroup(groupID)
if group == nil {
return false
}
return group.IsInRaidGroup(targetGroupID, isLeaderGroup)
}
// GetRaidGroups returns the raid groups for a specific group
func (m *Manager) GetRaidGroups(groupID int32) []int32 {
group := m.GetGroup(groupID)
if group == nil {
return []int32{}
}
return group.GetRaidGroups()
}
// ReplaceRaidGroups replaces the raid groups for a specific group
func (m *Manager) ReplaceRaidGroups(groupID int32, newGroups []int32) {
group := m.GetGroup(groupID)
if group != nil {
group.ReplaceRaidGroups(newGroups)
}
}
// Group options
// GetDefaultGroupOptions returns the default group options for a group
func (m *Manager) GetDefaultGroupOptions(groupID int32) (GroupOptions, bool) {
group := m.GetGroup(groupID)
if group == nil {
return GroupOptions{}, false
}
return group.GetGroupOptions(), true
}
// SetGroupOptions sets group options for a specific group
func (m *Manager) SetGroupOptions(groupID int32, options *GroupOptions) error {
group := m.GetGroup(groupID)
if group == nil {
return fmt.Errorf("group %d not found", groupID)
}
return group.SetGroupOptions(options)
}
// Background processing loops
// updateGroupsLoop periodically updates all groups
func (m *Manager) updateGroupsLoop() {
defer m.wg.Done()
ticker := time.NewTicker(m.Config.UpdateInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.processGroupUpdates()
case <-m.stopChan:
return
}
}
}
// updateBuffsLoop periodically updates group buffs
func (m *Manager) updateBuffsLoop() {
defer m.wg.Done()
ticker := time.NewTicker(m.Config.BuffUpdateInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.updateGroupBuffs()
case <-m.stopChan:
return
}
}
}
// cleanupExpiredInvitesLoop periodically cleans up expired invites
func (m *Manager) cleanupExpiredInvitesLoop() {
defer m.wg.Done()
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.cleanupExpiredInvites()
case <-m.stopChan:
return
}
}
}
// updateStatsLoop periodically updates statistics
func (m *Manager) updateStatsLoop() {
defer m.wg.Done()
ticker := time.NewTicker(1 * time.Minute) // Update stats every minute
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.updateStatistics()
case <-m.stopChan:
return
}
}
}
// processGroupUpdates processes periodic group updates
func (m *Manager) processGroupUpdates() {
groups := m.GetAllGroups()
for _, group := range groups {
if !group.IsDisbanded() {
// Update member information
members := group.GetMembers()
for _, gmi := range members {
if gmi.Member != nil {
group.UpdateGroupMemberInfo(gmi.Member, false)
}
}
}
}
}
// updateGroupBuffs updates group buffs for all groups
func (m *Manager) updateGroupBuffs() {
// TODO: Implement group buff updates
// This would require integration with the spell/buff system
}
// cleanupExpiredInvites removes expired invitations
func (m *Manager) cleanupExpiredInvites() {
m.invitesMutex.Lock()
defer m.invitesMutex.Unlock()
now := time.Now()
expiredCount := 0
// Clean up regular invites
for key, invite := range m.PendingInvites {
if now.After(invite.ExpiresTime) {
delete(m.PendingInvites, key)
expiredCount++
}
}
// Clean up raid invites
for key, invite := range m.RaidPendingInvites {
if now.After(invite.ExpiresTime) {
delete(m.RaidPendingInvites, key)
expiredCount++
}
}
// Update statistics
if expiredCount > 0 {
m.statsMutex.Lock()
m.Stats.ExpiredInvites += int64(expiredCount)
m.statsMutex.Unlock()
}
}
// updateStatistics updates manager statistics
func (m *Manager) updateStatistics() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
activeGroups := m.MasterList.GetActiveGroups()
raidGroups := m.MasterList.GetRaidGroups()
var totalMembers int64
var raidMembers int64
for _, group := range activeGroups {
totalMembers += int64(group.GetSize())
if group.IsGroupRaid() {
raidMembers += int64(group.GetSize())
}
}
m.Stats.ActiveGroups = int64(len(activeGroups))
m.Stats.ActiveRaids = int64(len(raidGroups))
if len(activeGroups) > 0 {
m.Stats.AverageGroupSize = float64(totalMembers) / float64(len(activeGroups))
} else {
m.Stats.AverageGroupSize = 0
}
m.Stats.LastStatsUpdate = time.Now()
}
// Statistics update methods
// updateStatsForNewGroup updates statistics when a new group is created
func (m *Manager) updateStatsForNewGroup() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
m.Stats.TotalGroups++
}
// updateStatsForRemovedGroup updates statistics when a group is removed
func (m *Manager) updateStatsForRemovedGroup() {
// Statistics are primarily tracked in updateStatistics()
}
// updateStatsForInvite updates statistics when an invite is sent
func (m *Manager) updateStatsForInvite() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
m.Stats.TotalInvites++
}
// updateStatsForAcceptedInvite updates statistics when an invite is accepted
func (m *Manager) updateStatsForAcceptedInvite() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
m.Stats.AcceptedInvites++
}
// updateStatsForDeclinedInvite updates statistics when an invite is declined
func (m *Manager) updateStatsForDeclinedInvite() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
m.Stats.DeclinedInvites++
}
// updateStatsForExpiredInvite updates statistics when an invite expires
func (m *Manager) updateStatsForExpiredInvite() {
if !m.Config.EnableStatistics {
return
}
m.statsMutex.Lock()
defer m.statsMutex.Unlock()
m.Stats.ExpiredInvites++
}
// Event system integration
// AddEventHandler adds an event handler
func (m *Manager) AddEventHandler(handler GroupEventHandler) {
m.eventHandlersMutex.Lock()
defer m.eventHandlersMutex.Unlock()
m.EventHandlers = append(m.EventHandlers, handler)
}
// Integration interfaces
// SetDatabase sets the database interface
func (m *Manager) SetDatabase(db GroupDatabase) {
m.database = db
}
// SetPacketHandler sets the packet handler interface
func (m *Manager) SetPacketHandler(handler GroupPacketHandler) {
m.packetHandler = handler
}
// SetValidator sets the validator interface
func (m *Manager) SetValidator(validator GroupValidator) {
m.validator = validator
}
// SetNotifier sets the notifier interface
func (m *Manager) SetNotifier(notifier GroupNotifier) {
m.notifier = notifier
}
// Event firing methods
// fireGroupCreatedEvent fires a group created event
func (m *Manager) fireGroupCreatedEvent(group *Group, leader Entity) {
m.eventHandlersMutex.RLock()
defer m.eventHandlersMutex.RUnlock()
for _, handler := range m.EventHandlers {
go handler.OnGroupCreated(group, leader)
}
}
// fireGroupDisbandedEvent fires a group disbanded event
func (m *Manager) fireGroupDisbandedEvent(group *Group) {
m.eventHandlersMutex.RLock()
defer m.eventHandlersMutex.RUnlock()
for _, handler := range m.EventHandlers {
go handler.OnGroupDisbanded(group)
}
}
// fireGroupInviteSentEvent fires a group invite sent event
func (m *Manager) fireGroupInviteSentEvent(leader, member Entity) {
m.eventHandlersMutex.RLock()
defer m.eventHandlersMutex.RUnlock()
for _, handler := range m.EventHandlers {
go handler.OnGroupInviteSent(leader, member)
}
}
// fireGroupInviteAcceptedEvent fires a group invite accepted event
func (m *Manager) fireGroupInviteAcceptedEvent(leader, member Entity, groupID int32) {
m.eventHandlersMutex.RLock()
defer m.eventHandlersMutex.RUnlock()
for _, handler := range m.EventHandlers {
go handler.OnGroupInviteAccepted(leader, member, groupID)
}
}
// fireGroupInviteDeclinedEvent fires a group invite declined event
func (m *Manager) fireGroupInviteDeclinedEvent(leader, member Entity) {
m.eventHandlersMutex.RLock()
defer m.eventHandlersMutex.RUnlock()
for _, handler := range m.EventHandlers {
go handler.OnGroupInviteDeclined(leader, member)
}
}
// RemoveClientReference removes client references from all groups when a client disconnects
func (m *Manager) RemoveClientReference(client any) {
// Get all groups from the master list
groups := m.MasterList.GetAllGroups()
// Remove client reference from all groups
for _, group := range groups {
group.RemoveClientReference(client)
}
}

View File

@ -1,848 +0,0 @@
package groups
import (
"fmt"
"maps"
"strings"
"sync"
"time"
)
// MasterList manages groups with optimized lookups for:
// - Fast ID-based lookups (O(1))
// - Fast member-based lookups (indexed)
// - Fast zone-based filtering (indexed)
// - Fast size-based filtering (indexed)
// - Raid group management and lookups
// - Leader-based searching
// - Activity tracking and cleanup
type MasterList struct {
// Core storage
groups map[int32]*Group // ID -> Group
mutex sync.RWMutex
// Indices for O(1) lookups
byMember map[string]*Group // Member name -> group containing that member
byLeader map[string]*Group // Leader name -> group
byZone map[int32][]*Group // Zone ID -> groups with members in that zone
bySize map[int32][]*Group // Size -> groups of that size
activeGroups map[int32]*Group // Active (non-disbanded) groups
raidGroups map[int32]*Group // Groups that are part of raids
soloGroups map[int32]*Group // Single-member groups
fullGroups map[int32]*Group // Full groups (size = MAX_GROUP_SIZE)
// Activity tracking
byLastActivity map[time.Time][]*Group // Last activity time -> groups
// Cached metadata and slices
totalMembers int32 // Total active members across all groups
zones []int32 // Unique zones with group members
sizes []int32 // Unique group sizes
zoneStats map[int32]int // Zone ID -> group count
sizeStats map[int32]int // Size -> group count
allGroupsSlice []*Group // Cached slice of all groups
activeGroupsSlice []*Group // Cached slice of active groups
metaStale bool // Whether metadata cache needs refresh
}
// NewMasterList creates a new group master list
func NewMasterList() *MasterList {
return &MasterList{
groups: make(map[int32]*Group),
byMember: make(map[string]*Group),
byLeader: make(map[string]*Group),
byZone: make(map[int32][]*Group),
bySize: make(map[int32][]*Group),
activeGroups: make(map[int32]*Group),
raidGroups: make(map[int32]*Group),
soloGroups: make(map[int32]*Group),
fullGroups: make(map[int32]*Group),
byLastActivity: make(map[time.Time][]*Group),
zoneStats: make(map[int32]int),
sizeStats: make(map[int32]int),
allGroupsSlice: make([]*Group, 0),
activeGroupsSlice: make([]*Group, 0),
metaStale: true,
}
}
// refreshMetaCache updates the cached metadata
// Note: This function assumes the caller already holds ml.mutex.Lock()
func (ml *MasterList) refreshMetaCache() {
if !ml.metaStale {
return
}
// Clear and rebuild zone and size stats
ml.zoneStats = make(map[int32]int)
ml.sizeStats = make(map[int32]int)
zoneSet := make(map[int32]struct{})
sizeSet := make(map[int32]struct{})
ml.totalMembers = 0
// Get snapshot of active groups to avoid holding lock while calling group methods
activeGroupsSnapshot := make([]*Group, 0, len(ml.activeGroups))
for _, group := range ml.activeGroups {
activeGroupsSnapshot = append(activeGroupsSnapshot, group)
}
// Collect unique values and stats
for _, group := range activeGroupsSnapshot {
size := group.GetSize()
ml.sizeStats[size]++
sizeSet[size] = struct{}{}
ml.totalMembers += size
// Collect zones from group members
members := group.GetMembers()
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
ml.zoneStats[zoneID]++
zoneSet[zoneID] = struct{}{}
}
}
// Clear and rebuild cached slices
ml.zones = ml.zones[:0]
for zoneID := range zoneSet {
ml.zones = append(ml.zones, zoneID)
}
ml.sizes = ml.sizes[:0]
for size := range sizeSet {
ml.sizes = append(ml.sizes, size)
}
// Rebuild all groups slice
ml.allGroupsSlice = ml.allGroupsSlice[:0]
if cap(ml.allGroupsSlice) < len(ml.groups) {
ml.allGroupsSlice = make([]*Group, 0, len(ml.groups))
}
for _, group := range ml.groups {
ml.allGroupsSlice = append(ml.allGroupsSlice, group)
}
// Rebuild active groups slice
ml.activeGroupsSlice = ml.activeGroupsSlice[:0]
if cap(ml.activeGroupsSlice) < len(ml.activeGroups) {
ml.activeGroupsSlice = make([]*Group, 0, len(ml.activeGroups))
}
for _, group := range ml.activeGroups {
ml.activeGroupsSlice = append(ml.activeGroupsSlice, group)
}
ml.metaStale = false
}
// updateGroupIndices updates all indices for a group
func (ml *MasterList) updateGroupIndices(group *Group, add bool) {
// Get all group data in one go to minimize lock contention
// This avoids holding the master list lock while calling multiple group methods
groupID := group.GetID()
// Create a snapshot of group data to avoid repeated method calls
groupData := struct {
id int32
size int32
leaderName string
isRaid bool
isDisbanded bool
members []*GroupMemberInfo
}{
id: groupID,
size: group.GetSize(),
leaderName: group.GetLeaderName(),
isRaid: group.IsGroupRaid(),
isDisbanded: group.IsDisbanded(),
members: group.GetMembers(),
}
// Use the snapshot data for indexing
size := groupData.size
leaderName := groupData.leaderName
isRaid := groupData.isRaid
isDisbanded := groupData.isDisbanded
members := groupData.members
if add {
// Add to size index
ml.bySize[size] = append(ml.bySize[size], group)
// Add to leader index
if leaderName != "" {
ml.byLeader[strings.ToLower(leaderName)] = group
}
// Add to member index
for _, member := range members {
if member.Name != "" {
ml.byMember[strings.ToLower(member.Name)] = group
}
}
// Add to zone index
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
ml.byZone[zoneID] = append(ml.byZone[zoneID], group)
}
// Add to specialized indices
if !isDisbanded {
ml.activeGroups[groupID] = group
}
if isRaid {
ml.raidGroups[groupID] = group
}
if size == 1 {
ml.soloGroups[groupID] = group
}
if size == MAX_GROUP_SIZE {
ml.fullGroups[groupID] = group
}
// Add to activity index
activity := group.GetLastActivity()
ml.byLastActivity[activity] = append(ml.byLastActivity[activity], group)
} else {
// Remove from size index
sizeGroups := ml.bySize[size]
for i, g := range sizeGroups {
if g.GetID() == groupID {
ml.bySize[size] = append(sizeGroups[:i], sizeGroups[i+1:]...)
break
}
}
// Remove from leader index
if leaderName != "" {
delete(ml.byLeader, strings.ToLower(leaderName))
}
// Remove from member index
for _, member := range members {
if member.Name != "" {
delete(ml.byMember, strings.ToLower(member.Name))
}
}
// Remove from zone index
zoneMap := make(map[int32]struct{})
for _, member := range members {
if member.ZoneID > 0 {
zoneMap[member.ZoneID] = struct{}{}
}
}
for zoneID := range zoneMap {
zoneGroups := ml.byZone[zoneID]
for i, g := range zoneGroups {
if g.GetID() == groupID {
ml.byZone[zoneID] = append(zoneGroups[:i], zoneGroups[i+1:]...)
break
}
}
}
// Remove from specialized indices
delete(ml.activeGroups, groupID)
delete(ml.raidGroups, groupID)
delete(ml.soloGroups, groupID)
delete(ml.fullGroups, groupID)
// Remove from activity index
activity := group.GetLastActivity()
activityGroups := ml.byLastActivity[activity]
for i, g := range activityGroups {
if g.GetID() == groupID {
ml.byLastActivity[activity] = append(activityGroups[:i], activityGroups[i+1:]...)
break
}
}
}
}
// AddGroup adds a group with full indexing
func (ml *MasterList) AddGroup(group *Group) bool {
if group == nil {
return false
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
if _, exists := ml.groups[group.GetID()]; exists {
return false
}
// Add to core storage
ml.groups[group.GetID()] = group
// Update all indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
return true
}
// GetGroup retrieves by ID (O(1))
func (ml *MasterList) GetGroup(groupID int32) *Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.groups[groupID]
}
// GetGroupSafe retrieves a group by ID with existence check
func (ml *MasterList) GetGroupSafe(groupID int32) (*Group, bool) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
group, exists := ml.groups[groupID]
return group, exists
}
// HasGroup checks if a group exists by ID
func (ml *MasterList) HasGroup(groupID int32) bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
_, exists := ml.groups[groupID]
return exists
}
// RemoveGroup removes a group and updates all indices
func (ml *MasterList) RemoveGroup(groupID int32) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
group, exists := ml.groups[groupID]
if !exists {
return false
}
// Remove from core storage
delete(ml.groups, groupID)
// Update all indices
ml.updateGroupIndices(group, false)
// Invalidate metadata cache
ml.metaStale = true
return true
}
// GetAllGroups returns all groups as a slice
func (ml *MasterList) GetAllGroups() []*Group {
// Use read lock first to check if we need to refresh
ml.mutex.RLock()
needsRefresh := ml.metaStale
if !needsRefresh {
// Return cached result without upgrading to write lock
result := make([]*Group, len(ml.allGroupsSlice))
copy(result, ml.allGroupsSlice)
ml.mutex.RUnlock()
return result
}
ml.mutex.RUnlock()
// Need to refresh - acquire write lock
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Double-check pattern - someone else might have refreshed while we waited
if ml.metaStale {
ml.refreshMetaCache()
}
// Return a copy to prevent external modification
result := make([]*Group, len(ml.allGroupsSlice))
copy(result, ml.allGroupsSlice)
return result
}
// GetAllGroupsMap returns a copy of all groups map
func (ml *MasterList) GetAllGroupsMap() map[int32]*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Group, len(ml.groups))
maps.Copy(result, ml.groups)
return result
}
// GetGroupCount returns the total number of groups
func (ml *MasterList) GetGroupCount() int32 {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return int32(len(ml.groups))
}
// Size returns the total number of groups
func (ml *MasterList) Size() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.groups)
}
// IsEmpty returns true if the master list is empty
func (ml *MasterList) IsEmpty() bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.groups) == 0
}
// Clear removes all groups from the list
func (ml *MasterList) Clear() {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Clear all maps
ml.groups = make(map[int32]*Group)
ml.byMember = make(map[string]*Group)
ml.byLeader = make(map[string]*Group)
ml.byZone = make(map[int32][]*Group)
ml.bySize = make(map[int32][]*Group)
ml.activeGroups = make(map[int32]*Group)
ml.raidGroups = make(map[int32]*Group)
ml.soloGroups = make(map[int32]*Group)
ml.fullGroups = make(map[int32]*Group)
ml.byLastActivity = make(map[time.Time][]*Group)
// Clear cached metadata
ml.zones = ml.zones[:0]
ml.sizes = ml.sizes[:0]
ml.allGroupsSlice = ml.allGroupsSlice[:0]
ml.activeGroupsSlice = ml.activeGroupsSlice[:0]
ml.zoneStats = make(map[int32]int)
ml.sizeStats = make(map[int32]int)
ml.totalMembers = 0
ml.metaStale = true
}
// GetGroupsByFilter returns groups matching the filter function
func (ml *MasterList) GetGroupsByFilter(filter func(*Group) bool) []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var result []*Group
for _, group := range ml.groups {
if filter(group) {
result = append(result, group)
}
}
return result
}
// GetGroupByMember returns the group containing the specified member (O(1))
func (ml *MasterList) GetGroupByMember(memberName string) *Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byMember[strings.ToLower(memberName)]
}
// GetGroupByLeader returns the group led by the specified leader (O(1))
func (ml *MasterList) GetGroupByLeader(leaderName string) *Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byLeader[strings.ToLower(leaderName)]
}
// GetActiveGroups returns all non-disbanded groups (O(1))
func (ml *MasterList) GetActiveGroups() []*Group {
// Use read lock first to check if we need to refresh
ml.mutex.RLock()
needsRefresh := ml.metaStale
if !needsRefresh {
// Return cached result without upgrading to write lock
result := make([]*Group, len(ml.activeGroupsSlice))
copy(result, ml.activeGroupsSlice)
ml.mutex.RUnlock()
return result
}
ml.mutex.RUnlock()
// Need to refresh - acquire write lock
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Double-check pattern - someone else might have refreshed while we waited
if ml.metaStale {
ml.refreshMetaCache()
}
// Return a copy to prevent external modification
result := make([]*Group, len(ml.activeGroupsSlice))
copy(result, ml.activeGroupsSlice)
return result
}
// GetGroupsByZone returns groups with members in the specified zone (O(1))
func (ml *MasterList) GetGroupsByZone(zoneID int32) []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
groups := ml.byZone[zoneID]
if groups == nil {
return []*Group{}
}
// Return a copy to prevent external modification
result := make([]*Group, len(groups))
copy(result, groups)
return result
}
// GetGroupsBySize returns groups of the specified size (O(1))
func (ml *MasterList) GetGroupsBySize(size int32) []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
groups := ml.bySize[size]
if groups == nil {
return []*Group{}
}
// Return a copy to prevent external modification
result := make([]*Group, len(groups))
copy(result, groups)
return result
}
// GetRaidGroups returns all groups that are part of raids (O(1))
func (ml *MasterList) GetRaidGroups() []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Group, 0, len(ml.raidGroups))
for _, group := range ml.raidGroups {
result = append(result, group)
}
return result
}
// GetSoloGroups returns all groups with only one member (O(1))
func (ml *MasterList) GetSoloGroups() []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Group, 0, len(ml.soloGroups))
for _, group := range ml.soloGroups {
result = append(result, group)
}
return result
}
// GetFullGroups returns all groups at maximum capacity (O(1))
func (ml *MasterList) GetFullGroups() []*Group {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Group, 0, len(ml.fullGroups))
for _, group := range ml.fullGroups {
result = append(result, group)
}
return result
}
// GetGroupsByLeader returns groups led by entities with the specified name
func (ml *MasterList) GetGroupsByLeader(leaderName string) []*Group {
group := ml.GetGroupByLeader(leaderName)
if group != nil {
return []*Group{group}
}
return []*Group{}
}
// GetGroupsByMember returns groups containing a member with the specified name
func (ml *MasterList) GetGroupsByMember(memberName string) []*Group {
group := ml.GetGroupByMember(memberName)
if group != nil {
return []*Group{group}
}
return []*Group{}
}
// GetZones returns all unique zones with group members using cached results
func (ml *MasterList) GetZones() []int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]int32, len(ml.zones))
copy(result, ml.zones)
return result
}
// GetSizes returns all unique group sizes using cached results
func (ml *MasterList) GetSizes() []int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]int32, len(ml.sizes))
copy(result, ml.sizes)
return result
}
// GetTotalMembers returns the total number of active members across all groups
func (ml *MasterList) GetTotalMembers() int32 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
return ml.totalMembers
}
// GetGroupStatistics returns statistics about the groups in the master list using cached data
func (ml *MasterList) GetGroupStatistics() *GroupMasterListStats {
// Use read lock first to check if we need to refresh
ml.mutex.RLock()
needsRefresh := ml.metaStale
if !needsRefresh {
// Calculate stats from cached data
var totalRaidMembers int32
for _, group := range ml.raidGroups {
totalRaidMembers += group.GetSize()
}
var averageGroupSize float64
if len(ml.activeGroups) > 0 {
averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups))
}
stats := &GroupMasterListStats{
TotalGroups: int32(len(ml.groups)),
ActiveGroups: int32(len(ml.activeGroups)),
RaidGroups: int32(len(ml.raidGroups)),
TotalMembers: ml.totalMembers,
TotalRaidMembers: totalRaidMembers,
AverageGroupSize: averageGroupSize,
SoloGroups: int32(len(ml.soloGroups)),
FullGroups: int32(len(ml.fullGroups)),
}
ml.mutex.RUnlock()
return stats
}
ml.mutex.RUnlock()
// Need to refresh - acquire write lock
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Double-check pattern
if ml.metaStale {
ml.refreshMetaCache()
}
var totalRaidMembers int32
for _, group := range ml.raidGroups {
totalRaidMembers += group.GetSize()
}
var averageGroupSize float64
if len(ml.activeGroups) > 0 {
averageGroupSize = float64(ml.totalMembers) / float64(len(ml.activeGroups))
}
return &GroupMasterListStats{
TotalGroups: int32(len(ml.groups)),
ActiveGroups: int32(len(ml.activeGroups)),
RaidGroups: int32(len(ml.raidGroups)),
TotalMembers: ml.totalMembers,
TotalRaidMembers: totalRaidMembers,
AverageGroupSize: averageGroupSize,
SoloGroups: int32(len(ml.soloGroups)),
FullGroups: int32(len(ml.fullGroups)),
}
}
// GroupMasterListStats holds statistics about the groups master list
type GroupMasterListStats struct {
TotalGroups int32 `json:"total_groups"`
ActiveGroups int32 `json:"active_groups"`
RaidGroups int32 `json:"raid_groups"`
TotalMembers int32 `json:"total_members"`
TotalRaidMembers int32 `json:"total_raid_members"`
AverageGroupSize float64 `json:"average_group_size"`
SoloGroups int32 `json:"solo_groups"`
FullGroups int32 `json:"full_groups"`
}
// RefreshGroupIndices refreshes indices for a group (used when group state changes)
func (ml *MasterList) RefreshGroupIndices(group *Group) {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Remove from old indices
ml.updateGroupIndices(group, false)
// Add to new indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
}
// UpdateGroup updates an existing group and refreshes indices
func (ml *MasterList) UpdateGroup(group *Group) error {
if group == nil {
return fmt.Errorf("group cannot be nil")
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
old, exists := ml.groups[group.GetID()]
if !exists {
return fmt.Errorf("group %d not found", group.GetID())
}
// Remove old group from indices (but not core storage yet)
ml.updateGroupIndices(old, false)
// Update core storage
ml.groups[group.GetID()] = group
// Add new group to indices
ml.updateGroupIndices(group, true)
// Invalidate metadata cache
ml.metaStale = true
return nil
}
// ForEach executes a function for each group
func (ml *MasterList) ForEach(fn func(int32, *Group)) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
for id, group := range ml.groups {
fn(id, group)
}
}
// Cleanup removes disbanded groups from the master list
func (ml *MasterList) Cleanup() int32 {
ml.mutex.Lock()
defer ml.mutex.Unlock()
removed := int32(0)
for id, group := range ml.groups {
if group.IsDisbanded() {
// Remove from core storage
delete(ml.groups, id)
// Remove from indices (group is already disbanded, so activeGroups won't include it)
ml.updateGroupIndices(group, false)
removed++
}
}
if removed > 0 {
// Invalidate metadata cache
ml.metaStale = true
}
return removed
}
// ValidateAll validates all groups in the master list
func (ml *MasterList) ValidateAll() []error {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var errors []error
for id, group := range ml.groups {
if group == nil {
errors = append(errors, fmt.Errorf("group ID %d is nil", id))
continue
}
// Check for basic validity
if group.GetID() != id {
errors = append(errors, fmt.Errorf("group ID mismatch: map key %d != group ID %d", id, group.GetID()))
}
if group.GetID() <= 0 {
errors = append(errors, fmt.Errorf("group %d has invalid ID", group.GetID()))
}
if group.GetSize() == 0 && !group.IsDisbanded() {
errors = append(errors, fmt.Errorf("group %d is empty but not disbanded", group.GetID()))
}
if group.GetSize() > MAX_GROUP_SIZE {
errors = append(errors, fmt.Errorf("group %d exceeds maximum size (%d > %d)",
group.GetID(), group.GetSize(), MAX_GROUP_SIZE))
}
// Check for leader
members := group.GetMembers()
hasLeader := false
leaderCount := 0
for _, member := range members {
if member.Leader {
hasLeader = true
leaderCount++
}
}
if !hasLeader && !group.IsDisbanded() {
errors = append(errors, fmt.Errorf("group %d has no leader", group.GetID()))
}
if leaderCount > 1 {
errors = append(errors, fmt.Errorf("group %d has multiple leaders (%d)", group.GetID(), leaderCount))
}
// Validate index consistency
if !group.IsDisbanded() {
if _, exists := ml.activeGroups[id]; !exists {
errors = append(errors, fmt.Errorf("active group %d not found in activeGroups index", id))
}
}
if group.IsGroupRaid() {
if _, exists := ml.raidGroups[id]; !exists {
errors = append(errors, fmt.Errorf("raid group %d not found in raidGroups index", id))
}
}
if group.GetSize() == 1 {
if _, exists := ml.soloGroups[id]; !exists {
errors = append(errors, fmt.Errorf("solo group %d not found in soloGroups index", id))
}
}
if group.GetSize() == MAX_GROUP_SIZE {
if _, exists := ml.fullGroups[id]; !exists {
errors = append(errors, fmt.Errorf("full group %d not found in fullGroups index", id))
}
}
}
return errors
}
// IsValid returns true if all groups are valid
func (ml *MasterList) IsValid() bool {
errors := ml.ValidateAll()
return len(errors) == 0
}

View File

@ -1,257 +0,0 @@
package groups
import (
"sync"
"time"
)
// GroupOptions holds group configuration settings
type GroupOptions struct {
LootMethod int8 `json:"loot_method" db:"loot_method"`
LootItemsRarity int8 `json:"loot_items_rarity" db:"loot_items_rarity"`
AutoSplit int8 `json:"auto_split" db:"auto_split"`
DefaultYell int8 `json:"default_yell" db:"default_yell"`
GroupLockMethod int8 `json:"group_lock_method" db:"group_lock_method"`
GroupAutolock int8 `json:"group_autolock" db:"group_autolock"`
SoloAutolock int8 `json:"solo_autolock" db:"solo_autolock"`
AutoLootMethod int8 `json:"auto_loot_method" db:"auto_loot_method"`
LastLootedIndex int8 `json:"last_looted_index" db:"last_looted_index"`
}
// GroupMemberInfo contains all information about a group member
type GroupMemberInfo struct {
// Group and member identification
GroupID int32 `json:"group_id" db:"group_id"`
Name string `json:"name" db:"name"`
Zone string `json:"zone" db:"zone"`
// Health and power stats
HPCurrent int32 `json:"hp_current" db:"hp_current"`
HPMax int32 `json:"hp_max" db:"hp_max"`
PowerCurrent int32 `json:"power_current" db:"power_current"`
PowerMax int32 `json:"power_max" db:"power_max"`
// Level and character info
LevelCurrent int16 `json:"level_current" db:"level_current"`
LevelMax int16 `json:"level_max" db:"level_max"`
RaceID int8 `json:"race_id" db:"race_id"`
ClassID int8 `json:"class_id" db:"class_id"`
// Group status
Leader bool `json:"leader" db:"leader"`
IsClient bool `json:"is_client" db:"is_client"`
IsRaidLooter bool `json:"is_raid_looter" db:"is_raid_looter"`
// Zone and instance info
ZoneID int32 `json:"zone_id" db:"zone_id"`
InstanceID int32 `json:"instance_id" db:"instance_id"`
// Mentoring
MentorTargetCharID int32 `json:"mentor_target_char_id" db:"mentor_target_char_id"`
// Network info for cross-server groups
ClientPeerAddress string `json:"client_peer_address" db:"client_peer_address"`
ClientPeerPort int16 `json:"client_peer_port" db:"client_peer_port"`
// Entity reference (local members only)
Member Entity `json:"-"`
// Client reference (players only) - interface to avoid circular deps
Client any `json:"-"`
// Timestamps
JoinTime time.Time `json:"join_time" db:"join_time"`
LastUpdate time.Time `json:"last_update" db:"last_update"`
}
// Group is now defined in group.go - this type definition removed to avoid duplication
// GroupMessage represents a message sent to the group
type GroupMessage struct {
Type int8 `json:"type"`
Channel int16 `json:"channel"`
Message string `json:"message"`
FromName string `json:"from_name"`
Language int32 `json:"language"`
Timestamp time.Time `json:"timestamp"`
ExcludeClient any `json:"-"`
}
// GroupUpdate represents a group update event
type GroupUpdate struct {
Type int8 `json:"type"`
GroupID int32 `json:"group_id"`
MemberInfo *GroupMemberInfo `json:"member_info,omitempty"`
Options *GroupOptions `json:"options,omitempty"`
RaidGroups []int32 `json:"raid_groups,omitempty"`
ForceRaidUpdate bool `json:"force_raid_update"`
ExcludeClient any `json:"-"`
Timestamp time.Time `json:"timestamp"`
}
// GroupInvite represents a pending group invitation
type GroupInvite struct {
InviterName string `json:"inviter_name"`
InviteeName string `json:"invitee_name"`
GroupID int32 `json:"group_id"`
IsRaidInvite bool `json:"is_raid_invite"`
CreatedTime time.Time `json:"created_time"`
ExpiresTime time.Time `json:"expires_time"`
}
// GroupManager manages all player groups
type GroupManager struct {
// Group storage
groups map[int32]*Group
groupsMutex sync.RWMutex
// Group ID generation
nextGroupID int32
nextGroupIDMutex sync.Mutex
// Pending invitations
pendingInvites map[string]*GroupInvite
raidPendingInvites map[string]*GroupInvite
invitesMutex sync.RWMutex
// Event handlers
eventHandlers []GroupEventHandler
eventHandlersMutex sync.RWMutex
// Configuration
config GroupManagerConfig
// Statistics
stats GroupManagerStats
statsMutex sync.RWMutex
// Background processing
stopChan chan struct{}
wg sync.WaitGroup
// Integration interfaces
database GroupDatabase
packetHandler GroupPacketHandler
validator GroupValidator
notifier GroupNotifier
}
// GroupManagerConfig holds configuration for the group manager
type GroupManagerConfig struct {
MaxGroups int32 `json:"max_groups"`
MaxRaidGroups int32 `json:"max_raid_groups"`
InviteTimeout time.Duration `json:"invite_timeout"`
UpdateInterval time.Duration `json:"update_interval"`
BuffUpdateInterval time.Duration `json:"buff_update_interval"`
EnableCrossServer bool `json:"enable_cross_server"`
EnableRaids bool `json:"enable_raids"`
EnableQuestSharing bool `json:"enable_quest_sharing"`
EnableAutoInvite bool `json:"enable_auto_invite"`
EnableStatistics bool `json:"enable_statistics"`
}
// GroupManagerStats holds statistics about group management
type GroupManagerStats struct {
TotalGroups int64 `json:"total_groups"`
ActiveGroups int64 `json:"active_groups"`
TotalRaids int64 `json:"total_raids"`
ActiveRaids int64 `json:"active_raids"`
TotalInvites int64 `json:"total_invites"`
AcceptedInvites int64 `json:"accepted_invites"`
DeclinedInvites int64 `json:"declined_invites"`
ExpiredInvites int64 `json:"expired_invites"`
AverageGroupSize float64 `json:"average_group_size"`
AverageGroupDuration time.Duration `json:"average_group_duration"`
LastStatsUpdate time.Time `json:"last_stats_update"`
}
// Default group options
func DefaultGroupOptions() GroupOptions {
return GroupOptions{
LootMethod: LOOT_METHOD_ROUND_ROBIN,
LootItemsRarity: LOOT_RARITY_COMMON,
AutoSplit: AUTO_SPLIT_DISABLED,
DefaultYell: DEFAULT_YELL_DISABLED,
GroupLockMethod: LOCK_METHOD_OPEN,
GroupAutolock: AUTO_LOCK_DISABLED,
SoloAutolock: AUTO_LOCK_DISABLED,
AutoLootMethod: AUTO_LOOT_DISABLED,
LastLootedIndex: 0,
}
}
// Copy creates a copy of GroupMemberInfo
func (gmi *GroupMemberInfo) Copy() *GroupMemberInfo {
copy := *gmi
return &copy
}
// IsValid checks if the group member info is valid
func (gmi *GroupMemberInfo) IsValid() bool {
return gmi.GroupID > 0 && len(gmi.Name) > 0
}
// UpdateStats updates member stats from entity
func (gmi *GroupMemberInfo) UpdateStats() {
if gmi.Member == nil {
return
}
entity := gmi.Member
gmi.HPCurrent = entity.GetHP()
gmi.HPMax = entity.GetTotalHP()
gmi.PowerCurrent = entity.GetPower()
gmi.PowerMax = entity.GetTotalPower()
gmi.LevelCurrent = int16(entity.GetLevel())
gmi.LevelMax = int16(entity.GetLevel()) // TODO: Get actual max level
gmi.LastUpdate = time.Now()
// Update zone info if entity has zone
if zone := entity.GetZone(); zone != nil {
gmi.ZoneID = zone.GetZoneID()
gmi.InstanceID = zone.GetInstanceID()
gmi.Zone = zone.GetZoneName()
}
}
// Copy creates a copy of GroupOptions
func (go_opts *GroupOptions) Copy() GroupOptions {
return *go_opts
}
// IsValid checks if group options are valid
func (go_opts *GroupOptions) IsValid() bool {
return go_opts.LootMethod >= LOOT_METHOD_LEADER_ONLY && go_opts.LootMethod <= LOOT_METHOD_LOTTO &&
go_opts.LootItemsRarity >= LOOT_RARITY_COMMON && go_opts.LootItemsRarity <= LOOT_RARITY_FABLED
}
// NewGroupMessage creates a new group message
func NewGroupMessage(msgType int8, channel int16, message, fromName string, language int32) *GroupMessage {
return &GroupMessage{
Type: msgType,
Channel: channel,
Message: message,
FromName: fromName,
Language: language,
Timestamp: time.Now(),
}
}
// NewGroupUpdate creates a new group update
func NewGroupUpdate(updateType int8, groupID int32) *GroupUpdate {
return &GroupUpdate{
Type: updateType,
GroupID: groupID,
Timestamp: time.Now(),
}
}
// IsExpired checks if the group invite has expired
func (gi *GroupInvite) IsExpired() bool {
return time.Now().After(gi.ExpiresTime)
}
// TimeRemaining returns the remaining time for the invite
func (gi *GroupInvite) TimeRemaining() time.Duration {
return time.Until(gi.ExpiresTime)
}

View File

@ -136,6 +136,17 @@ const (
OP_EntityVerbsReplyMsg OP_EntityVerbsReplyMsg
OP_EntityVerbsVerbMsg OP_EntityVerbsVerbMsg
// Group system opcodes
OP_GroupUpdateMsg
OP_RaidUpdateMsg
OP_GroupInviteMsg
OP_GroupDeclineMsg
OP_GroupChatMsg
OP_GroupMessageMsg
OP_GroupOptionsMsg
OP_DefaultGroupOptionsMsg
OP_DefaultGroupOptionsRequestMsg
// Add more opcodes as needed... // Add more opcodes as needed...
_maxInternalOpcode // Sentinel value _maxInternalOpcode // Sentinel value
) )
@ -231,6 +242,17 @@ var OpcodeNames = map[InternalOpcode]string{
OP_EntityVerbsRequestMsg: "OP_EntityVerbsRequestMsg", OP_EntityVerbsRequestMsg: "OP_EntityVerbsRequestMsg",
OP_EntityVerbsReplyMsg: "OP_EntityVerbsReplyMsg", OP_EntityVerbsReplyMsg: "OP_EntityVerbsReplyMsg",
OP_EntityVerbsVerbMsg: "OP_EntityVerbsVerbMsg", OP_EntityVerbsVerbMsg: "OP_EntityVerbsVerbMsg",
// Group system opcodes
OP_GroupUpdateMsg: "OP_GroupUpdateMsg",
OP_RaidUpdateMsg: "OP_RaidUpdateMsg",
OP_GroupInviteMsg: "OP_GroupInviteMsg",
OP_GroupDeclineMsg: "OP_GroupDeclineMsg",
OP_GroupChatMsg: "OP_GroupChatMsg",
OP_GroupMessageMsg: "OP_GroupMessageMsg",
OP_GroupOptionsMsg: "OP_GroupOptionsMsg",
OP_DefaultGroupOptionsMsg: "OP_DefaultGroupOptionsMsg",
OP_DefaultGroupOptionsRequestMsg: "OP_DefaultGroupOptionsRequestMsg",
} }
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes // OpcodeManager handles the mapping between client-specific opcodes and internal opcodes