package titles import ( "fmt" "sync" "time" ) // TitleManager manages the entire title system for the server type TitleManager struct { masterList *MasterTitlesList // Global title definitions playerLists map[int32]*PlayerTitlesList // Player-specific title collections mutex sync.RWMutex // Thread safety // Background cleanup cleanupTicker *time.Ticker stopCleanup chan bool // Statistics totalTitlesGranted int64 totalTitlesExpired int64 } // NewTitleManager creates a new title manager instance func NewTitleManager() *TitleManager { tm := &TitleManager{ masterList: NewMasterTitlesList(), playerLists: make(map[int32]*PlayerTitlesList), totalTitlesGranted: 0, totalTitlesExpired: 0, } // Start background cleanup process tm.startCleanupProcess() return tm } // GetMasterList returns the master titles list func (tm *TitleManager) GetMasterList() *MasterTitlesList { return tm.masterList } // GetPlayerTitles retrieves or creates a player's title collection func (tm *TitleManager) GetPlayerTitles(playerID int32) *PlayerTitlesList { tm.mutex.Lock() defer tm.mutex.Unlock() playerList, exists := tm.playerLists[playerID] if !exists { playerList = NewPlayerTitlesList(playerID, tm.masterList) tm.playerLists[playerID] = playerList } return playerList } // GrantTitle grants a title to a player func (tm *TitleManager) GrantTitle(playerID, titleID int32, sourceAchievementID, sourceQuestID uint32) error { playerList := tm.GetPlayerTitles(playerID) err := playerList.AddTitle(titleID, sourceAchievementID, sourceQuestID) if err == nil { tm.mutex.Lock() tm.totalTitlesGranted++ tm.mutex.Unlock() } return err } // RevokeTitle removes a title from a player func (tm *TitleManager) RevokeTitle(playerID, titleID int32) error { tm.mutex.RLock() playerList, exists := tm.playerLists[playerID] tm.mutex.RUnlock() if !exists { return fmt.Errorf("player %d has no titles", playerID) } return playerList.RemoveTitle(titleID) } // SetPlayerActivePrefix sets a player's active prefix title func (tm *TitleManager) SetPlayerActivePrefix(playerID, titleID int32) error { playerList := tm.GetPlayerTitles(playerID) return playerList.SetActivePrefix(titleID) } // SetPlayerActiveSuffix sets a player's active suffix title func (tm *TitleManager) SetPlayerActiveSuffix(playerID, titleID int32) error { playerList := tm.GetPlayerTitles(playerID) return playerList.SetActiveSuffix(titleID) } // GetPlayerFormattedName returns a player's name with active titles func (tm *TitleManager) GetPlayerFormattedName(playerID int32, playerName string) string { tm.mutex.RLock() playerList, exists := tm.playerLists[playerID] tm.mutex.RUnlock() if !exists { return playerName } return playerList.GetFormattedName(playerName) } // ProcessAchievementCompletion handles title grants from achievement completion func (tm *TitleManager) ProcessAchievementCompletion(playerID int32, achievementID uint32) error { playerList := tm.GetPlayerTitles(playerID) return playerList.GrantTitleFromAchievement(achievementID) } // CreateTitle creates a new title and adds it to the master list func (tm *TitleManager) CreateTitle(name, description, category string, position, source, rarity int32) (*Title, error) { title := NewTitle(0, name) // ID will be assigned automatically title.SetDescription(description) title.SetCategory(category) title.Position = position title.Source = source title.SetRarity(rarity) err := tm.masterList.AddTitle(title) if err != nil { return nil, err } return title, nil } // CreateAchievementTitle creates a title tied to a specific achievement func (tm *TitleManager) CreateAchievementTitle(name, description string, achievementID uint32, position, rarity int32) (*Title, error) { title := NewTitle(0, name) title.SetDescription(description) title.SetCategory(CategoryAchievement) title.Position = position title.Source = TitleSourceAchievement title.SetRarity(rarity) title.AchievementID = achievementID err := tm.masterList.AddTitle(title) if err != nil { return nil, err } return title, nil } // CreateTemporaryTitle creates a title that expires after a certain time func (tm *TitleManager) CreateTemporaryTitle(name, description string, hours int32, position, source, rarity int32) (*Title, error) { title := NewTitle(0, name) title.SetDescription(description) title.Position = position title.Source = source title.SetRarity(rarity) title.ExpirationHours = hours title.SetFlag(FlagTemporary) err := tm.masterList.AddTitle(title) if err != nil { return nil, err } return title, nil } // CreateUniqueTitle creates a title that only one player can have func (tm *TitleManager) CreateUniqueTitle(name, description string, position, source int32) (*Title, error) { title := NewTitle(0, name) title.SetDescription(description) title.Position = position title.Source = source title.SetRarity(TitleRarityUnique) title.SetFlag(FlagUnique) err := tm.masterList.AddTitle(title) if err != nil { return nil, err } return title, nil } // GetTitlesByCategory retrieves all titles in a category func (tm *TitleManager) GetTitlesByCategory(category string) []*Title { return tm.masterList.GetTitlesByCategory(category) } // GetTitlesBySource retrieves all titles from a specific source func (tm *TitleManager) GetTitlesBySource(source int32) []*Title { return tm.masterList.GetTitlesBySource(source) } // GetTitlesByRarity retrieves all titles of a specific rarity func (tm *TitleManager) GetTitlesByRarity(rarity int32) []*Title { return tm.masterList.GetTitlesByRarity(rarity) } // SearchTitles searches for titles by name (case-insensitive partial match) func (tm *TitleManager) SearchTitles(query string) []*Title { allTitles := tm.masterList.GetAllTitles(false) // Exclude hidden result := make([]*Title, 0) // Simple case-insensitive contains search // TODO: Implement more sophisticated search with fuzzy matching for _, title := range allTitles { if contains(title.Name, query) || contains(title.Description, query) { result = append(result, title) } } return result } // contains performs case-insensitive substring search func contains(s, substr string) bool { // Simple implementation - could be improved with proper Unicode handling sLower := []rune(s) substrLower := []rune(substr) for i := range sLower { if sLower[i] >= 'A' && sLower[i] <= 'Z' { sLower[i] = sLower[i] + 32 } } for i := range substrLower { if substrLower[i] >= 'A' && substrLower[i] <= 'Z' { substrLower[i] = substrLower[i] + 32 } } sStr := string(sLower) subStr := string(substrLower) for i := 0; i <= len(sStr)-len(subStr); i++ { if sStr[i:i+len(subStr)] == subStr { return true } } return false } // GetPlayerTitlePacketData builds packet data for a player's titles func (tm *TitleManager) GetPlayerTitlePacketData(playerID int32, playerName string) *TitlePacketData { tm.mutex.RLock() playerList, exists := tm.playerLists[playerID] tm.mutex.RUnlock() if !exists { // Create basic packet data with default titles return &TitlePacketData{ PlayerID: uint32(playerID), PlayerName: playerName, PrefixTitle: "", SuffixTitle: "Citizen", SubTitle: "", LastName: "", Titles: []TitleEntry{{"Citizen", false}}, NumTitles: 1, CurrentPrefix: int16(TitleIDNone), CurrentSuffix: int16(TitleIDCitizen), } } return playerList.BuildPacketData(playerName) } // startCleanupProcess begins the background cleanup of expired titles func (tm *TitleManager) startCleanupProcess() { tm.cleanupTicker = time.NewTicker(1 * time.Hour) // Run cleanup every hour tm.stopCleanup = make(chan bool) go func() { for { select { case <-tm.cleanupTicker.C: tm.cleanupExpiredTitles() case <-tm.stopCleanup: tm.cleanupTicker.Stop() return } } }() } // StopCleanupProcess stops the background cleanup func (tm *TitleManager) StopCleanupProcess() { if tm.stopCleanup != nil { tm.stopCleanup <- true } } // cleanupExpiredTitles removes expired titles from all players func (tm *TitleManager) cleanupExpiredTitles() { tm.mutex.RLock() playerLists := make([]*PlayerTitlesList, 0, len(tm.playerLists)) for _, list := range tm.playerLists { playerLists = append(playerLists, list) } tm.mutex.RUnlock() totalExpired := 0 for _, playerList := range playerLists { expired := playerList.CleanupExpiredTitles() totalExpired += expired } if totalExpired > 0 { tm.mutex.Lock() tm.totalTitlesExpired += int64(totalExpired) tm.mutex.Unlock() } } // GetStatistics returns title system statistics func (tm *TitleManager) GetStatistics() map[string]any { tm.mutex.RLock() defer tm.mutex.RUnlock() return map[string]any{ "total_titles": tm.masterList.GetTitleCount(), "total_players": len(tm.playerLists), "titles_granted": tm.totalTitlesGranted, "titles_expired": tm.totalTitlesExpired, "available_categories": tm.masterList.GetAvailableCategories(), } } // RemovePlayerFromMemory removes a player's title data from memory (when they log out) func (tm *TitleManager) RemovePlayerFromMemory(playerID int32) { tm.mutex.Lock() defer tm.mutex.Unlock() delete(tm.playerLists, playerID) } // LoadPlayerTitles loads a player's titles from database func (tm *TitleManager) LoadPlayerTitles(playerID int32, db *DB) error { playerList := tm.GetPlayerTitles(playerID) return playerList.LoadFromDatabase(db) } // SavePlayerTitles saves a player's titles to database func (tm *TitleManager) SavePlayerTitles(playerID int32, db *DB) error { tm.mutex.RLock() playerList, exists := tm.playerLists[playerID] tm.mutex.RUnlock() if !exists { return fmt.Errorf("player %d has no title data to save", playerID) } return playerList.SaveToDatabase(db) } // LoadMasterTitles loads all titles from database func (tm *TitleManager) LoadMasterTitles(db *DB) error { return tm.masterList.LoadFromDatabase(db) } // SaveMasterTitles saves all titles to database func (tm *TitleManager) SaveMasterTitles(db *DB) error { return tm.masterList.SaveToDatabase(db) } // ValidateTitle validates a title before adding it func (tm *TitleManager) ValidateTitle(title *Title) error { return tm.masterList.ValidateTitle(title) } // Shutdown gracefully shuts down the title manager func (tm *TitleManager) Shutdown() { tm.StopCleanupProcess() }