eq2go/internal/titles/master_list.go

510 lines
14 KiB
Go

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
})
}