382 lines
10 KiB
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()
|
|
} |