336 lines
7.8 KiB
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()
|
|
} |