package guilds import ( "context" "fmt" "sort" "strings" "sync" "time" ) // NewGuildList creates a new guild list instance func NewGuildList() *GuildList { return &GuildList{ guilds: make(map[int32]*Guild), } } // AddGuild adds a guild to the list func (gl *GuildList) AddGuild(guild *Guild) { gl.mu.Lock() defer gl.mu.Unlock() gl.guilds[guild.GetID()] = guild } // GetGuild retrieves a guild by ID func (gl *GuildList) GetGuild(guildID int32) *Guild { gl.mu.RLock() defer gl.mu.RUnlock() return gl.guilds[guildID] } // RemoveGuild removes a guild from the list func (gl *GuildList) RemoveGuild(guildID int32) { gl.mu.Lock() defer gl.mu.Unlock() delete(gl.guilds, guildID) } // GetAllGuilds returns all guilds func (gl *GuildList) GetAllGuilds() []*Guild { gl.mu.RLock() defer gl.mu.RUnlock() guilds := make([]*Guild, 0, len(gl.guilds)) for _, guild := range gl.guilds { guilds = append(guilds, guild) } return guilds } // GetGuildCount returns the number of guilds func (gl *GuildList) GetGuildCount() int { gl.mu.RLock() defer gl.mu.RUnlock() return len(gl.guilds) } // FindGuildByName finds a guild by name (case-insensitive) func (gl *GuildList) FindGuildByName(name string) *Guild { gl.mu.RLock() defer gl.mu.RUnlock() for _, guild := range gl.guilds { if strings.EqualFold(guild.GetName(), name) { return guild } } return nil } // NewGuildManager creates a new guild manager instance func NewGuildManager(database GuildDatabase, clientManager ClientManager, playerManager PlayerManager) *GuildManager { return &GuildManager{ guildList: NewGuildList(), database: database, clientManager: clientManager, playerManager: playerManager, } } // SetEventHandler sets the guild event handler func (gm *GuildManager) SetEventHandler(handler GuildEventHandler) { gm.eventHandler = handler } // SetLogger sets the logger for the manager func (gm *GuildManager) SetLogger(logger LogHandler) { gm.logger = logger } // Initialize loads all guilds from the database func (gm *GuildManager) Initialize(ctx context.Context) error { // Load all guilds guildData, err := gm.database.LoadGuilds(ctx) if err != nil { return fmt.Errorf("failed to load guilds: %w", err) } for _, data := range guildData { guild, err := gm.loadGuildFromData(ctx, data) if err != nil { if gm.logger != nil { gm.logger.LogError("guilds", "Failed to load guild %d (%s): %v", data.ID, data.Name, err) } continue } gm.guildList.AddGuild(guild) if gm.logger != nil { gm.logger.LogDebug("guilds", "Loaded guild %d (%s) with %d members", guild.GetID(), guild.GetName(), len(guild.GetAllMembers())) } } if gm.logger != nil { gm.logger.LogInfo("guilds", "Loaded %d guilds", gm.guildList.GetGuildCount()) } return nil } // LoadGuild loads a specific guild by ID func (gm *GuildManager) LoadGuild(ctx context.Context, guildID int32) (*Guild, error) { // Check if already loaded if guild := gm.guildList.GetGuild(guildID); guild != nil { return guild, nil } // Load from database guildData, err := gm.database.LoadGuild(ctx, guildID) if err != nil { return nil, fmt.Errorf("failed to load guild data: %w", err) } guild, err := gm.loadGuildFromData(ctx, *guildData) if err != nil { return nil, fmt.Errorf("failed to create guild from data: %w", err) } gm.guildList.AddGuild(guild) return guild, nil } // CreateGuild creates a new guild func (gm *GuildManager) CreateGuild(ctx context.Context, name, motd string, leaderCharacterID int32) (*Guild, error) { // Validate guild name if err := gm.validateGuildName(name); err != nil { return nil, err } // Check if guild name already exists if gm.guildList.FindGuildByName(name) != nil { return nil, fmt.Errorf("guild name '%s' already exists", name) } // Get leader player info leaderInfo, err := gm.playerManager.GetPlayerInfo(leaderCharacterID) if err != nil { return nil, fmt.Errorf("failed to get leader info: %w", err) } // Create guild data guildData := GuildData{ Name: name, MOTD: motd, Level: 1, EXPCurrent: 111, EXPToNextLevel: 2521, FormedDate: time.Now(), } // Save to database guildID, err := gm.database.CreateGuild(ctx, guildData) if err != nil { return nil, fmt.Errorf("failed to create guild in database: %w", err) } guildData.ID = guildID // Create guild instance guild := NewGuild() guild.SetID(guildData.ID) guild.SetName(guildData.Name, false) guild.SetMOTD(guildData.MOTD, false) guild.SetLevel(guildData.Level, false) guild.SetEXPCurrent(guildData.EXPCurrent, false) guild.SetEXPToNextLevel(guildData.EXPToNextLevel, false) guild.SetFormedDate(guildData.FormedDate) // Add leader as first member leader := NewGuildMember(leaderCharacterID, leaderInfo.CharacterName, RankLeader) leader.AccountID = leaderInfo.AccountID leader.UpdatePlayerInfo(leaderInfo) guild.members[leaderCharacterID] = leader // Save member to database if err := gm.database.SaveGuildMembers(ctx, guildID, []*GuildMember{leader}); err != nil { return nil, fmt.Errorf("failed to save guild leader: %w", err) } // Add to guild list gm.guildList.AddGuild(guild) // Add guild creation event guild.AddNewGuildEvent(EventGuildLevelUp, fmt.Sprintf("Guild '%s' has been formed by %s", name, leaderInfo.CharacterName), time.Now(), true) // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnGuildCreated(guild) } if gm.logger != nil { gm.logger.LogInfo("guilds", "Created guild %d (%s) with leader %s (%d)", guildID, name, leaderInfo.CharacterName, leaderCharacterID) } return guild, nil } // DeleteGuild deletes a guild func (gm *GuildManager) DeleteGuild(ctx context.Context, guildID int32, deleterName string) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } guildName := guild.GetName() // Remove from database if err := gm.database.DeleteGuild(ctx, guildID); err != nil { return fmt.Errorf("failed to delete guild from database: %w", err) } // Remove from guild list gm.guildList.RemoveGuild(guildID) // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnGuildDeleted(guildID, guildName) } if gm.logger != nil { gm.logger.LogInfo("guilds", "Deleted guild %d (%s) by %s", guildID, guildName, deleterName) } return nil } // GetGuild returns a guild by ID func (gm *GuildManager) GetGuild(guildID int32) *Guild { return gm.guildList.GetGuild(guildID) } // GetGuildByName returns a guild by name func (gm *GuildManager) GetGuildByName(name string) *Guild { return gm.guildList.FindGuildByName(name) } // GetAllGuilds returns all guilds func (gm *GuildManager) GetAllGuilds() []*Guild { return gm.guildList.GetAllGuilds() } // GetGuildByCharacterID returns the guild for a character func (gm *GuildManager) GetGuildByCharacterID(ctx context.Context, characterID int32) (*Guild, error) { // Try to find in loaded guilds first for _, guild := range gm.guildList.GetAllGuilds() { if guild.GetGuildMember(characterID) != nil { return guild, nil } } // Look up in database guildID, err := gm.database.GetGuildIDByCharacterID(ctx, characterID) if err != nil { return nil, fmt.Errorf("character %d is not in a guild: %w", characterID, err) } // Load the guild if not already loaded return gm.LoadGuild(ctx, guildID) } // InvitePlayer invites a player to a guild func (gm *GuildManager) InvitePlayer(ctx context.Context, guildID, inviterID int32, playerName string, rank int8) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } // Validate inviter permissions inviter := guild.GetGuildMember(inviterID) if inviter == nil { return fmt.Errorf("inviter %d is not a guild member", inviterID) } if guild.GetPermission(inviter.GetRank(), PermissionInvite) == 0 { return fmt.Errorf("inviter does not have permission to invite") } // Validate target player targetID, err := gm.playerManager.ValidatePlayerExists(playerName) if err != nil { return fmt.Errorf("player '%s' not found: %w", playerName, err) } // Check if player is already in a guild if existingGuild, _ := gm.GetGuildByCharacterID(ctx, targetID); existingGuild != nil { return fmt.Errorf("player '%s' is already in guild '%s'", playerName, existingGuild.GetName()) } // TODO: Send guild invitation to player // This would typically involve sending a packet to the client if gm.logger != nil { gm.logger.LogDebug("guilds", "Player %s invited to guild %s by %s", playerName, guild.GetName(), inviter.GetName()) } return nil } // AddMemberToGuild adds a member to a guild func (gm *GuildManager) AddMemberToGuild(ctx context.Context, guildID, characterID int32, inviterName string, rank int8) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } // Check if already a member if guild.GetGuildMember(characterID) != nil { return fmt.Errorf("character %d is already a guild member", characterID) } // Get player info playerInfo, err := gm.playerManager.GetPlayerInfo(characterID) if err != nil { return fmt.Errorf("failed to get player info: %w", err) } // Create guild member member := NewGuildMember(characterID, playerInfo.CharacterName, rank) member.AccountID = playerInfo.AccountID member.UpdatePlayerInfo(playerInfo) // Add to guild guild.members[characterID] = member guild.memberSaveNeeded = true // Save to database if err := gm.database.SaveGuildMembers(ctx, guildID, []*GuildMember{member}); err != nil { return fmt.Errorf("failed to save new guild member: %w", err) } // Add guild event guild.AddNewGuildEvent(EventMemberJoins, fmt.Sprintf("%s has joined the guild (invited by %s)", playerInfo.CharacterName, inviterName), time.Now(), true) // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnMemberJoined(guild, member, inviterName) } if gm.logger != nil { gm.logger.LogInfo("guilds", "Player %s (%d) joined guild %s (%d)", playerInfo.CharacterName, characterID, guild.GetName(), guildID) } return nil } // RemoveMemberFromGuild removes a member from a guild func (gm *GuildManager) RemoveMemberFromGuild(ctx context.Context, guildID, characterID int32, removerName, reason string) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } member := guild.GetGuildMember(characterID) if member == nil { return fmt.Errorf("character %d is not a guild member", characterID) } memberName := member.GetName() // Remove from guild guild.RemoveGuildMember(characterID, true) // Save changes if err := gm.saveGuildChanges(ctx, guild); err != nil { if gm.logger != nil { gm.logger.LogError("guilds", "Failed to save guild after removing member: %v", err) } } // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnMemberLeft(guild, member, reason) } if gm.logger != nil { gm.logger.LogInfo("guilds", "Player %s (%d) removed from guild %s (%d) by %s - %s", memberName, characterID, guild.GetName(), guildID, removerName, reason) } return nil } // PromoteMember promotes a guild member func (gm *GuildManager) PromoteMember(ctx context.Context, guildID, characterID int32, promoterName string) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } member := guild.GetGuildMember(characterID) if member == nil { return fmt.Errorf("character %d is not a guild member", characterID) } oldRank := member.GetRank() if oldRank <= RankLeader { return fmt.Errorf("cannot promote guild leader") } // Promote if !guild.PromoteGuildMember(characterID, promoterName, true) { return fmt.Errorf("failed to promote member") } // Save changes if err := gm.saveGuildChanges(ctx, guild); err != nil { if gm.logger != nil { gm.logger.LogError("guilds", "Failed to save guild after promotion: %v", err) } } // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnMemberPromoted(guild, member, oldRank, member.GetRank(), promoterName) } return nil } // DemoteMember demotes a guild member func (gm *GuildManager) DemoteMember(ctx context.Context, guildID, characterID int32, demoterName string) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } member := guild.GetGuildMember(characterID) if member == nil { return fmt.Errorf("character %d is not a guild member", characterID) } oldRank := member.GetRank() if oldRank >= RankRecruit { return fmt.Errorf("cannot demote recruit further") } // Demote if !guild.DemoteGuildMember(characterID, demoterName, true) { return fmt.Errorf("failed to demote member") } // Save changes if err := gm.saveGuildChanges(ctx, guild); err != nil { if gm.logger != nil { gm.logger.LogError("guilds", "Failed to save guild after demotion: %v", err) } } // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnMemberDemoted(guild, member, oldRank, member.GetRank(), demoterName) } return nil } // AwardPoints awards points to guild members func (gm *GuildManager) AwardPoints(ctx context.Context, guildID int32, characterIDs []int32, points float64, comment, awardedBy string) error { guild := gm.guildList.GetGuild(guildID) if guild == nil { return fmt.Errorf("guild %d not found", guildID) } for _, characterID := range characterIDs { if !guild.AddPointsToGuildMember(characterID, points, awardedBy, comment, true) { if gm.logger != nil { gm.logger.LogWarning("guilds", "Failed to award points to character %d", characterID) } } } // Save changes if err := gm.saveGuildChanges(ctx, guild); err != nil { if gm.logger != nil { gm.logger.LogError("guilds", "Failed to save guild after awarding points: %v", err) } } // Notify event handler if gm.eventHandler != nil { gm.eventHandler.OnPointsAwarded(guild, characterIDs, points, comment, awardedBy) } return nil } // SaveAllGuilds saves all guilds that need saving func (gm *GuildManager) SaveAllGuilds(ctx context.Context) error { guilds := gm.guildList.GetAllGuilds() var saveErrors []error for _, guild := range guilds { if err := gm.saveGuildChanges(ctx, guild); err != nil { saveErrors = append(saveErrors, fmt.Errorf("guild %d: %w", guild.GetID(), err)) } } if len(saveErrors) > 0 { return fmt.Errorf("failed to save some guilds: %v", saveErrors) } return nil } // SearchGuilds searches for guilds based on criteria func (gm *GuildManager) SearchGuilds(criteria GuildSearchCriteria) []*Guild { guilds := gm.guildList.GetAllGuilds() var results []*Guild for _, guild := range guilds { if gm.matchesSearchCriteria(guild, criteria) { results = append(results, guild) } } // Sort by name sort.Slice(results, func(i, j int) bool { return results[i].GetName() < results[j].GetName() }) return results } // GetGuildStatistics returns guild system statistics func (gm *GuildManager) GetGuildStatistics() GuildStatistics { guilds := gm.guildList.GetAllGuilds() stats := GuildStatistics{ TotalGuilds: len(guilds), } totalMembers := 0 activeGuilds := 0 totalEvents := 0 totalRecruiters := 0 uniqueAccounts := make(map[int32]bool) highestLevel := int8(1) for _, guild := range guilds { members := guild.GetAllMembers() memberCount := len(members) totalMembers += memberCount if memberCount > 0 { activeGuilds++ } // Track unique accounts for _, member := range members { uniqueAccounts[member.AccountID] = true if member.IsRecruiter() { totalRecruiters++ } } // Guild level if guild.GetLevel() > highestLevel { highestLevel = guild.GetLevel() } // Event count (approximate) totalEvents += len(guild.guildEvents) } stats.TotalMembers = totalMembers stats.ActiveGuilds = activeGuilds stats.TotalEvents = totalEvents stats.TotalRecruiters = totalRecruiters stats.UniqueAccounts = len(uniqueAccounts) stats.HighestGuildLevel = highestLevel if len(guilds) > 0 { stats.AverageGuildSize = float64(totalMembers) / float64(len(guilds)) } return stats } // Helper methods // loadGuildFromData creates a guild instance from database data func (gm *GuildManager) loadGuildFromData(ctx context.Context, data GuildData) (*Guild, error) { guild := NewGuild() guild.SetID(data.ID) guild.SetName(data.Name, false) guild.SetMOTD(data.MOTD, false) guild.SetLevel(data.Level, false) guild.SetEXPCurrent(data.EXPCurrent, false) guild.SetEXPToNextLevel(data.EXPToNextLevel, false) guild.SetFormedDate(data.FormedDate) // Load members memberData, err := gm.database.LoadGuildMembers(ctx, data.ID) if err != nil { return nil, fmt.Errorf("failed to load guild members: %w", err) } for _, md := range memberData { member := &GuildMember{ CharacterID: md.CharacterID, AccountID: md.AccountID, RecruiterID: md.RecruiterID, Name: md.Name, GuildStatus: md.GuildStatus, Points: md.Points, AdventureClass: md.AdventureClass, AdventureLevel: md.AdventureLevel, TradeskillClass: md.TradeskillClass, TradeskillLevel: md.TradeskillLevel, Rank: md.Rank, MemberFlags: md.MemberFlags, Zone: md.Zone, JoinDate: md.JoinDate, LastLoginDate: md.LastLoginDate, Note: md.Note, OfficerNote: md.OfficerNote, RecruiterDescription: md.RecruiterDescription, RecruiterPictureData: md.RecruiterPictureData, RecruitingShowAdventureClass: md.RecruitingShowAdventureClass, PointHistory: make([]PointHistory, 0), } // Load point history historyData, err := gm.database.LoadPointHistory(ctx, md.CharacterID) if err == nil { for _, hd := range historyData { member.PointHistory = append(member.PointHistory, PointHistory{ Date: hd.Date, ModifiedBy: hd.ModifiedBy, Comment: hd.Comment, Points: hd.Points, SaveNeeded: false, }) } } guild.members[md.CharacterID] = member } // Load events eventData, err := gm.database.LoadGuildEvents(ctx, data.ID) if err == nil { for _, ed := range eventData { guild.guildEvents = append(guild.guildEvents, GuildEvent{ EventID: ed.EventID, Date: ed.Date, Type: ed.Type, Description: ed.Description, Locked: ed.Locked, SaveNeeded: false, }) } } // Load ranks rankData, err := gm.database.LoadGuildRanks(ctx, data.ID) if err == nil { for _, rd := range rankData { guild.ranks[rd.Rank] = rd.Name } } // Load permissions permissionData, err := gm.database.LoadGuildPermissions(ctx, data.ID) if err == nil { for _, pd := range permissionData { if guild.permissions[pd.Rank] == nil { guild.permissions[pd.Rank] = make(map[int8]int8) } guild.permissions[pd.Rank][pd.Permission] = pd.Value } } // Load event filters filterData, err := gm.database.LoadGuildEventFilters(ctx, data.ID) if err == nil { for _, fd := range filterData { if guild.eventFilters[fd.EventID] == nil { guild.eventFilters[fd.EventID] = make(map[int8]int8) } guild.eventFilters[fd.EventID][fd.Category] = fd.Value } } // Load recruiting settings recruitingData, err := gm.database.LoadGuildRecruiting(ctx, data.ID) if err == nil { for _, rd := range recruitingData { if rd.Flag < 0 { // Description tag (stored with negative flag values) guild.recruitingDescTags[-rd.Flag-1] = rd.Value } else { // Recruiting flag guild.recruitingFlags[rd.Flag] = rd.Value } } } // Update next event ID if len(guild.guildEvents) > 0 { maxEventID := int64(0) for _, event := range guild.guildEvents { if event.EventID > maxEventID { maxEventID = event.EventID } } guild.nextEventID = maxEventID + 1 } // Clear save flags guild.saveNeeded = false guild.memberSaveNeeded = false guild.eventsSaveNeeded = false guild.ranksSaveNeeded = false guild.eventFiltersSaveNeeded = false guild.pointsHistorySaveNeeded = false guild.recruitingSaveNeeded = false return guild, nil } // saveGuildChanges saves any pending changes for a guild func (gm *GuildManager) saveGuildChanges(ctx context.Context, guild *Guild) error { var saveErrors []error if guild.GetSaveNeeded() { if err := gm.database.SaveGuild(ctx, guild); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild data: %w", err)) } else { guild.SetSaveNeeded(false) } } if guild.memberSaveNeeded { members := guild.GetAllMembers() if err := gm.database.SaveGuildMembers(ctx, guild.GetID(), members); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild members: %w", err)) } else { guild.memberSaveNeeded = false } } if guild.eventsSaveNeeded { if err := gm.database.SaveGuildEvents(ctx, guild.GetID(), guild.guildEvents); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild events: %w", err)) } else { guild.eventsSaveNeeded = false } } if guild.ranksSaveNeeded { if err := gm.database.SaveGuildRanks(ctx, guild.GetID(), guild.ranks); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild ranks: %w", err)) } else { guild.ranksSaveNeeded = false } } if guild.eventFiltersSaveNeeded { if err := gm.database.SaveGuildEventFilters(ctx, guild.GetID(), guild.eventFilters); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild event filters: %w", err)) } else { guild.eventFiltersSaveNeeded = false } } if guild.recruitingSaveNeeded { if err := gm.database.SaveGuildRecruiting(ctx, guild.GetID(), guild.recruitingFlags, guild.recruitingDescTags); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save guild recruiting: %w", err)) } else { guild.recruitingSaveNeeded = false } } if guild.pointsHistorySaveNeeded { for _, member := range guild.GetAllMembers() { if err := gm.database.SavePointHistory(ctx, member.GetCharacterID(), member.GetPointHistory()); err != nil { saveErrors = append(saveErrors, fmt.Errorf("failed to save point history for %d: %w", member.GetCharacterID(), err)) } } guild.pointsHistorySaveNeeded = false } if len(saveErrors) > 0 { return fmt.Errorf("save errors: %v", saveErrors) } return nil } // validateGuildName validates a guild name func (gm *GuildManager) validateGuildName(name string) error { if len(strings.TrimSpace(name)) == 0 { return fmt.Errorf("guild name cannot be empty") } if len(name) > MaxGuildNameLength { return fmt.Errorf("guild name too long: %d > %d", len(name), MaxGuildNameLength) } // Check for invalid characters if strings.ContainsAny(name, "<>&\"'") { return fmt.Errorf("guild name contains invalid characters") } return nil } // matchesSearchCriteria checks if a guild matches search criteria func (gm *GuildManager) matchesSearchCriteria(guild *Guild, criteria GuildSearchCriteria) bool { // Name pattern matching if criteria.NamePattern != "" { if !strings.Contains(strings.ToLower(guild.GetName()), strings.ToLower(criteria.NamePattern)) { return false } } // Level range level := guild.GetLevel() if criteria.MinLevel > 0 && level < criteria.MinLevel { return false } if criteria.MaxLevel > 0 && level > criteria.MaxLevel { return false } // Member count range memberCount := len(guild.GetAllMembers()) if criteria.MinMembers > 0 && memberCount < criteria.MinMembers { return false } if criteria.MaxMembers > 0 && memberCount > criteria.MaxMembers { return false } // Recruiting only if criteria.RecruitingOnly && guild.GetNumRecruiters() == 0 { return false } // Play style if criteria.PlayStyle > 0 && guild.GetRecruitingPlayStyle() != criteria.PlayStyle { return false } // Required flags for _, flag := range criteria.RequiredFlags { if guild.GetRecruitingFlag(flag) == 0 { return false } } // Required description tags for _, tag := range criteria.RequiredDescTags { found := false for i := int8(0); i < 4; i++ { if guild.GetRecruitingDescTag(i) == tag { found = true break } } if !found { return false } } // Excluded description tags for _, tag := range criteria.ExcludedDescTags { for i := int8(0); i < 4; i++ { if guild.GetRecruitingDescTag(i) == tag { return false } } } return true }