package titles import ( "database/sql" "fmt" "time" "eq2emu/internal/database" ) // DB wraps a database connection for title operations type DB struct { db *database.Database } // OpenDB opens a database connection func OpenDB(dsn string) (*DB, error) { db, err := database.NewMySQL(dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } return &DB{db: db}, nil } // Close closes the database connection func (db *DB) Close() error { if db.db != nil { return db.db.Close() } return nil } // CreateTables creates the necessary tables for the title system func (db *DB) CreateTables() error { // Create titles table titlesTableSQL := ` CREATE TABLE IF NOT EXISTS titles ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(255), 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 ON UPDATE CURRENT_TIMESTAMP, INDEX idx_titles_category (category), INDEX idx_titles_achievement (achievement_id) ) ` // Create player_titles table playerTitlesTableSQL := ` CREATE TABLE IF NOT EXISTS player_titles ( player_id INTEGER NOT NULL, title_id INTEGER NOT NULL, achievement_id INTEGER, granted_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expiration_date TIMESTAMP, is_active TINYINT(1) DEFAULT 0, PRIMARY KEY (player_id, title_id), FOREIGN KEY (title_id) REFERENCES titles(id), INDEX idx_player_titles_player (player_id), INDEX idx_player_titles_expiration (expiration_date) ) ` // Execute table creation if _, err := db.db.Exec(titlesTableSQL); err != nil { return fmt.Errorf("failed to create titles table: %w", err) } if _, err := db.db.Exec(playerTitlesTableSQL); err != nil { return fmt.Errorf("failed to create player_titles table: %w", err) } return nil } // LoadMasterTitles loads all titles from the database func (db *DB) LoadMasterTitles() ([]*Title, error) { var titles []*Title query := `SELECT id, name, description, category, position, source, rarity, flags, achievement_id FROM titles` rows, err := db.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to load titles: %w", err) } defer rows.Close() for rows.Next() { title := &Title{} var achievementID sql.NullInt64 err := rows.Scan( &title.ID, &title.Name, &title.Description, &title.Category, &title.Position, &title.Source, &title.Rarity, &title.Flags, &achievementID, ) if err != nil { return nil, fmt.Errorf("failed to scan title: %w", err) } // Handle nullable achievement_id if achievementID.Valid { title.AchievementID = uint32(achievementID.Int64) } titles = append(titles, title) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error reading titles: %w", err) } return titles, nil } // SaveMasterTitles saves all titles to the database func (db *DB) SaveMasterTitles(titles []*Title) error { // Use a transaction for atomic updates tx, err := db.db.Begin() if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // Clear existing titles if _, err = tx.Exec("DELETE FROM titles"); err != nil { return fmt.Errorf("failed to clear titles table: %w", err) } // Insert all titles insertQuery := ` INSERT INTO titles (id, name, description, category, position, source, rarity, flags, achievement_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ` for _, title := range titles { _, err = tx.Exec(insertQuery, title.ID, title.Name, title.Description, title.Category, int(title.Position), int(title.Source), int(title.Rarity), int64(title.Flags), nullableUint32(title.AchievementID), ) if err != nil { return fmt.Errorf("failed to insert title %d: %w", title.ID, err) } } return nil } // LoadPlayerTitles loads titles for a specific player func (db *DB) LoadPlayerTitles(playerID int32) ([]*PlayerTitle, error) { var playerTitles []*PlayerTitle query := ` SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ? ` rows, err := db.db.Query(query, playerID) if err != nil { return nil, fmt.Errorf("failed to load player titles: %w", err) } defer rows.Close() for rows.Next() { playerTitle := &PlayerTitle{ PlayerID: playerID, } var achievementID sql.NullInt64 var grantedDate, expirationDate sql.NullInt64 var isActive int err := rows.Scan( &playerTitle.TitleID, &achievementID, &grantedDate, &expirationDate, &isActive, ) if err != nil { return nil, fmt.Errorf("failed to scan player title: %w", err) } // Handle nullable achievement_id if achievementID.Valid { playerTitle.AchievementID = uint32(achievementID.Int64) } // Handle granted_date if grantedDate.Valid { playerTitle.EarnedDate = time.Unix(grantedDate.Int64, 0) } // Handle nullable expiration_date if expirationDate.Valid { playerTitle.ExpiresAt = time.Unix(expirationDate.Int64, 0) } playerTitles = append(playerTitles, playerTitle) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error reading player titles: %w", err) } return playerTitles, nil } // SavePlayerTitles saves a player's titles to the database func (db *DB) SavePlayerTitles(playerID int32, titles []*PlayerTitle, activePrefixID, activeSuffixID int32) error { // Use a transaction for atomic updates tx, err := db.db.Begin() if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // Clear existing titles for this player deleteQuery := "DELETE FROM player_titles WHERE player_id = ?" if _, err = tx.Exec(deleteQuery, playerID); err != nil { return fmt.Errorf("failed to clear player titles: %w", err) } // Insert all current titles insertQuery := ` INSERT INTO player_titles (player_id, title_id, achievement_id, granted_date, expiration_date, is_active) VALUES (?, ?, ?, ?, ?, ?) ` for _, playerTitle := range titles { isActive := 0 if playerTitle.TitleID == activePrefixID || playerTitle.TitleID == activeSuffixID { isActive = 1 } _, err = tx.Exec(insertQuery, playerID, playerTitle.TitleID, nullableUint32(playerTitle.AchievementID), playerTitle.EarnedDate.Unix(), nullableTime(playerTitle.ExpiresAt), isActive, ) if err != nil { return fmt.Errorf("failed to insert player title %d: %w", playerTitle.TitleID, err) } } return nil } // GetActivePlayerTitles retrieves the active prefix and suffix titles for a player func (db *DB) GetActivePlayerTitles(playerID int32) (prefixID, suffixID int32, err error) { query := ` SELECT pt.title_id, t.position FROM player_titles pt JOIN titles t ON pt.title_id = t.id WHERE pt.player_id = ? AND pt.is_active = 1 ` rows, err := db.db.Query(query, playerID) if err != nil { return 0, 0, fmt.Errorf("failed to get active titles: %w", err) } defer rows.Close() for rows.Next() { var titleID, position int32 err := rows.Scan(&titleID, &position) if err != nil { return 0, 0, fmt.Errorf("failed to scan active title: %w", err) } if position == TitlePositionPrefix { prefixID = titleID } else if position == TitlePositionSuffix { suffixID = titleID } } if err = rows.Err(); err != nil { return 0, 0, fmt.Errorf("error reading active titles: %w", err) } return prefixID, suffixID, nil } // Helper functions for nullable values func nullableUint32(val uint32) any { if val == 0 { return nil } return val } func nullableTime(t time.Time) any { if t.IsZero() { return nil } return t.Unix() }