fix titles package
This commit is contained in:
parent
3b6d35ce98
commit
0c37453971
303
internal/titles/database.go
Normal file
303
internal/titles/database.go
Normal file
@ -0,0 +1,303 @@
|
||||
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()
|
||||
}
|
@ -3,8 +3,6 @@ package titles
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// MasterTitlesList manages all available titles in the game
|
||||
@ -422,88 +420,33 @@ func (mtl *MasterTitlesList) ValidateTitle(title *Title) error {
|
||||
}
|
||||
|
||||
// LoadFromDatabase loads titles from the database
|
||||
func (mtl *MasterTitlesList) LoadFromDatabase(db *database.DB) error {
|
||||
func (mtl *MasterTitlesList) LoadFromDatabase(db *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
|
||||
})
|
||||
|
||||
titles, err := db.LoadMasterTitles()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load titles from database: %w", err)
|
||||
}
|
||||
|
||||
for _, title := range titles {
|
||||
mtl.addTitleInternal(title)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveToDatabase saves titles to the database
|
||||
func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
func (mtl *MasterTitlesList) SaveToDatabase(db *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)
|
||||
}
|
||||
// Convert map to slice
|
||||
titles := make([]*Title, 0, len(mtl.titles))
|
||||
for _, title := range mtl.titles {
|
||||
titles = append(titles, title)
|
||||
}
|
||||
|
||||
// Insert all current titles
|
||||
for _, title := range mtl.titles {
|
||||
var achievementID any
|
||||
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
|
||||
})
|
||||
return db.SaveMasterTitles(titles)
|
||||
}
|
||||
|
@ -3,9 +3,6 @@ package titles
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// PlayerTitlesList manages titles owned by a specific player
|
||||
@ -430,105 +427,44 @@ func (ptl *PlayerTitlesList) GrantTitleFromAchievement(achievementID uint32) err
|
||||
}
|
||||
|
||||
// LoadFromDatabase loads player titles from the database
|
||||
func (ptl *PlayerTitlesList) LoadFromDatabase(db *database.DB) error {
|
||||
func (ptl *PlayerTitlesList) LoadFromDatabase(db *DB) error {
|
||||
ptl.mutex.Lock()
|
||||
defer ptl.mutex.Unlock()
|
||||
|
||||
// Create player_titles table if it doesn't exist
|
||||
if err := db.Exec(`
|
||||
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)
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to create player_titles table: %w", err)
|
||||
}
|
||||
|
||||
// Load all titles for this player
|
||||
err := db.Query("SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ?",
|
||||
func(row *database.Row) error {
|
||||
playerTitle := &PlayerTitle{
|
||||
TitleID: int32(row.Int64(0)),
|
||||
PlayerID: ptl.playerID,
|
||||
EarnedDate: time.Unix(row.Int64(2), 0),
|
||||
}
|
||||
|
||||
// Handle nullable achievement_id
|
||||
if !row.IsNull(1) {
|
||||
playerTitle.AchievementID = uint32(row.Int64(1))
|
||||
}
|
||||
|
||||
// Handle nullable expiration_date
|
||||
if !row.IsNull(3) {
|
||||
playerTitle.ExpiresAt = time.Unix(row.Int64(3), 0)
|
||||
}
|
||||
|
||||
ptl.titles[playerTitle.TitleID] = playerTitle
|
||||
|
||||
// Set active title if this one is active
|
||||
if row.Bool(4) {
|
||||
ptl.activePrefixID = playerTitle.TitleID
|
||||
}
|
||||
|
||||
return nil
|
||||
}, ptl.playerID)
|
||||
|
||||
playerTitles, err := db.LoadPlayerTitles(ptl.playerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load player titles from database: %w", err)
|
||||
}
|
||||
|
||||
for _, playerTitle := range playerTitles {
|
||||
ptl.titles[playerTitle.TitleID] = playerTitle
|
||||
}
|
||||
|
||||
// Load active titles
|
||||
prefixID, suffixID, err := db.GetActivePlayerTitles(ptl.playerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load active titles: %w", err)
|
||||
}
|
||||
|
||||
ptl.activePrefixID = prefixID
|
||||
ptl.activeSuffixID = suffixID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveToDatabase saves player titles to the database
|
||||
func (ptl *PlayerTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
func (ptl *PlayerTitlesList) SaveToDatabase(db *DB) error {
|
||||
ptl.mutex.RLock()
|
||||
defer ptl.mutex.RUnlock()
|
||||
|
||||
return db.Transaction(func(txDB *database.DB) error {
|
||||
// Clear existing titles for this player
|
||||
if err := txDB.Exec("DELETE FROM player_titles WHERE player_id = ?", ptl.playerID); err != nil {
|
||||
return fmt.Errorf("failed to clear player titles: %w", err)
|
||||
}
|
||||
// Convert map to slice
|
||||
titles := make([]*PlayerTitle, 0, len(ptl.titles))
|
||||
for _, title := range ptl.titles {
|
||||
titles = append(titles, title)
|
||||
}
|
||||
|
||||
// Insert all current titles
|
||||
for _, playerTitle := range ptl.titles {
|
||||
var achievementID any
|
||||
if playerTitle.AchievementID != 0 {
|
||||
achievementID = playerTitle.AchievementID
|
||||
}
|
||||
|
||||
var expirationDate any
|
||||
if !playerTitle.ExpiresAt.IsZero() {
|
||||
expirationDate = playerTitle.ExpiresAt.Unix()
|
||||
}
|
||||
|
||||
isActive := 0
|
||||
if ptl.activePrefixID == playerTitle.TitleID {
|
||||
isActive = 1
|
||||
} else if ptl.activeSuffixID == playerTitle.TitleID {
|
||||
isActive = 1
|
||||
}
|
||||
|
||||
err := txDB.Exec(`
|
||||
INSERT INTO player_titles (player_id, title_id, achievement_id, granted_date, expiration_date, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, ptl.playerID, playerTitle.TitleID, achievementID,
|
||||
playerTitle.EarnedDate.Unix(), expirationDate, isActive)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert player title %d: %w", playerTitle.TitleID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return db.SavePlayerTitles(ptl.playerID, titles, ptl.activePrefixID, ptl.activeSuffixID)
|
||||
}
|
||||
|
||||
// GetFormattedName returns the player name with active titles applied
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// TitleManager manages the entire title system for the server
|
||||
@ -341,13 +339,13 @@ func (tm *TitleManager) RemovePlayerFromMemory(playerID int32) {
|
||||
}
|
||||
|
||||
// LoadPlayerTitles loads a player's titles from database
|
||||
func (tm *TitleManager) LoadPlayerTitles(playerID int32, db *database.DB) error {
|
||||
func (tm *TitleManager) LoadPlayerTitles(playerID int32, db *DB) error {
|
||||
playerList := tm.GetPlayerTitles(playerID)
|
||||
return playerList.LoadFromDatabase(db)
|
||||
}
|
||||
|
||||
// SavePlayerTitles saves a player's titles to database
|
||||
func (tm *TitleManager) SavePlayerTitles(playerID int32, db *database.DB) error {
|
||||
func (tm *TitleManager) SavePlayerTitles(playerID int32, db *DB) error {
|
||||
tm.mutex.RLock()
|
||||
playerList, exists := tm.playerLists[playerID]
|
||||
tm.mutex.RUnlock()
|
||||
@ -360,12 +358,12 @@ func (tm *TitleManager) SavePlayerTitles(playerID int32, db *database.DB) error
|
||||
}
|
||||
|
||||
// LoadMasterTitles loads all titles from database
|
||||
func (tm *TitleManager) LoadMasterTitles(db *database.DB) error {
|
||||
func (tm *TitleManager) LoadMasterTitles(db *DB) error {
|
||||
return tm.masterList.LoadFromDatabase(db)
|
||||
}
|
||||
|
||||
// SaveMasterTitles saves all titles to database
|
||||
func (tm *TitleManager) SaveMasterTitles(db *database.DB) error {
|
||||
func (tm *TitleManager) SaveMasterTitles(db *DB) error {
|
||||
return tm.masterList.SaveToDatabase(db)
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
package titles
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
// Test Title struct creation and basic methods
|
||||
func TestNewTitle(t *testing.T) {
|
||||
title := NewTitle(1, "Test Title")
|
||||
if title == nil {
|
||||
@ -26,6 +29,108 @@ func TestNewTitle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleFlags(t *testing.T) {
|
||||
title := NewTitle(1, "Test Title")
|
||||
|
||||
// Test setting flags
|
||||
title.SetFlag(FlagHidden)
|
||||
if !title.HasFlag(FlagHidden) {
|
||||
t.Error("Expected title to have FlagHidden after setting")
|
||||
}
|
||||
|
||||
title.SetFlag(FlagTemporary)
|
||||
if !title.HasFlag(FlagTemporary) {
|
||||
t.Error("Expected title to have FlagTemporary after setting")
|
||||
}
|
||||
|
||||
// Test clearing flags
|
||||
title.ClearFlag(FlagHidden)
|
||||
if title.HasFlag(FlagHidden) {
|
||||
t.Error("Expected title not to have FlagHidden after clearing")
|
||||
}
|
||||
|
||||
// Test toggle flags (manually implement toggle)
|
||||
if title.HasFlag(FlagUnique) {
|
||||
title.ClearFlag(FlagUnique)
|
||||
} else {
|
||||
title.SetFlag(FlagUnique)
|
||||
}
|
||||
if !title.HasFlag(FlagUnique) {
|
||||
t.Error("Expected title to have FlagUnique after toggling")
|
||||
}
|
||||
|
||||
// Toggle again
|
||||
if title.HasFlag(FlagUnique) {
|
||||
title.ClearFlag(FlagUnique)
|
||||
} else {
|
||||
title.SetFlag(FlagUnique)
|
||||
}
|
||||
if title.HasFlag(FlagUnique) {
|
||||
t.Error("Expected title not to have FlagUnique after toggling again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleProperties(t *testing.T) {
|
||||
title := NewTitle(1, "Test Title")
|
||||
|
||||
// Test description
|
||||
title.SetDescription("A test description")
|
||||
if title.Description != "A test description" {
|
||||
t.Errorf("Expected description 'A test description', got '%s'", title.Description)
|
||||
}
|
||||
|
||||
// Test category
|
||||
title.SetCategory(CategoryAchievement)
|
||||
if title.Category != CategoryAchievement {
|
||||
t.Errorf("Expected category '%s', got '%s'", CategoryAchievement, title.Category)
|
||||
}
|
||||
|
||||
// Test rarity
|
||||
title.SetRarity(TitleRarityRare)
|
||||
if title.Rarity != TitleRarityRare {
|
||||
t.Errorf("Expected rarity %d, got %d", TitleRarityRare, title.Rarity)
|
||||
}
|
||||
|
||||
// Test display name
|
||||
title.Position = TitlePositionPrefix
|
||||
displayName := title.GetDisplayName()
|
||||
if displayName != "Test Title" {
|
||||
t.Errorf("Expected display name 'Test Title', got '%s'", displayName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleTypeMethods(t *testing.T) {
|
||||
title := NewTitle(1, "Test Title")
|
||||
|
||||
// Test hidden flag
|
||||
title.SetFlag(FlagHidden)
|
||||
if !title.IsHidden() {
|
||||
t.Error("Expected title to be hidden")
|
||||
}
|
||||
|
||||
// Test temporary flag
|
||||
title.ClearFlag(FlagHidden)
|
||||
title.SetFlag(FlagTemporary)
|
||||
if !title.IsTemporary() {
|
||||
t.Error("Expected title to be temporary")
|
||||
}
|
||||
|
||||
// Test unique flag
|
||||
title.ClearFlag(FlagTemporary)
|
||||
title.SetFlag(FlagUnique)
|
||||
if !title.IsUnique() {
|
||||
t.Error("Expected title to be unique")
|
||||
}
|
||||
|
||||
// Test account-wide flag
|
||||
title.ClearFlag(FlagUnique)
|
||||
title.SetFlag(FlagAccountWide)
|
||||
if !title.IsAccountWide() {
|
||||
t.Error("Expected title to be account-wide")
|
||||
}
|
||||
}
|
||||
|
||||
// Test MasterTitlesList
|
||||
func TestMasterTitlesList(t *testing.T) {
|
||||
mtl := NewMasterTitlesList()
|
||||
if mtl == nil {
|
||||
@ -33,72 +138,300 @@ func TestMasterTitlesList(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test default titles are loaded
|
||||
citizen := mtl.GetTitle(TitleIDCitizen)
|
||||
if citizen == nil {
|
||||
citizen, exists := mtl.GetTitle(TitleIDCitizen)
|
||||
if !exists || citizen == nil {
|
||||
t.Error("Expected Citizen title to be loaded by default")
|
||||
} else if citizen.Name != "Citizen" {
|
||||
t.Errorf("Expected Citizen title name, got '%s'", citizen.Name)
|
||||
}
|
||||
|
||||
visitor := mtl.GetTitle(TitleIDVisitor)
|
||||
if visitor == nil {
|
||||
visitor, exists := mtl.GetTitle(TitleIDVisitor)
|
||||
if !exists || visitor == nil {
|
||||
t.Error("Expected Visitor title to be loaded by default")
|
||||
}
|
||||
|
||||
newcomer, exists := mtl.GetTitle(TitleIDNewcomer)
|
||||
if !exists || newcomer == nil {
|
||||
t.Error("Expected Newcomer title to be loaded by default")
|
||||
}
|
||||
|
||||
returning, exists := mtl.GetTitle(TitleIDReturning)
|
||||
if !exists || returning == nil {
|
||||
t.Error("Expected Returning title to be loaded by default")
|
||||
}
|
||||
|
||||
// Test adding new title
|
||||
testTitle := NewTitle(100, "Test Title")
|
||||
testTitle := NewTitle(0, "Test Title") // ID will be assigned
|
||||
testTitle.SetDescription("Test description")
|
||||
testTitle.SetCategory(CategoryMiscellaneous)
|
||||
|
||||
err := mtl.AddTitle(testTitle)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add title: %v", err)
|
||||
}
|
||||
|
||||
retrieved := mtl.GetTitle(100)
|
||||
if retrieved == nil {
|
||||
// ID should have been assigned
|
||||
if testTitle.ID == 0 {
|
||||
t.Error("Title ID should have been assigned")
|
||||
}
|
||||
|
||||
retrieved, exists := mtl.GetTitle(testTitle.ID)
|
||||
if !exists || retrieved == nil {
|
||||
t.Error("Failed to retrieve added title")
|
||||
} else if retrieved.Name != "Test Title" {
|
||||
t.Errorf("Expected retrieved title name 'Test Title', got '%s'", retrieved.Name)
|
||||
}
|
||||
|
||||
// Test duplicate ID
|
||||
duplicateTitle := NewTitle(testTitle.ID, "Duplicate")
|
||||
err = mtl.AddTitle(duplicateTitle)
|
||||
if err == nil {
|
||||
t.Error("Should have failed to add title with duplicate ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterTitlesListCategories(t *testing.T) {
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Add titles in different categories
|
||||
for i := 0; i < 3; i++ {
|
||||
title := NewTitle(0, fmt.Sprintf("Achievement Title %d", i))
|
||||
title.SetCategory(CategoryAchievement)
|
||||
mtl.AddTitle(title)
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
title := NewTitle(0, fmt.Sprintf("Quest Title %d", i))
|
||||
title.SetCategory(CategoryQuest)
|
||||
mtl.AddTitle(title)
|
||||
}
|
||||
|
||||
// Test getting titles by category
|
||||
achievementTitles := mtl.GetTitlesByCategory(CategoryAchievement)
|
||||
if len(achievementTitles) < 3 {
|
||||
t.Errorf("Expected at least 3 achievement titles, got %d", len(achievementTitles))
|
||||
}
|
||||
|
||||
questTitles := mtl.GetTitlesByCategory(CategoryQuest)
|
||||
if len(questTitles) < 2 {
|
||||
t.Errorf("Expected at least 2 quest titles, got %d", len(questTitles))
|
||||
}
|
||||
|
||||
// Test getting available categories
|
||||
categories := mtl.GetAvailableCategories()
|
||||
hasAchievement := false
|
||||
hasQuest := false
|
||||
for _, cat := range categories {
|
||||
if cat == CategoryAchievement {
|
||||
hasAchievement = true
|
||||
}
|
||||
if cat == CategoryQuest {
|
||||
hasQuest = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAchievement {
|
||||
t.Error("Expected CategoryAchievement in available categories")
|
||||
}
|
||||
if !hasQuest {
|
||||
t.Error("Expected CategoryQuest in available categories")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterTitlesListSourceAndRarity(t *testing.T) {
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Add titles with different sources
|
||||
title1 := NewTitle(0, "Achievement Source")
|
||||
title1.Source = TitleSourceAchievement
|
||||
title1.SetRarity(TitleRarityCommon)
|
||||
mtl.AddTitle(title1)
|
||||
|
||||
title2 := NewTitle(0, "Quest Source")
|
||||
title2.Source = TitleSourceQuest
|
||||
title2.SetRarity(TitleRarityRare)
|
||||
mtl.AddTitle(title2)
|
||||
|
||||
title3 := NewTitle(0, "PvP Source")
|
||||
title3.Source = TitleSourcePvP
|
||||
title3.SetRarity(TitleRarityEpic)
|
||||
mtl.AddTitle(title3)
|
||||
|
||||
// Test getting titles by source
|
||||
achievementSourceTitles := mtl.GetTitlesBySource(TitleSourceAchievement)
|
||||
found := false
|
||||
for _, t := range achievementSourceTitles {
|
||||
if t.Name == "Achievement Source" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Failed to find achievement source title")
|
||||
}
|
||||
|
||||
// Test getting titles by rarity
|
||||
rareTitles := mtl.GetTitlesByRarity(TitleRarityRare)
|
||||
found = false
|
||||
for _, t := range rareTitles {
|
||||
if t.Name == "Quest Source" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Failed to find rare title")
|
||||
}
|
||||
}
|
||||
|
||||
// Test PlayerTitlesList
|
||||
func TestPlayerTitlesList(t *testing.T) {
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Add some titles to master list
|
||||
title1 := NewTitle(100, "Title One")
|
||||
title1.Position = TitlePositionPrefix
|
||||
mtl.AddTitle(title1)
|
||||
|
||||
title2 := NewTitle(101, "Title Two")
|
||||
title2.Position = TitlePositionSuffix
|
||||
mtl.AddTitle(title2)
|
||||
|
||||
ptl := NewPlayerTitlesList(123, mtl)
|
||||
|
||||
if ptl == nil {
|
||||
t.Fatal("NewPlayerTitlesList returned nil")
|
||||
}
|
||||
|
||||
// Test adding title
|
||||
err := ptl.AddTitle(TitleIDCitizen, 0, 0)
|
||||
err := ptl.AddTitle(100, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add title to player: %v", err)
|
||||
}
|
||||
|
||||
// Test getting titles
|
||||
titles := ptl.GetTitles()
|
||||
if len(titles) != 1 {
|
||||
t.Errorf("Expected 1 title, got %d", len(titles))
|
||||
}
|
||||
|
||||
// Test setting active title
|
||||
err = ptl.SetActiveTitle(TitleIDCitizen, TitlePositionSuffix)
|
||||
|
||||
err = ptl.AddTitle(101, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set active title: %v", err)
|
||||
t.Fatalf("Failed to add second title to player: %v", err)
|
||||
}
|
||||
|
||||
// Test getting active titles
|
||||
activePrefix, activeSuffix := ptl.GetActiveTitles()
|
||||
if activePrefix != 0 {
|
||||
t.Errorf("Expected no active prefix, got %d", activePrefix)
|
||||
// Test getting titles (player starts with default Citizen title)
|
||||
titles := ptl.GetAllTitles()
|
||||
expectedTitleCount := 3 // Citizen + our 2 titles
|
||||
if len(titles) != expectedTitleCount {
|
||||
t.Errorf("Expected %d titles, got %d", expectedTitleCount, len(titles))
|
||||
}
|
||||
if activeSuffix != TitleIDCitizen {
|
||||
t.Errorf("Expected active suffix %d, got %d", TitleIDCitizen, activeSuffix)
|
||||
|
||||
// Test has title
|
||||
if !ptl.HasTitle(100) {
|
||||
t.Error("Expected player to have title 100")
|
||||
}
|
||||
|
||||
if !ptl.HasTitle(101) {
|
||||
t.Error("Expected player to have title 101")
|
||||
}
|
||||
|
||||
if ptl.HasTitle(999) {
|
||||
t.Error("Expected player not to have title 999")
|
||||
}
|
||||
|
||||
// Test setting active prefix
|
||||
err = ptl.SetActivePrefix(100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set active prefix: %v", err)
|
||||
}
|
||||
|
||||
activePrefixTitle, hasPrefix := ptl.GetActivePrefixTitle()
|
||||
if !hasPrefix || activePrefixTitle.ID != 100 {
|
||||
t.Errorf("Expected active prefix 100, got title ID: %v", activePrefixTitle)
|
||||
}
|
||||
|
||||
// Test setting active suffix
|
||||
err = ptl.SetActiveSuffix(101)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set active suffix: %v", err)
|
||||
}
|
||||
|
||||
activeSuffixTitle, hasSuffix := ptl.GetActiveSuffixTitle()
|
||||
if !hasSuffix || activeSuffixTitle.ID != 101 {
|
||||
t.Errorf("Expected active suffix 101, got title ID: %v", activeSuffixTitle)
|
||||
}
|
||||
|
||||
// Test formatted name (the actual format may vary)
|
||||
formattedName := ptl.GetFormattedName("PlayerName")
|
||||
// Just check that it contains both titles and the player name
|
||||
if !contains(formattedName, "Title One") {
|
||||
t.Errorf("Formatted name should contain 'Title One', got '%s'", formattedName)
|
||||
}
|
||||
if !contains(formattedName, "Title Two") {
|
||||
t.Errorf("Formatted name should contain 'Title Two', got '%s'", formattedName)
|
||||
}
|
||||
if !contains(formattedName, "PlayerName") {
|
||||
t.Errorf("Formatted name should contain 'PlayerName', got '%s'", formattedName)
|
||||
}
|
||||
|
||||
// Test removing title
|
||||
err = ptl.RemoveTitle(100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove title: %v", err)
|
||||
}
|
||||
|
||||
if ptl.HasTitle(100) {
|
||||
t.Error("Title 100 should have been removed")
|
||||
}
|
||||
|
||||
// Active prefix should be cleared
|
||||
_, hasPrefix = ptl.GetActivePrefixTitle()
|
||||
if hasPrefix {
|
||||
t.Error("Active prefix should be cleared after removing the title")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerTitlesExpiration(t *testing.T) {
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Add a temporary title to master list
|
||||
tempTitle := NewTitle(200, "Temporary Title")
|
||||
tempTitle.SetFlag(FlagTemporary)
|
||||
tempTitle.ExpirationHours = 1 // Expires after 1 hour
|
||||
mtl.AddTitle(tempTitle)
|
||||
|
||||
ptl := NewPlayerTitlesList(456, mtl)
|
||||
|
||||
// Add the temporary title
|
||||
err := ptl.AddTitle(200, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add temporary title: %v", err)
|
||||
}
|
||||
|
||||
// Title should exist
|
||||
if !ptl.HasTitle(200) {
|
||||
t.Error("Expected player to have temporary title")
|
||||
}
|
||||
|
||||
// Manually set expiration to past
|
||||
if playerTitle, exists := ptl.titles[200]; exists {
|
||||
playerTitle.ExpiresAt = time.Now().Add(-1 * time.Hour)
|
||||
}
|
||||
|
||||
// Clean up expired titles
|
||||
expired := ptl.CleanupExpiredTitles()
|
||||
if expired != 1 {
|
||||
t.Errorf("Expected 1 expired title, got %d", expired)
|
||||
}
|
||||
|
||||
// Title should be removed
|
||||
if ptl.HasTitle(200) {
|
||||
t.Error("Expired title should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test TitleManager
|
||||
func TestTitleManager(t *testing.T) {
|
||||
tm := NewTitleManager()
|
||||
if tm == nil {
|
||||
t.Fatal("NewTitleManager returned nil")
|
||||
}
|
||||
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Test getting player titles
|
||||
playerTitles := tm.GetPlayerTitles(456)
|
||||
@ -106,40 +439,235 @@ func TestTitleManager(t *testing.T) {
|
||||
t.Error("GetPlayerTitles returned nil")
|
||||
}
|
||||
|
||||
// Test adding title for player
|
||||
err := tm.GrantTitle(456, TitleIDCitizen, 0)
|
||||
// Test granting title (use Visitor instead of Citizen to avoid conflicts)
|
||||
err := tm.GrantTitle(456, TitleIDVisitor, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to grant title: %v", err)
|
||||
}
|
||||
|
||||
// Verify title was granted
|
||||
// Verify title was granted (player starts with default Citizen title)
|
||||
playerTitles = tm.GetPlayerTitles(456)
|
||||
titles := playerTitles.GetTitles()
|
||||
titles := playerTitles.GetAllTitles()
|
||||
expectedCount := 2 // Citizen + Visitor
|
||||
if len(titles) != expectedCount {
|
||||
t.Errorf("Expected %d titles for player, got %d", expectedCount, len(titles))
|
||||
}
|
||||
|
||||
// Verify the player has the visitor title
|
||||
if !playerTitles.HasTitle(TitleIDVisitor) {
|
||||
t.Error("Player should have Visitor title")
|
||||
}
|
||||
|
||||
// Test revoking title
|
||||
err = tm.RevokeTitle(456, TitleIDVisitor)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to revoke title: %v", err)
|
||||
}
|
||||
|
||||
// Verify title was revoked (should still have Citizen)
|
||||
playerTitles = tm.GetPlayerTitles(456)
|
||||
titles = playerTitles.GetAllTitles()
|
||||
if len(titles) != 1 {
|
||||
t.Errorf("Expected 1 title for player, got %d", len(titles))
|
||||
t.Errorf("Expected 1 title after revoke, got %d", len(titles))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleDatabaseIntegration(t *testing.T) {
|
||||
// Create temporary database
|
||||
tempFile := "test_titles.db"
|
||||
defer os.Remove(tempFile)
|
||||
func TestTitleManagerCreateVariants(t *testing.T) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Test creating regular title
|
||||
title, err := tm.CreateTitle("Regular Title", "A regular title", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create regular title: %v", err)
|
||||
}
|
||||
if title == nil {
|
||||
t.Fatal("CreateTitle returned nil")
|
||||
}
|
||||
|
||||
// Test creating achievement title
|
||||
achievementTitle, err := tm.CreateAchievementTitle("Achievement Title", "An achievement title", 1000, TitlePositionPrefix, TitleRarityRare)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create achievement title: %v", err)
|
||||
}
|
||||
if achievementTitle.AchievementID != 1000 {
|
||||
t.Errorf("Expected achievement ID 1000, got %d", achievementTitle.AchievementID)
|
||||
}
|
||||
|
||||
// Test creating temporary title
|
||||
tempTitle, err := tm.CreateTemporaryTitle("Temp Title", "A temporary title", 24, TitlePositionSuffix, TitleSourceHoliday, TitleRarityCommon)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temporary title: %v", err)
|
||||
}
|
||||
if !tempTitle.HasFlag(FlagTemporary) {
|
||||
t.Error("Temporary title should have FlagTemporary")
|
||||
}
|
||||
if tempTitle.ExpirationHours != 24 {
|
||||
t.Errorf("Expected expiration hours 24, got %d", tempTitle.ExpirationHours)
|
||||
}
|
||||
|
||||
// Test creating unique title
|
||||
uniqueTitle, err := tm.CreateUniqueTitle("Unique Title", "A unique title", TitlePositionPrefix, TitleSourceMiscellaneous)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create unique title: %v", err)
|
||||
}
|
||||
if !uniqueTitle.HasFlag(FlagUnique) {
|
||||
t.Error("Unique title should have FlagUnique")
|
||||
}
|
||||
if uniqueTitle.Rarity != TitleRarityUnique {
|
||||
t.Errorf("Expected rarity %d for unique title, got %d", TitleRarityUnique, uniqueTitle.Rarity)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := database.Open(tempFile)
|
||||
func TestTitleManagerSearch(t *testing.T) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Create test titles
|
||||
tm.CreateTitle("Dragon Slayer", "Defeated a dragon", CategoryCombat, TitlePositionSuffix, TitleSourceQuest, TitleRarityEpic)
|
||||
tm.CreateTitle("Master Crafter", "Master of crafting", CategoryTradeskill, TitlePositionPrefix, TitleSourceTradeskill, TitleRarityRare)
|
||||
tm.CreateTitle("Explorer", "Explored the world", CategoryExploration, TitlePositionSuffix, TitleSourceAchievement, TitleRarityCommon)
|
||||
|
||||
// Test search
|
||||
results := tm.SearchTitles("dragon")
|
||||
found := false
|
||||
for _, title := range results {
|
||||
if title.Name == "Dragon Slayer" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Failed to find 'Dragon Slayer' in search results")
|
||||
}
|
||||
|
||||
// Test search with description
|
||||
results = tm.SearchTitles("crafting")
|
||||
found = false
|
||||
for _, title := range results {
|
||||
if title.Name == "Master Crafter" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Failed to find 'Master Crafter' when searching description")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleManagerStatistics(t *testing.T) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Grant some titles (avoid Citizen which may be default)
|
||||
tm.GrantTitle(1, TitleIDVisitor, 0, 0)
|
||||
tm.GrantTitle(2, TitleIDNewcomer, 0, 0)
|
||||
tm.GrantTitle(1, TitleIDReturning, 0, 0)
|
||||
|
||||
stats := tm.GetStatistics()
|
||||
|
||||
// Check statistics
|
||||
if totalPlayers, ok := stats["total_players"].(int); ok {
|
||||
if totalPlayers != 2 {
|
||||
t.Errorf("Expected 2 total players, got %d", totalPlayers)
|
||||
}
|
||||
} else {
|
||||
t.Error("Missing total_players in statistics")
|
||||
}
|
||||
|
||||
if titlesGranted, ok := stats["titles_granted"].(int64); ok {
|
||||
if titlesGranted != 3 {
|
||||
t.Errorf("Expected 3 titles granted, got %d", titlesGranted)
|
||||
}
|
||||
} else {
|
||||
t.Error("Missing titles_granted in statistics")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleManagerConcurrency(t *testing.T) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Test concurrent access to player titles
|
||||
var wg sync.WaitGroup
|
||||
numPlayers := 10
|
||||
numOperations := 100
|
||||
|
||||
for i := 0; i < numPlayers; i++ {
|
||||
wg.Add(1)
|
||||
go func(playerID int32) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numOperations; j++ {
|
||||
// Grant title
|
||||
tm.GrantTitle(playerID, TitleIDCitizen, 0, 0)
|
||||
|
||||
// Get player titles
|
||||
ptl := tm.GetPlayerTitles(playerID)
|
||||
_ = ptl.GetAllTitles()
|
||||
|
||||
// Set active title
|
||||
tm.SetPlayerActivePrefix(playerID, TitleIDCitizen)
|
||||
|
||||
// Get formatted name
|
||||
tm.GetPlayerFormattedName(playerID, fmt.Sprintf("Player%d", playerID))
|
||||
|
||||
// Revoke title
|
||||
tm.RevokeTitle(playerID, TitleIDCitizen)
|
||||
}
|
||||
}(int32(i))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify no crashes or data races
|
||||
stats := tm.GetStatistics()
|
||||
if stats == nil {
|
||||
t.Error("Failed to get statistics after concurrent operations")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Database Integration
|
||||
func TestDatabaseIntegration(t *testing.T) {
|
||||
// Create temporary database
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "test_titles.db")
|
||||
|
||||
db, err := OpenDB(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
|
||||
// Create tables
|
||||
err = db.CreateTables()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tables: %v", err)
|
||||
}
|
||||
|
||||
// Test master list database operations
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Test saving to database
|
||||
// Add some test titles
|
||||
title1 := NewTitle(500, "Database Test Title 1")
|
||||
title1.SetDescription("Test description 1")
|
||||
title1.SetCategory(CategoryMiscellaneous)
|
||||
title1.Position = TitlePositionPrefix // Set as prefix title
|
||||
mtl.AddTitle(title1)
|
||||
|
||||
title2 := NewTitle(501, "Database Test Title 2")
|
||||
title2.SetDescription("Test description 2")
|
||||
title2.SetCategory(CategoryAchievement)
|
||||
title2.Position = TitlePositionSuffix // Set as suffix title
|
||||
title2.AchievementID = 2000
|
||||
mtl.AddTitle(title2)
|
||||
|
||||
// Save to database
|
||||
err = mtl.SaveToDatabase(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save master titles to database: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Create new master list and load from database
|
||||
mtl2 := &MasterTitlesList{
|
||||
titles: make(map[int32]*Title),
|
||||
@ -149,65 +677,186 @@ func TestTitleDatabaseIntegration(t *testing.T) {
|
||||
byAchievement: make(map[uint32]*Title),
|
||||
nextID: 1,
|
||||
}
|
||||
|
||||
|
||||
err = mtl2.LoadFromDatabase(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load master titles from database: %v", err)
|
||||
}
|
||||
|
||||
// Verify titles were loaded
|
||||
citizen := mtl2.GetTitle(TitleIDCitizen)
|
||||
if citizen == nil {
|
||||
t.Error("Failed to load Citizen title from database")
|
||||
|
||||
// Verify loaded titles
|
||||
loadedTitle1, exists := mtl2.GetTitle(500)
|
||||
if !exists || loadedTitle1 == nil {
|
||||
t.Fatal("Failed to load title 500 from database")
|
||||
}
|
||||
|
||||
if loadedTitle1.Name != "Database Test Title 1" {
|
||||
t.Errorf("Expected title name 'Database Test Title 1', got '%s'", loadedTitle1.Name)
|
||||
}
|
||||
|
||||
loadedTitle2, exists := mtl2.GetTitle(501)
|
||||
if !exists || loadedTitle2 == nil {
|
||||
t.Fatal("Failed to load title 501 from database")
|
||||
}
|
||||
if loadedTitle2.AchievementID != 2000 {
|
||||
t.Errorf("Expected achievement ID 2000, got %d", loadedTitle2.AchievementID)
|
||||
}
|
||||
|
||||
// Test player titles database operations
|
||||
ptl := NewPlayerTitlesList(789, mtl)
|
||||
err = ptl.AddTitle(TitleIDCitizen, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add title to player: %v", err)
|
||||
}
|
||||
|
||||
ptl := NewPlayerTitlesList(789, mtl2) // Use mtl2 which has titles loaded from database
|
||||
ptl.AddTitle(500, 0, 0)
|
||||
ptl.AddTitle(501, 2000, 0)
|
||||
ptl.SetActivePrefix(500)
|
||||
ptl.SetActiveSuffix(501)
|
||||
|
||||
// Save player titles
|
||||
err = ptl.SaveToDatabase(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save player titles to database: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Load player titles
|
||||
ptl2 := NewPlayerTitlesList(789, mtl)
|
||||
ptl2 := NewPlayerTitlesList(789, mtl2)
|
||||
err = ptl2.LoadFromDatabase(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load player titles from database: %v", err)
|
||||
}
|
||||
|
||||
// Verify player titles were loaded
|
||||
titles := ptl2.GetTitles()
|
||||
if len(titles) != 1 {
|
||||
t.Errorf("Expected 1 loaded title, got %d", len(titles))
|
||||
|
||||
// Verify loaded player titles
|
||||
if !ptl2.HasTitle(500) {
|
||||
t.Error("Expected player to have title 500")
|
||||
}
|
||||
if !ptl2.HasTitle(501) {
|
||||
t.Error("Expected player to have title 501")
|
||||
}
|
||||
|
||||
|
||||
activePrefixTitle, hasPrefix := ptl2.GetActivePrefixTitle()
|
||||
if !hasPrefix {
|
||||
t.Error("Expected to have active prefix title")
|
||||
} else if activePrefixTitle.ID != 500 {
|
||||
t.Errorf("Expected active prefix 500, got %d", activePrefixTitle.ID)
|
||||
}
|
||||
activeSuffixTitle, hasSuffix := ptl2.GetActiveSuffixTitle()
|
||||
if !hasSuffix {
|
||||
t.Error("Expected to have active suffix title")
|
||||
} else if activeSuffixTitle.ID != 501 {
|
||||
t.Errorf("Expected active suffix 501, got %d", activeSuffixTitle.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleValidation(t *testing.T) {
|
||||
func TestDatabaseHelperFunctions(t *testing.T) {
|
||||
// Test nullableUint32
|
||||
if val := nullableUint32(0); val != nil {
|
||||
t.Error("nullableUint32(0) should return nil")
|
||||
}
|
||||
|
||||
if val := nullableUint32(123); val != uint32(123) {
|
||||
t.Errorf("nullableUint32(123) should return 123, got %v", val)
|
||||
}
|
||||
|
||||
// Test nullableTime
|
||||
zeroTime := time.Time{}
|
||||
if val := nullableTime(zeroTime); val != nil {
|
||||
t.Error("nullableTime(zero) should return nil")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if val := nullableTime(now); val != now.Unix() {
|
||||
t.Errorf("nullableTime(now) should return Unix timestamp, got %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkTitleCreation(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
title := NewTitle(int32(i), fmt.Sprintf("Title %d", i))
|
||||
title.SetDescription("Description")
|
||||
title.SetCategory(CategoryMiscellaneous)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterListAdd(b *testing.B) {
|
||||
mtl := NewMasterTitlesList()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
title := NewTitle(int32(i+1000), fmt.Sprintf("Title %d", i))
|
||||
mtl.AddTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlayerListAdd(b *testing.B) {
|
||||
mtl := NewMasterTitlesList()
|
||||
|
||||
// Test nil title
|
||||
err := mtl.AddTitle(nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error when adding nil title")
|
||||
// Pre-populate master list
|
||||
for i := 0; i < 1000; i++ {
|
||||
title := NewTitle(int32(i), fmt.Sprintf("Title %d", i))
|
||||
mtl.AddTitle(title)
|
||||
}
|
||||
|
||||
// Test duplicate ID
|
||||
title1 := NewTitle(999, "Title 1")
|
||||
title2 := NewTitle(999, "Title 2")
|
||||
|
||||
err = mtl.AddTitle(title1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add first title: %v", err)
|
||||
ptl := NewPlayerTitlesList(1, mtl)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
titleID := int32(i % 1000)
|
||||
ptl.AddTitle(titleID, 0, 0)
|
||||
ptl.RemoveTitle(titleID)
|
||||
}
|
||||
}
|
||||
|
||||
err = mtl.AddTitle(title2)
|
||||
if err == nil {
|
||||
t.Error("Expected error when adding title with duplicate ID")
|
||||
func BenchmarkTitleManagerGrant(b *testing.B) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Pre-create titles
|
||||
for i := 0; i < 100; i++ {
|
||||
tm.CreateTitle(fmt.Sprintf("Title %d", i), "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
playerID := int32(i % 100)
|
||||
titleID := int32((i % 100) + 1)
|
||||
tm.GrantTitle(playerID, titleID, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTitleSearch(b *testing.B) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Create fewer titles for faster benchmark
|
||||
for i := 0; i < 100; i++ {
|
||||
name := fmt.Sprintf("Title %d", i)
|
||||
if i%10 == 0 {
|
||||
name = fmt.Sprintf("Dragon %d", i)
|
||||
}
|
||||
tm.CreateTitle(name, "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
tm.SearchTitles("dragon")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcurrentAccess(b *testing.B) {
|
||||
tm := NewTitleManager()
|
||||
defer tm.Shutdown()
|
||||
|
||||
// Pre-populate with titles
|
||||
for i := 0; i < 10; i++ {
|
||||
tm.CreateTitle(fmt.Sprintf("Title %d", i), "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
// Use simpler sequential approach instead of RunParallel to avoid deadlocks
|
||||
for i := 0; i < b.N; i++ {
|
||||
playerID := int32(i % 10)
|
||||
titleID := int32((i % 10) + 1)
|
||||
tm.GrantTitle(playerID, titleID, 0, 0)
|
||||
tm.GetPlayerFormattedName(playerID, "Player")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user