eq2go/internal/titles/database.go

336 lines
7.8 KiB
Go

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