eq2go/internal/titles/title_manager.go

382 lines
10 KiB
Go

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]interface{} {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
return map[string]interface{}{
"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
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) LoadPlayerTitles(playerID int32) error {
playerList := tm.GetPlayerTitles(playerID)
return playerList.LoadFromDatabase()
}
// SavePlayerTitles saves a player's titles to database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) SavePlayerTitles(playerID int32) 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()
}
// LoadMasterTitles loads all titles from database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) LoadMasterTitles() error {
return tm.masterList.LoadFromDatabase()
}
// SaveMasterTitles saves all titles to database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) SaveMasterTitles() error {
return tm.masterList.SaveToDatabase()
}
// 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()
}