package titles import ( "fmt" "sync" "eq2emu/internal/database" ) // MasterTitlesList manages all available titles in the game type MasterTitlesList struct { titles map[int32]*Title // All titles indexed by ID categorized map[string][]*Title // Titles grouped by category bySource map[int32][]*Title // Titles grouped by source byRarity map[int32][]*Title // Titles grouped by rarity byAchievement map[uint32]*Title // Titles indexed by achievement ID nextID int32 // Next available title ID mutex sync.RWMutex // Thread safety } // NewMasterTitlesList creates a new master titles list func NewMasterTitlesList() *MasterTitlesList { mtl := &MasterTitlesList{ titles: make(map[int32]*Title), categorized: make(map[string][]*Title), bySource: make(map[int32][]*Title), byRarity: make(map[int32][]*Title), byAchievement: make(map[uint32]*Title), nextID: 1, } // Initialize default titles mtl.initializeDefaultTitles() return mtl } // initializeDefaultTitles creates the basic system titles func (mtl *MasterTitlesList) initializeDefaultTitles() { // System titles with negative IDs citizen := NewTitle(TitleIDCitizen, "Citizen") citizen.SetDescription("Default citizen title") citizen.SetFlag(FlagStarter) citizen.Position = TitlePositionSuffix mtl.addTitleInternal(citizen) visitor := NewTitle(TitleIDVisitor, "Visitor") visitor.SetDescription("Temporary visitor status") visitor.SetFlag(FlagTemporary) visitor.Position = TitlePositionSuffix mtl.addTitleInternal(visitor) newcomer := NewTitle(TitleIDNewcomer, "Newcomer") newcomer.SetDescription("New player welcome title") newcomer.SetFlag(FlagStarter) newcomer.ExpirationHours = 168 // 1 week newcomer.Position = TitlePositionPrefix mtl.addTitleInternal(newcomer) returning := NewTitle(TitleIDReturning, "Returning") returning.SetDescription("Welcome back title for returning players") returning.SetFlag(FlagTemporary) returning.ExpirationHours = 72 // 3 days returning.Position = TitlePositionPrefix mtl.addTitleInternal(returning) } // AddTitle adds a new title to the master list func (mtl *MasterTitlesList) AddTitle(title *Title) error { mtl.mutex.Lock() defer mtl.mutex.Unlock() if title == nil { return fmt.Errorf("cannot add nil title") } // Assign ID if not set if title.ID == 0 { title.ID = mtl.nextID mtl.nextID++ } // Check for duplicate ID if _, exists := mtl.titles[title.ID]; exists { return fmt.Errorf("title with ID %d already exists", title.ID) } // Validate title name length if len(title.Name) > MaxTitleNameLength { return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength) } // Validate description length if len(title.Description) > MaxTitleDescriptionLength { return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength) } // Check for unique titles if title.IsUnique() { // TODO: Check if any player already has this unique title } return mtl.addTitleInternal(title) } // addTitleInternal adds a title without validation (used internally) func (mtl *MasterTitlesList) addTitleInternal(title *Title) error { // Add to main map mtl.titles[title.ID] = title // Add to category index if mtl.categorized[title.Category] == nil { mtl.categorized[title.Category] = make([]*Title, 0) } mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title) // Add to source index if mtl.bySource[title.Source] == nil { mtl.bySource[title.Source] = make([]*Title, 0) } mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title) // Add to rarity index if mtl.byRarity[title.Rarity] == nil { mtl.byRarity[title.Rarity] = make([]*Title, 0) } mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title) // Add to achievement index if applicable if title.AchievementID > 0 { mtl.byAchievement[title.AchievementID] = title } // Update next ID if necessary if title.ID >= mtl.nextID { mtl.nextID = title.ID + 1 } return nil } // GetTitle retrieves a title by ID func (mtl *MasterTitlesList) GetTitle(id int32) (*Title, bool) { mtl.mutex.RLock() defer mtl.mutex.RUnlock() title, exists := mtl.titles[id] if !exists { return nil, false } return title.Clone(), true } // GetTitleByName retrieves a title by name (case-sensitive) func (mtl *MasterTitlesList) GetTitleByName(name string) (*Title, bool) { mtl.mutex.RLock() defer mtl.mutex.RUnlock() for _, title := range mtl.titles { if title.Name == name { return title.Clone(), true } } return nil, false } // GetTitleByAchievement retrieves a title associated with an achievement func (mtl *MasterTitlesList) GetTitleByAchievement(achievementID uint32) (*Title, bool) { mtl.mutex.RLock() defer mtl.mutex.RUnlock() title, exists := mtl.byAchievement[achievementID] if !exists { return nil, false } return title.Clone(), true } // GetTitlesByCategory retrieves all titles in a specific category func (mtl *MasterTitlesList) GetTitlesByCategory(category string) []*Title { mtl.mutex.RLock() defer mtl.mutex.RUnlock() titles := mtl.categorized[category] if titles == nil { return make([]*Title, 0) } // Return clones to prevent external modification result := make([]*Title, len(titles)) for i, title := range titles { result[i] = title.Clone() } return result } // GetTitlesBySource retrieves all titles from a specific source func (mtl *MasterTitlesList) GetTitlesBySource(source int32) []*Title { mtl.mutex.RLock() defer mtl.mutex.RUnlock() titles := mtl.bySource[source] if titles == nil { return make([]*Title, 0) } // Return clones to prevent external modification result := make([]*Title, len(titles)) for i, title := range titles { result[i] = title.Clone() } return result } // GetTitlesByRarity retrieves all titles of a specific rarity func (mtl *MasterTitlesList) GetTitlesByRarity(rarity int32) []*Title { mtl.mutex.RLock() defer mtl.mutex.RUnlock() titles := mtl.byRarity[rarity] if titles == nil { return make([]*Title, 0) } // Return clones to prevent external modification result := make([]*Title, len(titles)) for i, title := range titles { result[i] = title.Clone() } return result } // GetAllTitles retrieves all titles (excluding hidden ones by default) func (mtl *MasterTitlesList) GetAllTitles(includeHidden bool) []*Title { mtl.mutex.RLock() defer mtl.mutex.RUnlock() result := make([]*Title, 0, len(mtl.titles)) for _, title := range mtl.titles { if !includeHidden && title.IsHidden() { continue } result = append(result, title.Clone()) } return result } // GetAvailableCategories returns all categories that have titles func (mtl *MasterTitlesList) GetAvailableCategories() []string { mtl.mutex.RLock() defer mtl.mutex.RUnlock() categories := make([]string, 0, len(mtl.categorized)) for category := range mtl.categorized { categories = append(categories, category) } return categories } // RemoveTitle removes a title from the master list func (mtl *MasterTitlesList) RemoveTitle(id int32) error { mtl.mutex.Lock() defer mtl.mutex.Unlock() title, exists := mtl.titles[id] if !exists { return fmt.Errorf("title with ID %d does not exist", id) } // Remove from main map delete(mtl.titles, id) // Remove from category index categorySlice := mtl.categorized[title.Category] mtl.removeFromSlice(&categorySlice, title) mtl.categorized[title.Category] = categorySlice if len(mtl.categorized[title.Category]) == 0 { delete(mtl.categorized, title.Category) } // Remove from source index sourceSlice := mtl.bySource[title.Source] mtl.removeFromSlice(&sourceSlice, title) mtl.bySource[title.Source] = sourceSlice if len(mtl.bySource[title.Source]) == 0 { delete(mtl.bySource, title.Source) } // Remove from rarity index raritySlice := mtl.byRarity[title.Rarity] mtl.removeFromSlice(&raritySlice, title) mtl.byRarity[title.Rarity] = raritySlice if len(mtl.byRarity[title.Rarity]) == 0 { delete(mtl.byRarity, title.Rarity) } // Remove from achievement index if applicable if title.AchievementID > 0 { delete(mtl.byAchievement, title.AchievementID) } return nil } // removeFromSlice removes a title from a slice func (mtl *MasterTitlesList) removeFromSlice(slice *[]*Title, title *Title) { for i, t := range *slice { if t.ID == title.ID { *slice = append((*slice)[:i], (*slice)[i+1:]...) break } } } // UpdateTitle updates an existing title func (mtl *MasterTitlesList) UpdateTitle(title *Title) error { mtl.mutex.Lock() defer mtl.mutex.Unlock() if title == nil { return fmt.Errorf("cannot update with nil title") } existing, exists := mtl.titles[title.ID] if !exists { return fmt.Errorf("title with ID %d does not exist", title.ID) } // Remove old title from indices categorySlice := mtl.categorized[existing.Category] mtl.removeFromSlice(&categorySlice, existing) mtl.categorized[existing.Category] = categorySlice sourceSlice := mtl.bySource[existing.Source] mtl.removeFromSlice(&sourceSlice, existing) mtl.bySource[existing.Source] = sourceSlice raritySlice := mtl.byRarity[existing.Rarity] mtl.removeFromSlice(&raritySlice, existing) mtl.byRarity[existing.Rarity] = raritySlice if existing.AchievementID > 0 { delete(mtl.byAchievement, existing.AchievementID) } // Update the title mtl.titles[title.ID] = title // Re-add to indices with new values if mtl.categorized[title.Category] == nil { mtl.categorized[title.Category] = make([]*Title, 0) } mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title) if mtl.bySource[title.Source] == nil { mtl.bySource[title.Source] = make([]*Title, 0) } mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title) if mtl.byRarity[title.Rarity] == nil { mtl.byRarity[title.Rarity] = make([]*Title, 0) } mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title) if title.AchievementID > 0 { mtl.byAchievement[title.AchievementID] = title } return nil } // GetTitleCount returns the total number of titles func (mtl *MasterTitlesList) GetTitleCount() int { mtl.mutex.RLock() defer mtl.mutex.RUnlock() return len(mtl.titles) } // ValidateTitle checks if a title meets all requirements func (mtl *MasterTitlesList) ValidateTitle(title *Title) error { if title == nil { return fmt.Errorf("title cannot be nil") } if len(title.Name) == 0 { return fmt.Errorf("title name cannot be empty") } if len(title.Name) > MaxTitleNameLength { return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength) } if len(title.Description) > MaxTitleDescriptionLength { return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength) } if len(title.Requirements) > MaxTitleRequirements { return fmt.Errorf("title has too many requirements (max %d)", MaxTitleRequirements) } // Validate position if title.Position != TitlePositionPrefix && title.Position != TitlePositionSuffix { return fmt.Errorf("invalid title position: %d", title.Position) } // Validate rarity if title.Rarity < TitleRarityCommon || title.Rarity > TitleRarityUnique { return fmt.Errorf("invalid title rarity: %d", title.Rarity) } return nil } // LoadFromDatabase loads titles from the database func (mtl *MasterTitlesList) LoadFromDatabase(db *database.DB) error { mtl.mutex.Lock() defer mtl.mutex.Unlock() // Create titles table if it doesn't exist if err := db.Exec(` CREATE TABLE IF NOT EXISTS titles ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, category TEXT, position INTEGER NOT NULL DEFAULT 0, source INTEGER NOT NULL DEFAULT 0, rarity INTEGER NOT NULL DEFAULT 0, flags INTEGER NOT NULL DEFAULT 0, achievement_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); err != nil { return fmt.Errorf("failed to create titles table: %w", err) } // Load all titles from database err := db.Query("SELECT id, name, description, category, position, source, rarity, flags, achievement_id FROM titles", func(row *database.Row) error { title := &Title{ ID: int32(row.Int64(0)), Name: row.Text(1), Description: row.Text(2), Category: row.Text(3), Position: int32(row.Int(4)), Source: int32(row.Int(5)), Rarity: int32(row.Int(6)), Flags: uint32(row.Int64(7)), } // Handle nullable achievement_id if !row.IsNull(8) { title.AchievementID = uint32(row.Int64(8)) } mtl.addTitleInternal(title) return nil }) if err != nil { return fmt.Errorf("failed to load titles from database: %w", err) } return nil } // SaveToDatabase saves titles to the database func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error { mtl.mutex.RLock() defer mtl.mutex.RUnlock() return db.Transaction(func(txDB *database.DB) error { // Clear existing titles (this is a full sync) if err := txDB.Exec("DELETE FROM titles"); err != nil { return fmt.Errorf("failed to clear titles table: %w", err) } // Insert all current titles for _, title := range mtl.titles { var achievementID interface{} if title.AchievementID != 0 { achievementID = title.AchievementID } err := txDB.Exec(` INSERT INTO titles (id, name, description, category, position, source, rarity, flags, achievement_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, title.ID, title.Name, title.Description, title.Category, int(title.Position), int(title.Source), int(title.Rarity), int64(title.Flags), achievementID) if err != nil { return fmt.Errorf("failed to insert title %d: %w", title.ID, err) } } return nil }) }