package guilds import ( "fmt" "sync" "time" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Entity represents a game entity that can be in guilds (simplified interface) type Entity interface { GetID() int32 GetName() string GetLevel() int8 GetClass() int8 IsPlayer() bool IsOnline() bool } // Client represents a connected client (simplified interface) type Client interface { GetVersion() uint32 SendPacket(opcode packets.InternalOpcode, data []byte) error GetCharacterID() int32 } // Logger interface for logging operations type Logger interface { Debug(msg string, args ...any) Info(msg string, args ...any) Error(msg string, args ...any) } // PointHistory represents a point modification entry in a member's history type PointHistory struct { Date time.Time `json:"date" db:"date"` ModifiedBy string `json:"modified_by" db:"modified_by"` Comment string `json:"comment" db:"comment"` Points float64 `json:"points" db:"points"` SaveNeeded bool `json:"-" db:"-"` } // GuildMember represents a member of a guild (consolidated from multiple files) type GuildMember struct { CharacterID int32 `json:"character_id" db:"character_id"` AccountID int32 `json:"account_id" db:"account_id"` RecruiterID int32 `json:"recruiter_id" db:"recruiter_id"` Name string `json:"name" db:"name"` GuildStatus int32 `json:"guild_status" db:"guild_status"` Points float64 `json:"points" db:"points"` AdventureClass int8 `json:"adventure_class" db:"adventure_class"` AdventureLevel int8 `json:"adventure_level" db:"adventure_level"` TradeskillClass int8 `json:"tradeskill_class" db:"tradeskill_class"` TradeskillLevel int8 `json:"tradeskill_level" db:"tradeskill_level"` Rank int8 `json:"rank" db:"rank"` MemberFlags int8 `json:"member_flags" db:"member_flags"` Zone string `json:"zone" db:"zone"` JoinDate time.Time `json:"join_date" db:"join_date"` LastLoginDate time.Time `json:"last_login_date" db:"last_login_date"` Note string `json:"note" db:"note"` OfficerNote string `json:"officer_note" db:"officer_note"` RecruiterDescription string `json:"recruiter_description" db:"recruiter_description"` RecruiterPictureData []byte `json:"recruiter_picture_data" db:"recruiter_picture_data"` RecruitingShowAdventureClass int8 `json:"recruiting_show_adventure_class" db:"recruiting_show_adventure_class"` PointHistory []PointHistory `json:"point_history" db:"-"` // Runtime data Entity Entity `json:"-" db:"-"` Client Client `json:"-" db:"-"` LastUpdate time.Time `json:"-" db:"-"` SaveNeeded bool `json:"-" db:"-"` } // GuildEvent represents an event in the guild's history type GuildEvent struct { EventID int64 `json:"event_id" db:"event_id"` Date time.Time `json:"date" db:"date"` Type int32 `json:"type" db:"type"` Description string `json:"description" db:"description"` Locked int8 `json:"locked" db:"locked"` SaveNeeded bool `json:"-" db:"-"` } // GuildBankEvent represents an event in a guild bank's history type GuildBankEvent struct { EventID int64 `json:"event_id" db:"event_id"` Date time.Time `json:"date" db:"date"` Type int32 `json:"type" db:"type"` Description string `json:"description" db:"description"` } // Bank represents a guild bank with its event history type Bank struct { Name string `json:"name" db:"name"` Events []GuildBankEvent `json:"events" db:"-"` } // GuildInvite represents a pending guild invitation type GuildInvite struct { GuildID int32 `json:"guild_id"` GuildName string `json:"guild_name"` CharacterID int32 `json:"character_id"` CharacterName string `json:"character_name"` InviterID int32 `json:"inviter_id"` InviterName string `json:"inviter_name"` Rank int8 `json:"rank"` InviteDate time.Time `json:"invite_date"` ExpiresAt time.Time `json:"expires_at"` } // Guild represents a guild with all its properties and members (consolidated from multiple files) type Guild struct { // Core guild properties id int32 name string level int8 formedDate time.Time motd string expCurrent int64 expToNextLevel int64 recruitingShortDesc string recruitingFullDesc string recruitingMinLevel int8 recruitingPlayStyle int8 // Guild data structures members map[int32]*GuildMember guildEvents []GuildEvent permissions map[int8]map[int8]int8 // rank -> permission -> value eventFilters map[int8]map[int8]int8 // event_id -> category -> value recruitingFlags map[int8]int8 recruitingDescTags map[int8]int8 ranks map[int8]string // rank -> name banks [4]Bank // Guild hall information guildHallLocation string guildHallZoneName string guildHallFilename string // Tracking nextEventID int64 lastModified time.Time // Save flags saveNeeded bool memberSaveNeeded bool eventsSaveNeeded bool ranksSaveNeeded bool eventFiltersSaveNeeded bool pointsHistorySaveNeeded bool recruitingSaveNeeded bool // Thread safety membersMutex sync.RWMutex eventsMutex sync.RWMutex ranksMutex sync.RWMutex permissionsMutex sync.RWMutex filtersMutex sync.RWMutex recruitingMutex sync.RWMutex coreMutex sync.RWMutex } // GuildManager manages all guilds in the system (consolidated from multiple files) type GuildManager struct { // Guild storage guilds map[int32]*Guild guildsMutex sync.RWMutex // Guild ID generation nextGuildID int32 nextGuildIDMutex sync.Mutex // Pending invitations pendingInvites map[int32]*GuildInvite // character_id -> invite invitesMutex sync.RWMutex // Statistics (simplified) stats GuildManagerStats statsMutex sync.RWMutex // Dependencies database *database.Database logger Logger } // GuildManagerStats holds essential guild management statistics type GuildManagerStats struct { TotalGuilds int64 `json:"total_guilds"` ActiveGuilds int64 `json:"active_guilds"` TotalMembers int64 `json:"total_members"` TotalEvents int64 `json:"total_events"` TotalRecruiters int64 `json:"total_recruiters"` UniqueAccounts int64 `json:"unique_accounts"` 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"` } // GuildInfo provides basic guild information type GuildInfo struct { ID int32 `json:"id"` Name string `json:"name"` Level int8 `json:"level"` FormedDate time.Time `json:"formed_date"` MOTD string `json:"motd"` MemberCount int `json:"member_count"` OnlineMemberCount int `json:"online_member_count"` RecruiterCount int `json:"recruiter_count"` RecruitingShortDesc string `json:"recruiting_short_desc"` RecruitingFullDesc string `json:"recruiting_full_desc"` RecruitingMinLevel int8 `json:"recruiting_min_level"` RecruitingPlayStyle int8 `json:"recruiting_play_style"` IsRecruiting bool `json:"is_recruiting"` UniqueAccounts int `json:"unique_accounts"` ExpCurrent int64 `json:"exp_current"` ExpToNextLevel int64 `json:"exp_to_next_level"` } // NewGuildManager creates a new guild manager func NewGuildManager(db *database.Database, logger Logger) *GuildManager { if logger == nil { logger = &nullLogger{} } return &GuildManager{ guilds: make(map[int32]*Guild), nextGuildID: 1, pendingInvites: make(map[int32]*GuildInvite), database: db, logger: logger, stats: GuildManagerStats{LastStatsUpdate: time.Now()}, } } // Guild creation and management (preserving C++ API) // CreateGuild creates a new guild with the specified leader and name func (gm *GuildManager) CreateGuild(leaderID int32, guildName string) (*Guild, error) { if len(guildName) == 0 || len(guildName) > MaxGuildNameLength { return nil, fmt.Errorf("invalid guild name length") } // Check if guild name already exists if gm.GuildNameExists(guildName) { return nil, fmt.Errorf("guild name already exists") } // Generate guild ID gm.nextGuildIDMutex.Lock() guildID := gm.nextGuildID gm.nextGuildID++ gm.nextGuildIDMutex.Unlock() // Create the guild guild := &Guild{ id: guildID, name: guildName, level: 1, formedDate: time.Now(), motd: "", expCurrent: 111, expToNextLevel: 2521, recruitingMinLevel: 1, members: make(map[int32]*GuildMember), guildEvents: make([]GuildEvent, 0), permissions: make(map[int8]map[int8]int8), eventFilters: make(map[int8]map[int8]int8), recruitingFlags: make(map[int8]int8), recruitingDescTags: make(map[int8]int8), ranks: make(map[int8]string), nextEventID: 1, lastModified: time.Now(), saveNeeded: true, } // Initialize default recruiting flags guild.recruitingFlags[RecruitingFlagTraining] = 0 guild.recruitingFlags[RecruitingFlagFighters] = 0 guild.recruitingFlags[RecruitingFlagPriests] = 0 guild.recruitingFlags[RecruitingFlagScouts] = 0 guild.recruitingFlags[RecruitingFlagMages] = 0 guild.recruitingFlags[RecruitingFlagTradeskillers] = 0 // Initialize default description tags guild.recruitingDescTags[0] = RecruitingDescTagNone guild.recruitingDescTags[1] = RecruitingDescTagNone guild.recruitingDescTags[2] = RecruitingDescTagNone guild.recruitingDescTags[3] = RecruitingDescTagNone // Initialize default bank names guild.banks[0].Name = "Bank 1" guild.banks[1].Name = "Bank 2" guild.banks[2].Name = "Bank 3" guild.banks[3].Name = "Bank 4" // Initialize default rank names for rank, name := range DefaultRankNames { guild.ranks[rank] = name } // Add leader as first member leader := &GuildMember{ CharacterID: leaderID, Rank: RankLeader, JoinDate: time.Now(), LastLoginDate: time.Now(), PointHistory: make([]PointHistory, 0), SaveNeeded: true, } guild.members[leaderID] = leader guild.memberSaveNeeded = true // Add guild to manager gm.guildsMutex.Lock() gm.guilds[guildID] = guild gm.guildsMutex.Unlock() // Add formation event guild.AddNewGuildEvent(EventGuildFormed, fmt.Sprintf("%s formed the guild", leader.Name), time.Now(), true) // Update statistics gm.statsMutex.Lock() gm.stats.TotalGuilds++ gm.stats.ActiveGuilds++ gm.stats.TotalMembers++ gm.stats.LastStatsUpdate = time.Now() gm.statsMutex.Unlock() gm.logger.Info("Created new guild", "guild_id", guildID, "name", guildName, "leader_id", leaderID) return guild, nil } // DeleteGuild removes a guild from the manager func (gm *GuildManager) DeleteGuild(guildID int32, reason string) error { gm.guildsMutex.Lock() guild, exists := gm.guilds[guildID] if !exists { gm.guildsMutex.Unlock() return fmt.Errorf("guild not found: %d", guildID) } delete(gm.guilds, guildID) gm.guildsMutex.Unlock() // Add disbanding event guild.AddNewGuildEvent(EventGuildDisbanded, reason, time.Now(), true) // Clear all members guild.membersMutex.Lock() memberCount := len(guild.members) guild.members = make(map[int32]*GuildMember) guild.membersMutex.Unlock() // Update statistics gm.statsMutex.Lock() gm.stats.ActiveGuilds-- gm.stats.TotalMembers -= int64(memberCount) gm.stats.LastStatsUpdate = time.Now() gm.statsMutex.Unlock() gm.logger.Info("Deleted guild", "guild_id", guildID, "reason", reason) return nil } // GetGuild retrieves a guild by ID func (gm *GuildManager) GetGuild(guildID int32) *Guild { gm.guildsMutex.RLock() defer gm.guildsMutex.RUnlock() return gm.guilds[guildID] } // GetGuildByName retrieves a guild by name func (gm *GuildManager) GetGuildByName(name string) *Guild { gm.guildsMutex.RLock() defer gm.guildsMutex.RUnlock() for _, guild := range gm.guilds { if guild.GetName() == name { return guild } } return nil } // GuildNameExists checks if a guild name already exists func (gm *GuildManager) GuildNameExists(name string) bool { return gm.GetGuildByName(name) != nil } // GetPlayerGuild returns the guild for a player character func (gm *GuildManager) GetPlayerGuild(characterID int32) *Guild { gm.guildsMutex.RLock() defer gm.guildsMutex.RUnlock() for _, guild := range gm.guilds { if guild.HasMember(characterID) { return guild } } return nil } // Guild invitation system (preserving C++ API) // InvitePlayerToGuild invites a player to join a guild func (gm *GuildManager) InvitePlayerToGuild(guildID, inviterID, characterID int32, rank int8) int8 { guild := gm.GetGuild(guildID) if guild == nil { return GUILD_INVITE_GUILD_NOT_FOUND } // Check if inviter has permission inviter := guild.GetMember(inviterID) if inviter == nil { return GUILD_INVITE_NO_PERMISSION } if !guild.HasPermission(inviter.Rank, PermissionInvite) { return GUILD_INVITE_NO_PERMISSION } // Check if player is already in a guild if gm.GetPlayerGuild(characterID) != nil { return GUILD_INVITE_ALREADY_IN_GUILD } // Check if player already has a pending invite if gm.HasPendingInvite(characterID) { return GUILD_INVITE_ALREADY_HAS_INVITE } // Create the invitation invite := &GuildInvite{ GuildID: guildID, GuildName: guild.GetName(), CharacterID: characterID, InviterID: inviterID, InviterName: inviter.Name, Rank: rank, InviteDate: time.Now(), ExpiresAt: time.Now().Add(GUILD_INVITE_TIMEOUT), } gm.invitesMutex.Lock() gm.pendingInvites[characterID] = invite gm.invitesMutex.Unlock() gm.statsMutex.Lock() gm.stats.TotalInvites++ gm.stats.LastStatsUpdate = time.Now() gm.statsMutex.Unlock() gm.logger.Info("Guild invitation sent", "guild_id", guildID, "inviter_id", inviterID, "character_id", characterID) return GUILD_INVITE_SUCCESS } // AcceptGuildInvite handles accepting a guild invitation func (gm *GuildManager) AcceptGuildInvite(characterID int32) int8 { gm.invitesMutex.Lock() invite, exists := gm.pendingInvites[characterID] if !exists { gm.invitesMutex.Unlock() return GUILD_INVITE_NO_INVITE } // Check if invite has expired if invite.IsExpired() { delete(gm.pendingInvites, characterID) gm.invitesMutex.Unlock() gm.statsMutex.Lock() gm.stats.ExpiredInvites++ gm.statsMutex.Unlock() return GUILD_INVITE_EXPIRED } // Remove the invite delete(gm.pendingInvites, characterID) gm.invitesMutex.Unlock() // Get the guild guild := gm.GetGuild(invite.GuildID) if guild == nil { return GUILD_INVITE_GUILD_NOT_FOUND } // Add the member to the guild success := guild.AddNewGuildMember(characterID, invite.InviterName, time.Now(), invite.Rank) if !success { return GUILD_INVITE_FAILED } gm.statsMutex.Lock() gm.stats.AcceptedInvites++ gm.stats.TotalMembers++ gm.stats.LastStatsUpdate = time.Now() gm.statsMutex.Unlock() gm.logger.Info("Guild invitation accepted", "guild_id", invite.GuildID, "character_id", characterID) return GUILD_INVITE_SUCCESS } // DeclineGuildInvite handles declining a guild invitation func (gm *GuildManager) DeclineGuildInvite(characterID int32) { gm.invitesMutex.Lock() _, exists := gm.pendingInvites[characterID] if exists { delete(gm.pendingInvites, characterID) } gm.invitesMutex.Unlock() if exists { gm.statsMutex.Lock() gm.stats.DeclinedInvites++ gm.stats.LastStatsUpdate = time.Now() gm.statsMutex.Unlock() gm.logger.Info("Guild invitation declined", "character_id", characterID) } } // HasPendingInvite checks if a character has a pending guild invitation func (gm *GuildManager) HasPendingInvite(characterID int32) bool { gm.invitesMutex.RLock() invite, exists := gm.pendingInvites[characterID] gm.invitesMutex.RUnlock() if !exists || invite.IsExpired() { if exists { // Clean up expired invite gm.invitesMutex.Lock() delete(gm.pendingInvites, characterID) gm.invitesMutex.Unlock() } return false } return true } // GetPendingInvite returns the pending invite for a character func (gm *GuildManager) GetPendingInvite(characterID int32) *GuildInvite { gm.invitesMutex.RLock() defer gm.invitesMutex.RUnlock() invite, exists := gm.pendingInvites[characterID] if !exists || invite.IsExpired() { return nil } return invite } // Statistics and utilities // GetStats returns the guild manager statistics func (gm *GuildManager) GetStats() GuildManagerStats { gm.statsMutex.RLock() defer gm.statsMutex.RUnlock() return gm.stats } // GetGuildCount returns the current number of active guilds func (gm *GuildManager) GetGuildCount() int32 { gm.guildsMutex.RLock() defer gm.guildsMutex.RUnlock() return int32(len(gm.guilds)) } // GetAllGuilds returns a copy of all guilds func (gm *GuildManager) GetAllGuilds() []*Guild { gm.guildsMutex.RLock() defer gm.guildsMutex.RUnlock() guilds := make([]*Guild, 0, len(gm.guilds)) for _, guild := range gm.guilds { guilds = append(guilds, guild) } return guilds } // Guild methods (preserving C++ API) // GetID returns the guild ID func (g *Guild) GetID() int32 { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.id } // GetName returns the guild name func (g *Guild) GetName() string { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.name } // GetLevel returns the guild level func (g *Guild) GetLevel() int8 { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.level } // GetMOTD returns the guild message of the day func (g *Guild) GetMOTD() string { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.motd } // GetFormedDate returns the guild formation date func (g *Guild) GetFormedDate() time.Time { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.formedDate } // SetName sets the guild name func (g *Guild) SetName(name string, sendPacket bool) { if len(name) > MaxGuildNameLength { name = name[:MaxGuildNameLength] } g.coreMutex.Lock() g.name = name g.lastModified = time.Now() g.saveNeeded = true g.coreMutex.Unlock() if sendPacket { g.SendGuildUpdate() } } // SetLevel sets the guild level func (g *Guild) SetLevel(level int8, sendPacket bool) { if level > MaxGuildLevel { level = MaxGuildLevel } if level < 1 { level = 1 } g.coreMutex.Lock() g.level = level g.lastModified = time.Now() g.saveNeeded = true g.coreMutex.Unlock() if sendPacket { g.SendGuildUpdate() } } // SetMOTD sets the guild message of the day func (g *Guild) SetMOTD(motd string, sendPacket bool) { if len(motd) > MaxMOTDLength { motd = motd[:MaxMOTDLength] } g.coreMutex.Lock() g.motd = motd g.lastModified = time.Now() g.saveNeeded = true g.coreMutex.Unlock() if sendPacket { g.SendGuildUpdate() } } // AddEXPCurrent adds experience to the guild func (g *Guild) AddEXPCurrent(exp int64, sendPacket bool) { g.coreMutex.Lock() g.expCurrent += exp g.lastModified = time.Now() g.saveNeeded = true g.coreMutex.Unlock() if sendPacket { g.SendGuildUpdate() } } // GetEXPCurrent returns the current guild experience func (g *Guild) GetEXPCurrent() int64 { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.expCurrent } // GetEXPToNextLevel returns the experience needed for next level func (g *Guild) GetEXPToNextLevel() int64 { g.coreMutex.RLock() defer g.coreMutex.RUnlock() return g.expToNextLevel } // Member management (preserving C++ API) // AddNewGuildMember adds a new member to the guild func (g *Guild) AddNewGuildMember(characterID int32, invitedBy string, joinDate time.Time, rank int8) bool { g.membersMutex.Lock() defer g.membersMutex.Unlock() // Check if member already exists if _, exists := g.members[characterID]; exists { return false } member := &GuildMember{ CharacterID: characterID, Rank: rank, JoinDate: joinDate, LastLoginDate: time.Now(), PointHistory: make([]PointHistory, 0), SaveNeeded: true, } g.members[characterID] = member g.memberSaveNeeded = true g.lastModified = time.Now() // Add guild event g.addNewGuildEventNoLock(EventMemberJoins, fmt.Sprintf("%s has joined the guild", member.Name), time.Now(), true) return true } // RemoveGuildMember removes a member from the guild func (g *Guild) RemoveGuildMember(characterID int32, sendPacket bool) { g.membersMutex.Lock() defer g.membersMutex.Unlock() if member, exists := g.members[characterID]; exists { // Add guild event g.addNewGuildEventNoLock(EventMemberLeaves, fmt.Sprintf("%s has left the guild", member.Name), time.Now(), sendPacket) delete(g.members, characterID) g.memberSaveNeeded = true g.lastModified = time.Now() } } // GetMember returns a guild member by character ID func (g *Guild) GetMember(characterID int32) *GuildMember { g.membersMutex.RLock() defer g.membersMutex.RUnlock() return g.members[characterID] } // GetMemberByName returns a guild member by name func (g *Guild) GetMemberByName(playerName string) *GuildMember { g.membersMutex.RLock() defer g.membersMutex.RUnlock() for _, member := range g.members { if member.Name == playerName { return member } } return nil } // HasMember checks if a character is a member of the guild func (g *Guild) HasMember(characterID int32) bool { g.membersMutex.RLock() defer g.membersMutex.RUnlock() _, exists := g.members[characterID] return exists } // GetMemberCount returns the number of members in the guild func (g *Guild) GetMemberCount() int { g.membersMutex.RLock() defer g.membersMutex.RUnlock() return len(g.members) } // GetAllMembers returns all guild members func (g *Guild) GetAllMembers() []*GuildMember { g.membersMutex.RLock() defer g.membersMutex.RUnlock() members := make([]*GuildMember, 0, len(g.members)) for _, member := range g.members { members = append(members, member) } return members } // PromoteGuildMember promotes a guild member func (g *Guild) PromoteGuildMember(characterID int32, promoterName string, sendPacket bool) bool { g.membersMutex.Lock() defer g.membersMutex.Unlock() member, exists := g.members[characterID] if !exists || member.Rank <= RankLeader { return false } oldRank := member.Rank member.Rank-- member.SaveNeeded = true g.memberSaveNeeded = true g.lastModified = time.Now() // Add guild event g.addNewGuildEventNoLock(EventMemberPromoted, fmt.Sprintf("%s has been promoted from %s to %s by %s", member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), promoterName), time.Now(), sendPacket) return true } // DemoteGuildMember demotes a guild member func (g *Guild) DemoteGuildMember(characterID int32, demoterName string, sendPacket bool) bool { g.membersMutex.Lock() defer g.membersMutex.Unlock() member, exists := g.members[characterID] if !exists || member.Rank >= RankRecruit { return false } oldRank := member.Rank member.Rank++ member.SaveNeeded = true g.memberSaveNeeded = true g.lastModified = time.Now() // Add guild event g.addNewGuildEventNoLock(EventMemberDemoted, fmt.Sprintf("%s has been demoted from %s to %s by %s", member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), demoterName), time.Now(), sendPacket) return true } // Permission system (preserving C++ API) // SetPermission sets a guild permission for a rank func (g *Guild) SetPermission(rank, permission, value int8, sendPacket, saveNeeded bool) bool { if rank < RankLeader || rank > RankRecruit { return false } g.permissionsMutex.Lock() defer g.permissionsMutex.Unlock() if g.permissions[rank] == nil { g.permissions[rank] = make(map[int8]int8) } g.permissions[rank][permission] = value g.lastModified = time.Now() if saveNeeded { g.ranksSaveNeeded = true } return true } // GetPermission returns a guild permission for a rank func (g *Guild) GetPermission(rank, permission int8) int8 { g.permissionsMutex.RLock() defer g.permissionsMutex.RUnlock() if rankPerms, exists := g.permissions[rank]; exists { if value, exists := rankPerms[permission]; exists { return value } } // Return default permission based on rank return g.getDefaultPermission(rank, permission) } // HasPermission checks if a rank has a specific permission func (g *Guild) HasPermission(rank, permission int8) bool { return g.GetPermission(rank, permission) > 0 } // Rank management (preserving C++ API) // SetRankName sets a custom rank name func (g *Guild) SetRankName(rank int8, name string, sendPacket bool) bool { if rank < RankLeader || rank > RankRecruit { return false } g.ranksMutex.Lock() g.ranks[rank] = name g.lastModified = time.Now() g.ranksSaveNeeded = true g.ranksMutex.Unlock() if sendPacket { g.SendGuildUpdate() } return true } // GetRankName returns the name for a rank func (g *Guild) GetRankName(rank int8) string { g.ranksMutex.RLock() defer g.ranksMutex.RUnlock() return g.getRankNameNoLock(rank) } // Event system (preserving C++ API) // AddNewGuildEvent adds a new event to the guild func (g *Guild) AddNewGuildEvent(eventType int32, description string, date time.Time, sendPacket bool) { g.eventsMutex.Lock() defer g.eventsMutex.Unlock() g.addNewGuildEventNoLock(eventType, description, date, sendPacket) } // GetGuildEvent returns a guild event by ID func (g *Guild) GetGuildEvent(eventID int64) *GuildEvent { g.eventsMutex.RLock() defer g.eventsMutex.RUnlock() for i := range g.guildEvents { if g.guildEvents[i].EventID == eventID { return &g.guildEvents[i] } } return nil } // GetNextEventID returns the next available event ID func (g *Guild) GetNextEventID() int64 { g.eventsMutex.Lock() defer g.eventsMutex.Unlock() eventID := g.nextEventID g.nextEventID++ return eventID } // Recruiting system (preserving C++ API) // SetRecruitingFlag sets a recruiting flag func (g *Guild) SetRecruitingFlag(flag, value int8, sendPacket bool) bool { if flag < RecruitingFlagTraining || flag > RecruitingFlagTradeskillers { return false } g.recruitingMutex.Lock() g.recruitingFlags[flag] = value g.lastModified = time.Now() g.recruitingSaveNeeded = true g.recruitingMutex.Unlock() if sendPacket { g.SendGuildUpdate() } return true } // GetRecruitingFlag returns a recruiting flag func (g *Guild) GetRecruitingFlag(flag int8) int8 { g.recruitingMutex.RLock() defer g.recruitingMutex.RUnlock() if value, exists := g.recruitingFlags[flag]; exists { return value } return 0 } // GetGuildInfo returns basic guild information func (g *Guild) GetGuildInfo() GuildInfo { g.coreMutex.RLock() g.membersMutex.RLock() g.recruitingMutex.RLock() defer g.coreMutex.RUnlock() defer g.membersMutex.RUnlock() defer g.recruitingMutex.RUnlock() return GuildInfo{ ID: g.id, Name: g.name, Level: g.level, FormedDate: g.formedDate, MOTD: g.motd, MemberCount: len(g.members), OnlineMemberCount: g.getOnlineMemberCountNoLock(), RecruiterCount: g.getRecruiterCountNoLock(), RecruitingShortDesc: g.recruitingShortDesc, RecruitingFullDesc: g.recruitingFullDesc, RecruitingMinLevel: g.recruitingMinLevel, RecruitingPlayStyle: g.recruitingPlayStyle, IsRecruiting: g.getRecruiterCountNoLock() > 0, UniqueAccounts: g.getUniqueAccountCountNoLock(), ExpCurrent: g.expCurrent, ExpToNextLevel: g.expToNextLevel, } } // Packet sending methods // SendGuildUpdate sends a guild update packet to all members func (g *Guild) SendGuildUpdate() { members := g.GetAllMembers() for _, member := range members { if member.Client != nil { g.sendGuildUpdateToClient(member.Client) } } } // sendGuildUpdateToClient sends a guild update packet to a specific client func (g *Guild) sendGuildUpdateToClient(client Client) { clientVersion := client.GetVersion() packetData, err := g.buildGuildUpdatePacket(clientVersion) if err != nil { return } client.SendPacket(packets.OP_GuildUpdateMsg, packetData) } // Helper methods (internal, no lock versions) func (g *Guild) getDefaultPermission(rank, permission int8) int8 { // Leaders have all permissions by default if rank == RankLeader { return 1 } // Default permissions based on rank and permission type switch permission { case PermissionSeeGuildChat, PermissionSpeakInGuildChat: return 1 // All members can see and speak in guild chat case PermissionReceivePoints: return 1 // All members can receive points case PermissionSeeOfficerChat, PermissionSpeakInOfficerChat: if rank <= RankOfficer { return 1 } case PermissionInvite: if rank <= RankSeniorMember { return 1 } } return 0 // Default to no permission } func (g *Guild) getRankNameNoLock(rank int8) string { if name, exists := g.ranks[rank]; exists { return name } if defaultName, exists := DefaultRankNames[rank]; exists { return defaultName } return "Unknown" } func (g *Guild) addNewGuildEventNoLock(eventType int32, description string, date time.Time, sendPacket bool) { event := GuildEvent{ EventID: g.nextEventID, Date: date, Type: eventType, Description: description, Locked: 0, SaveNeeded: true, } g.nextEventID++ // Add to front of events list (newest first) g.guildEvents = append([]GuildEvent{event}, g.guildEvents...) // Limit event history if len(g.guildEvents) > MaxEvents { g.guildEvents = g.guildEvents[:MaxEvents] } g.eventsSaveNeeded = true g.lastModified = time.Now() } func (g *Guild) getOnlineMemberCountNoLock() int { count := 0 for _, member := range g.members { if member.Entity != nil && member.Entity.IsOnline() { count++ } } return count } func (g *Guild) getRecruiterCountNoLock() int { count := 0 for _, member := range g.members { if member.MemberFlags&MemberFlagRecruitingForGuild != 0 { count++ } } return count } func (g *Guild) getUniqueAccountCountNoLock() int { accounts := make(map[int32]bool) for _, member := range g.members { accounts[member.AccountID] = true } return len(accounts) } // Helper methods for GuildInvite // IsExpired checks if the guild invite has expired func (gi *GuildInvite) IsExpired() bool { return time.Now().After(gi.ExpiresAt) } // TimeRemaining returns the remaining time for the invite func (gi *GuildInvite) TimeRemaining() time.Duration { return time.Until(gi.ExpiresAt) } // buildGuildUpdatePacket builds a guild update packet using the packet system func (g *Guild) buildGuildUpdatePacket(clientVersion uint32) ([]byte, error) { g.coreMutex.RLock() g.membersMutex.RLock() g.ranksMutex.RLock() g.permissionsMutex.RLock() g.recruitingMutex.RLock() defer g.coreMutex.RUnlock() defer g.membersMutex.RUnlock() defer g.ranksMutex.RUnlock() defer g.permissionsMutex.RUnlock() defer g.recruitingMutex.RUnlock() packet := make(map[string]any) // Basic guild information packet["guild_name"] = g.name packet["guild_motd"] = g.motd packet["guild_id"] = g.id packet["guild_level"] = uint8(g.level) packet["formed_date"] = uint32(g.formedDate.Unix()) packet["unique_accounts"] = uint16(g.getUniqueAccountCountNoLock()) packet["num_members"] = uint16(len(g.members)) packet["exp_current"] = uint64(g.expCurrent) packet["exp_to_next_level"] = uint64(g.expToNextLevel) // Version-specific fields if clientVersion >= 1008 { packet["guild_hall_location"] = g.guildHallLocation packet["guild_hall_zonename"] = g.guildHallZoneName packet["guild_hall_filename"] = g.guildHallFilename } // Event filters (simplified - would need proper implementation based on C++) packet["event_filter_retain1"] = uint32(0xFFFFFFFF) packet["event_filter_retain2"] = uint32(0xFFFFFFFF) packet["event_filter_retain3"] = uint32(0xFFFFFFFF) packet["event_filter_retain4"] = uint32(0xFFFFFFFF) packet["event_filter_broadcast1"] = uint32(0xFFFFFFFF) packet["event_filter_broadcast2"] = uint32(0xFFFFFFFF) packet["event_filter_broadcast3"] = uint32(0xFFFFFFFF) packet["event_filter_broadcast4"] = uint32(0xFFFFFFFF) // Recruiting information if clientVersion >= 562 { packet["recruiting_looking_for"] = uint8(g.getRecruitingLookingForNoLock()) packet["recruiting_desc_tag1"] = uint8(g.recruitingDescTags[0]) packet["recruiting_desc_tag2"] = uint8(g.recruitingDescTags[1]) packet["recruiting_desc_tag3"] = uint8(g.recruitingDescTags[2]) packet["recruiting_desc_tag4"] = uint8(g.recruitingDescTags[3]) packet["recruiting_playstyle"] = uint8(g.recruitingPlayStyle) packet["recruiting_min_level"] = uint8(g.recruitingMinLevel) } packet["recuiting_short_description"] = g.recruitingShortDesc // Note: typo preserved from C++ packet["recruiting_full_description"] = g.recruitingFullDesc // Rank names and permissions for rank := int8(RankLeader); rank <= int8(RankRecruit); rank++ { rankNameField := fmt.Sprintf("rank%d_name", rank) perm1Field := fmt.Sprintf("rank%d_permissions1", rank) perm2Field := fmt.Sprintf("rank%d_permissions2", rank) packet[rankNameField] = g.getRankNameNoLock(rank) // Pack permissions into two uint32 values (simplified) perm1, perm2 := g.packPermissionsNoLock(rank) packet[perm1Field] = perm1 packet[perm2Field] = perm2 } // Bank names (for version >= 562) if clientVersion >= 562 { packet["bank1_name"] = g.banks[0].Name packet["bank2_name"] = g.banks[1].Name packet["bank3_name"] = g.banks[2].Name packet["bank4_name"] = g.banks[3].Name } // Build the packet return packets.BuildPacket("GuildUpdate", packet, clientVersion, 0) } // packPermissionsNoLock packs guild permissions for a rank into two uint32 values func (g *Guild) packPermissionsNoLock(rank int8) (uint32, uint32) { var perm1, perm2 uint32 // Pack the first 32 permissions into perm1 for i := int8(0); i < 32; i++ { if g.getPermissionNoLock(rank, i) > 0 { perm1 |= 1 << i } } // Pack the next 32 permissions into perm2 for i := int8(32); i < 64; i++ { if g.getPermissionNoLock(rank, i) > 0 { perm2 |= 1 << (i - 32) } } return perm1, perm2 } // getPermissionNoLock returns a guild permission without locking (for internal use) func (g *Guild) getPermissionNoLock(rank, permission int8) int8 { if rankPerms, exists := g.permissions[rank]; exists { if value, exists := rankPerms[permission]; exists { return value } } // Return default permission based on rank return g.getDefaultPermission(rank, permission) } // getRecruitingLookingForNoLock returns the recruiting flags as a packed byte func (g *Guild) getRecruitingLookingForNoLock() uint8 { var flags uint8 if g.recruitingFlags[RecruitingFlagTraining] > 0 { flags |= 1 << 0 } if g.recruitingFlags[RecruitingFlagFighters] > 0 { flags |= 1 << 1 } if g.recruitingFlags[RecruitingFlagPriests] > 0 { flags |= 1 << 2 } if g.recruitingFlags[RecruitingFlagScouts] > 0 { flags |= 1 << 3 } if g.recruitingFlags[RecruitingFlagMages] > 0 { flags |= 1 << 4 } if g.recruitingFlags[RecruitingFlagTradeskillers] > 0 { flags |= 1 << 5 } return flags } // 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) {}