303 lines
8.0 KiB
Go
303 lines
8.0 KiB
Go
package titles
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
"zombiezen.com/go/sqlite/sqlitex"
|
|
)
|
|
|
|
// DB wraps a SQLite connection for title operations
|
|
type DB struct {
|
|
conn *sqlite.Conn
|
|
}
|
|
|
|
// OpenDB opens a database connection
|
|
func OpenDB(path string) (*DB, error) {
|
|
conn, err := sqlite.OpenConn(path, sqlite.OpenReadWrite|sqlite.OpenCreate|sqlite.OpenWAL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// Enable foreign keys
|
|
if err := sqlitex.ExecTransient(conn, "PRAGMA foreign_keys = ON;", nil); err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
|
}
|
|
|
|
return &DB{conn: conn}, nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (db *DB) Close() error {
|
|
if db.conn != nil {
|
|
return db.conn.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,
|
|
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
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_titles_category ON titles(category);
|
|
CREATE INDEX IF NOT EXISTS idx_titles_achievement ON titles(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 INTEGER DEFAULT 0,
|
|
PRIMARY KEY (player_id, title_id),
|
|
FOREIGN KEY (title_id) REFERENCES titles(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_player_titles_player ON player_titles(player_id);
|
|
CREATE INDEX IF NOT EXISTS idx_player_titles_expiration ON player_titles(expiration_date);
|
|
`
|
|
|
|
// Execute table creation
|
|
if err := sqlitex.ExecuteScript(db.conn, titlesTableSQL, &sqlitex.ExecOptions{}); err != nil {
|
|
return fmt.Errorf("failed to create titles table: %w", err)
|
|
}
|
|
|
|
if err := sqlitex.ExecuteScript(db.conn, playerTitlesTableSQL, &sqlitex.ExecOptions{}); 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`
|
|
|
|
err := sqlitex.Execute(db.conn, query, &sqlitex.ExecOptions{
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
title := &Title{
|
|
ID: int32(stmt.ColumnInt64(0)),
|
|
Name: stmt.ColumnText(1),
|
|
Description: stmt.ColumnText(2),
|
|
Category: stmt.ColumnText(3),
|
|
Position: int32(stmt.ColumnInt(4)),
|
|
Source: int32(stmt.ColumnInt(5)),
|
|
Rarity: int32(stmt.ColumnInt(6)),
|
|
Flags: uint32(stmt.ColumnInt64(7)),
|
|
}
|
|
|
|
// Handle nullable achievement_id
|
|
if stmt.ColumnType(8) != sqlite.TypeNull {
|
|
title.AchievementID = uint32(stmt.ColumnInt64(8))
|
|
}
|
|
|
|
titles = append(titles, title)
|
|
return nil
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load 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
|
|
endFn, err := sqlitex.ImmediateTransaction(db.conn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start transaction: %w", err)
|
|
}
|
|
defer endFn(&err)
|
|
|
|
// Clear existing titles
|
|
if err := sqlitex.Execute(db.conn, "DELETE FROM titles", &sqlitex.ExecOptions{}); 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 := sqlitex.Execute(db.conn, insertQuery, &sqlitex.ExecOptions{
|
|
Args: []any{
|
|
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 = ?
|
|
`
|
|
|
|
err := sqlitex.Execute(db.conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{playerID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
playerTitle := &PlayerTitle{
|
|
TitleID: int32(stmt.ColumnInt64(0)),
|
|
PlayerID: playerID,
|
|
EarnedDate: time.Unix(stmt.ColumnInt64(2), 0),
|
|
}
|
|
|
|
// Handle nullable achievement_id
|
|
if stmt.ColumnType(1) != sqlite.TypeNull {
|
|
playerTitle.AchievementID = uint32(stmt.ColumnInt64(1))
|
|
}
|
|
|
|
// Handle nullable expiration_date
|
|
if stmt.ColumnType(3) != sqlite.TypeNull {
|
|
playerTitle.ExpiresAt = time.Unix(stmt.ColumnInt64(3), 0)
|
|
}
|
|
|
|
playerTitles = append(playerTitles, playerTitle)
|
|
return nil
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load 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
|
|
endFn, err := sqlitex.ImmediateTransaction(db.conn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start transaction: %w", err)
|
|
}
|
|
defer endFn(&err)
|
|
|
|
// Clear existing titles for this player
|
|
deleteQuery := "DELETE FROM player_titles WHERE player_id = ?"
|
|
if err := sqlitex.Execute(db.conn, deleteQuery, &sqlitex.ExecOptions{
|
|
Args: []any{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 := sqlitex.Execute(db.conn, insertQuery, &sqlitex.ExecOptions{
|
|
Args: []any{
|
|
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
|
|
`
|
|
|
|
err = sqlitex.Execute(db.conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{playerID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
titleID := int32(stmt.ColumnInt64(0))
|
|
position := int32(stmt.ColumnInt(1))
|
|
|
|
if position == TitlePositionPrefix {
|
|
prefixID = titleID
|
|
} else if position == TitlePositionSuffix {
|
|
suffixID = titleID
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("failed to get 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()
|
|
} |