remove redundant database wrapper, update achievements to use raw go-sqlite

This commit is contained in:
Sky Johnson 2025-08-02 09:54:32 -05:00
parent 0a390959fa
commit 1288bc086f
6 changed files with 1762 additions and 1142 deletions

View File

@ -1,301 +1,422 @@
package achievements package achievements
import ( import (
"eq2emu/internal/database" "context"
"fmt" "fmt"
"time" "time"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
) )
// LoadAllAchievements loads all achievements from database into master list // LoadAllAchievements loads all achievements from database into master list
func LoadAllAchievements(db *database.DB, masterList *MasterList) error { func LoadAllAchievements(pool *sqlitex.Pool, masterList *MasterList) error {
conn, err := pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer pool.Put(conn)
query := `SELECT achievement_id, title, uncompleted_text, completed_text, query := `SELECT achievement_id, title, uncompleted_text, completed_text,
category, expansion, icon, point_value, qty_req, hide_achievement, category, expansion, icon, point_value, qty_req, hide_achievement,
unknown3a, unknown3b FROM achievements` unknown3a, unknown3b FROM achievements`
err := db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
achievement := NewAchievement() ResultFunc: func(stmt *sqlite.Stmt) error {
achievement.ID = uint32(row.Int(0)) achievement := NewAchievement()
achievement.Title = row.Text(1) achievement.ID = uint32(stmt.ColumnInt64(0))
achievement.UncompletedText = row.Text(2) achievement.Title = stmt.ColumnText(1)
achievement.CompletedText = row.Text(3) achievement.UncompletedText = stmt.ColumnText(2)
achievement.Category = row.Text(4) achievement.CompletedText = stmt.ColumnText(3)
achievement.Expansion = row.Text(5) achievement.Category = stmt.ColumnText(4)
achievement.Icon = uint16(row.Int(6)) achievement.Expansion = stmt.ColumnText(5)
achievement.PointValue = uint32(row.Int(7)) achievement.Icon = uint16(stmt.ColumnInt64(6))
achievement.QtyRequired = uint32(row.Int(8)) achievement.PointValue = uint32(stmt.ColumnInt32(7))
achievement.Hide = row.Bool(9) achievement.QtyRequired = uint32(stmt.ColumnInt64(8))
achievement.Unknown3A = uint32(row.Int(10)) achievement.Hide = stmt.ColumnInt64(9) != 0
achievement.Unknown3B = uint32(row.Int(11)) achievement.Unknown3A = uint32(stmt.ColumnInt64(10))
achievement.Unknown3B = uint32(stmt.ColumnInt64(11))
// Load requirements and rewards // Load requirements and rewards
if err := loadAchievementRequirements(db, achievement); err != nil { if err := loadAchievementRequirements(conn, achievement); err != nil {
return fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.ID, err) return fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.ID, err)
} }
if err := loadAchievementRewards(db, achievement); err != nil { if err := loadAchievementRewards(conn, achievement); err != nil {
return fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.ID, err) return fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.ID, err)
} }
if !masterList.AddAchievement(achievement) { if !masterList.AddAchievement(achievement) {
return fmt.Errorf("duplicate achievement ID: %d", achievement.ID) return fmt.Errorf("duplicate achievement ID: %d", achievement.ID)
} }
return nil return nil
},
}) })
return err
} }
// loadAchievementRequirements loads requirements for a specific achievement // loadAchievementRequirements loads requirements for a specific achievement
func loadAchievementRequirements(db *database.DB, achievement *Achievement) error { func loadAchievementRequirements(conn *sqlite.Conn, achievement *Achievement) error {
query := `SELECT achievement_id, name, qty_req query := `SELECT achievement_id, name, qty_req
FROM achievements_requirements FROM achievements_requirements
WHERE achievement_id = ?` WHERE achievement_id = ?`
return db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
req := Requirement{ Args: []any{achievement.ID},
AchievementID: uint32(row.Int(0)), ResultFunc: func(stmt *sqlite.Stmt) error {
Name: row.Text(1), req := Requirement{
QtyRequired: uint32(row.Int(2)), AchievementID: uint32(stmt.ColumnInt64(0)),
} Name: stmt.ColumnText(1),
achievement.AddRequirement(req) QtyRequired: uint32(stmt.ColumnInt64(2)),
return nil }
}, achievement.ID) achievement.AddRequirement(req)
return nil
},
})
} }
// loadAchievementRewards loads rewards for a specific achievement // loadAchievementRewards loads rewards for a specific achievement
func loadAchievementRewards(db *database.DB, achievement *Achievement) error { func loadAchievementRewards(conn *sqlite.Conn, achievement *Achievement) error {
query := `SELECT achievement_id, reward query := `SELECT achievement_id, reward
FROM achievements_rewards FROM achievements_rewards
WHERE achievement_id = ?` WHERE achievement_id = ?`
return db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
reward := Reward{ Args: []any{achievement.ID},
AchievementID: uint32(row.Int(0)), ResultFunc: func(stmt *sqlite.Stmt) error {
Reward: row.Text(1), reward := Reward{
} AchievementID: uint32(stmt.ColumnInt64(0)),
achievement.AddReward(reward) Reward: stmt.ColumnText(1),
return nil }
}, achievement.ID) achievement.AddReward(reward)
return nil
},
})
} }
// LoadPlayerAchievements loads player achievements from database // LoadPlayerAchievements loads player achievements from database
func LoadPlayerAchievements(db *database.DB, playerID uint32, playerList *PlayerList) error { func LoadPlayerAchievements(pool *sqlitex.Pool, playerID uint32, playerList *PlayerList) error {
conn, err := pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer pool.Put(conn)
query := `SELECT achievement_id, title, uncompleted_text, completed_text, query := `SELECT achievement_id, title, uncompleted_text, completed_text,
category, expansion, icon, point_value, qty_req, hide_achievement, category, expansion, icon, point_value, qty_req, hide_achievement,
unknown3a, unknown3b FROM achievements` unknown3a, unknown3b FROM achievements`
err := db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
achievement := NewAchievement() ResultFunc: func(stmt *sqlite.Stmt) error {
achievement.ID = uint32(row.Int(0)) achievement := NewAchievement()
achievement.Title = row.Text(1) achievement.ID = uint32(stmt.ColumnInt64(0))
achievement.UncompletedText = row.Text(2) achievement.Title = stmt.ColumnText(1)
achievement.CompletedText = row.Text(3) achievement.UncompletedText = stmt.ColumnText(2)
achievement.Category = row.Text(4) achievement.CompletedText = stmt.ColumnText(3)
achievement.Expansion = row.Text(5) achievement.Category = stmt.ColumnText(4)
achievement.Icon = uint16(row.Int(6)) achievement.Expansion = stmt.ColumnText(5)
achievement.PointValue = uint32(row.Int(7)) achievement.Icon = uint16(stmt.ColumnInt64(6))
achievement.QtyRequired = uint32(row.Int(8)) achievement.PointValue = uint32(stmt.ColumnInt64(7))
achievement.Hide = row.Bool(9) achievement.QtyRequired = uint32(stmt.ColumnInt64(8))
achievement.Unknown3A = uint32(row.Int(10)) achievement.Hide = stmt.ColumnInt64(9) != 0
achievement.Unknown3B = uint32(row.Int(11)) achievement.Unknown3A = uint32(stmt.ColumnInt64(10))
achievement.Unknown3B = uint32(stmt.ColumnInt64(11))
// Load requirements and rewards // Load requirements and rewards
if err := loadAchievementRequirements(db, achievement); err != nil { if err := loadAchievementRequirements(conn, achievement); err != nil {
return fmt.Errorf("failed to load requirements: %w", err) return fmt.Errorf("failed to load requirements: %w", err)
} }
if err := loadAchievementRewards(db, achievement); err != nil { if err := loadAchievementRewards(conn, achievement); err != nil {
return fmt.Errorf("failed to load rewards: %w", err) return fmt.Errorf("failed to load rewards: %w", err)
} }
if !playerList.AddAchievement(achievement) { if !playerList.AddAchievement(achievement) {
return fmt.Errorf("duplicate achievement ID: %d", achievement.ID) return fmt.Errorf("duplicate achievement ID: %d", achievement.ID)
} }
return nil return nil
},
}) })
return err
} }
// LoadPlayerAchievementUpdates loads player achievement progress from database // LoadPlayerAchievementUpdates loads player achievement progress from database
func LoadPlayerAchievementUpdates(db *database.DB, playerID uint32, updateList *PlayerUpdateList) error { func LoadPlayerAchievementUpdates(pool *sqlitex.Pool, playerID uint32, updateList *PlayerUpdateList) error {
conn, err := pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer pool.Put(conn)
query := `SELECT char_id, achievement_id, completed_date query := `SELECT char_id, achievement_id, completed_date
FROM character_achievements FROM character_achievements
WHERE char_id = ?` WHERE char_id = ?`
return db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
update := NewUpdate() Args: []any{playerID},
update.ID = uint32(row.Int(1)) ResultFunc: func(stmt *sqlite.Stmt) error {
update := NewUpdate()
update.ID = uint32(stmt.ColumnInt64(1))
// Convert completed_date from Unix timestamp // Convert completed_date from Unix timestamp
if !row.IsNull(2) { if stmt.ColumnType(2) != sqlite.TypeNull {
timestamp := row.Int64(2) timestamp := stmt.ColumnInt64(2)
update.CompletedDate = time.Unix(timestamp, 0) update.CompletedDate = time.Unix(timestamp, 0)
} }
// Load update items // Load update items
if err := loadPlayerAchievementUpdateItems(db, playerID, update); err != nil { if err := loadPlayerAchievementUpdateItems(conn, playerID, update); err != nil {
return fmt.Errorf("failed to load update items: %w", err) return fmt.Errorf("failed to load update items: %w", err)
} }
if !updateList.AddUpdate(update) { if !updateList.AddUpdate(update) {
return fmt.Errorf("duplicate achievement update ID: %d", update.ID) return fmt.Errorf("duplicate achievement update ID: %d", update.ID)
} }
return nil return nil
}, playerID) },
})
} }
// loadPlayerAchievementUpdateItems loads progress items for an achievement update // loadPlayerAchievementUpdateItems loads progress items for an achievement update
func loadPlayerAchievementUpdateItems(db *database.DB, playerID uint32, update *Update) error { func loadPlayerAchievementUpdateItems(conn *sqlite.Conn, playerID uint32, update *Update) error {
query := `SELECT achievement_id, items query := `SELECT achievement_id, items
FROM character_achievements_items FROM character_achievements_items
WHERE char_id = ? AND achievement_id = ?` WHERE char_id = ? AND achievement_id = ?`
return db.Query(query, func(row *database.Row) error { return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
item := UpdateItem{ Args: []any{playerID, update.ID},
AchievementID: uint32(row.Int(0)), ResultFunc: func(stmt *sqlite.Stmt) error {
ItemUpdate: uint32(row.Int(1)), item := UpdateItem{
} AchievementID: uint32(stmt.ColumnInt64(0)),
update.AddUpdateItem(item) ItemUpdate: uint32(stmt.ColumnInt64(1)),
return nil }
}, playerID, update.ID) update.AddUpdateItem(item)
return nil
},
})
} }
// SavePlayerAchievementUpdate saves or updates player achievement progress // SavePlayerAchievementUpdate saves or updates player achievement progress
func SavePlayerAchievementUpdate(db *database.DB, playerID uint32, update *Update) error { func SavePlayerAchievementUpdate(pool *sqlitex.Pool, playerID uint32, update *Update) error {
return db.Transaction(func(tx *database.DB) error { conn, err := pool.Take(context.Background())
// Save or update main achievement record if err != nil {
query := `INSERT OR REPLACE INTO character_achievements return fmt.Errorf("failed to get connection: %w", err)
(char_id, achievement_id, completed_date) VALUES (?, ?, ?)` }
defer pool.Put(conn)
var completedDate *int64 err = sqlitex.Execute(conn, "BEGIN", nil)
if !update.CompletedDate.IsZero() { if err != nil {
timestamp := update.CompletedDate.Unix() return fmt.Errorf("failed to begin transaction: %w", err)
completedDate = &timestamp }
} defer sqlitex.Execute(conn, "ROLLBACK", nil)
if err := tx.Exec(query, playerID, update.ID, completedDate); err != nil { // Save or update main achievement record
return fmt.Errorf("failed to save achievement update: %w", err) query := `INSERT OR REPLACE INTO character_achievements
} (char_id, achievement_id, completed_date) VALUES (?, ?, ?)`
// Delete existing update items var completedDate any
deleteQuery := `DELETE FROM character_achievements_items if !update.CompletedDate.IsZero() {
WHERE char_id = ? AND achievement_id = ?` completedDate = update.CompletedDate.Unix()
if err := tx.Exec(deleteQuery, playerID, update.ID); err != nil { }
return fmt.Errorf("failed to delete old update items: %w", err)
}
// Insert new update items err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
itemQuery := `INSERT INTO character_achievements_items Args: []any{playerID, update.ID, completedDate},
(char_id, achievement_id, items) VALUES (?, ?, ?)`
for _, item := range update.UpdateItems {
if err := tx.Exec(itemQuery, playerID, item.AchievementID, item.ItemUpdate); err != nil {
return fmt.Errorf("failed to save update item: %w", err)
}
}
return nil
}) })
if err != nil {
return fmt.Errorf("failed to save achievement update: %w", err)
}
// Delete existing update items
deleteQuery := `DELETE FROM character_achievements_items
WHERE char_id = ? AND achievement_id = ?`
err = sqlitex.Execute(conn, deleteQuery, &sqlitex.ExecOptions{
Args: []any{playerID, update.ID},
})
if err != nil {
return fmt.Errorf("failed to delete old update items: %w", err)
}
// Insert new update items
itemQuery := `INSERT INTO character_achievements_items
(char_id, achievement_id, items) VALUES (?, ?, ?)`
for _, item := range update.UpdateItems {
err = sqlitex.Execute(conn, itemQuery, &sqlitex.ExecOptions{
Args: []any{playerID, item.AchievementID, item.ItemUpdate},
})
if err != nil {
return fmt.Errorf("failed to save update item: %w", err)
}
}
return sqlitex.Execute(conn, "COMMIT", nil)
} }
// DeletePlayerAchievementUpdate removes player achievement progress from database // DeletePlayerAchievementUpdate removes player achievement progress from database
func DeletePlayerAchievementUpdate(db *database.DB, playerID uint32, achievementID uint32) error { func DeletePlayerAchievementUpdate(pool *sqlitex.Pool, playerID uint32, achievementID uint32) error {
return db.Transaction(func(tx *database.DB) error { conn, err := pool.Take(context.Background())
// Delete main achievement record if err != nil {
query := `DELETE FROM character_achievements return fmt.Errorf("failed to get connection: %w", err)
WHERE char_id = ? AND achievement_id = ?` }
if err := tx.Exec(query, playerID, achievementID); err != nil { defer pool.Put(conn)
return fmt.Errorf("failed to delete achievement update: %w", err)
}
// Delete update items err = sqlitex.Execute(conn, "BEGIN", nil)
itemQuery := `DELETE FROM character_achievements_items if err != nil {
WHERE char_id = ? AND achievement_id = ?` return fmt.Errorf("failed to begin transaction: %w", err)
if err := tx.Exec(itemQuery, playerID, achievementID); err != nil { }
return fmt.Errorf("failed to delete update items: %w", err) defer sqlitex.Execute(conn, "ROLLBACK", nil)
}
return nil // Delete main achievement record
query := `DELETE FROM character_achievements
WHERE char_id = ? AND achievement_id = ?`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{playerID, achievementID},
}) })
if err != nil {
return fmt.Errorf("failed to delete achievement update: %w", err)
}
// Delete update items
itemQuery := `DELETE FROM character_achievements_items
WHERE char_id = ? AND achievement_id = ?`
err = sqlitex.Execute(conn, itemQuery, &sqlitex.ExecOptions{
Args: []any{playerID, achievementID},
})
if err != nil {
return fmt.Errorf("failed to delete update items: %w", err)
}
return sqlitex.Execute(conn, "COMMIT", nil)
} }
// SaveAchievement saves or updates an achievement in the database // SaveAchievement saves or updates an achievement in the database
func SaveAchievement(db *database.DB, achievement *Achievement) error { func SaveAchievement(pool *sqlitex.Pool, achievement *Achievement) error {
return db.Transaction(func(tx *database.DB) error { conn, err := pool.Take(context.Background())
// Save main achievement record if err != nil {
query := `INSERT OR REPLACE INTO achievements return fmt.Errorf("failed to get connection: %w", err)
(achievement_id, title, uncompleted_text, completed_text, }
category, expansion, icon, point_value, qty_req, defer pool.Put(conn)
hide_achievement, unknown3a, unknown3b)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
if err := tx.Exec(query, achievement.ID, achievement.Title, err = sqlitex.Execute(conn, "BEGIN", nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer sqlitex.Execute(conn, "ROLLBACK", nil)
// Save main achievement record
query := `INSERT OR REPLACE INTO achievements
(achievement_id, title, uncompleted_text, completed_text,
category, expansion, icon, point_value, qty_req,
hide_achievement, unknown3a, unknown3b)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{
achievement.ID, achievement.Title,
achievement.UncompletedText, achievement.CompletedText, achievement.UncompletedText, achievement.CompletedText,
achievement.Category, achievement.Expansion, achievement.Icon, achievement.Category, achievement.Expansion, achievement.Icon,
achievement.PointValue, achievement.QtyRequired, achievement.Hide, achievement.PointValue, achievement.QtyRequired, achievement.Hide,
achievement.Unknown3A, achievement.Unknown3B); err != nil { achievement.Unknown3A, achievement.Unknown3B,
return fmt.Errorf("failed to save achievement: %w", err) },
}
// Delete existing requirements and rewards
if err := tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", achievement.ID); err != nil {
return fmt.Errorf("failed to delete old requirements: %w", err)
}
if err := tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", achievement.ID); err != nil {
return fmt.Errorf("failed to delete old rewards: %w", err)
}
// Insert requirements
reqQuery := `INSERT INTO achievements_requirements
(achievement_id, name, qty_req) VALUES (?, ?, ?)`
for _, req := range achievement.Requirements {
if err := tx.Exec(reqQuery, req.AchievementID, req.Name, req.QtyRequired); err != nil {
return fmt.Errorf("failed to save requirement: %w", err)
}
}
// Insert rewards
rewardQuery := `INSERT INTO achievements_rewards
(achievement_id, reward) VALUES (?, ?)`
for _, reward := range achievement.Rewards {
if err := tx.Exec(rewardQuery, reward.AchievementID, reward.Reward); err != nil {
return fmt.Errorf("failed to save reward: %w", err)
}
}
return nil
}) })
if err != nil {
return fmt.Errorf("failed to save achievement: %w", err)
}
// Delete existing requirements and rewards
err = sqlitex.Execute(conn, "DELETE FROM achievements_requirements WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievement.ID},
})
if err != nil {
return fmt.Errorf("failed to delete old requirements: %w", err)
}
err = sqlitex.Execute(conn, "DELETE FROM achievements_rewards WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievement.ID},
})
if err != nil {
return fmt.Errorf("failed to delete old rewards: %w", err)
}
// Insert requirements
reqQuery := `INSERT INTO achievements_requirements
(achievement_id, name, qty_req) VALUES (?, ?, ?)`
for _, req := range achievement.Requirements {
err = sqlitex.Execute(conn, reqQuery, &sqlitex.ExecOptions{
Args: []any{req.AchievementID, req.Name, req.QtyRequired},
})
if err != nil {
return fmt.Errorf("failed to save requirement: %w", err)
}
}
// Insert rewards
rewardQuery := `INSERT INTO achievements_rewards
(achievement_id, reward) VALUES (?, ?)`
for _, reward := range achievement.Rewards {
err = sqlitex.Execute(conn, rewardQuery, &sqlitex.ExecOptions{
Args: []any{reward.AchievementID, reward.Reward},
})
if err != nil {
return fmt.Errorf("failed to save reward: %w", err)
}
}
return sqlitex.Execute(conn, "COMMIT", nil)
} }
// DeleteAchievement removes an achievement and all related records from database // DeleteAchievement removes an achievement and all related records from database
func DeleteAchievement(db *database.DB, achievementID uint32) error { func DeleteAchievement(pool *sqlitex.Pool, achievementID uint32) error {
return db.Transaction(func(tx *database.DB) error { conn, err := pool.Take(context.Background())
// Delete main achievement if err != nil {
if err := tx.Exec("DELETE FROM achievements WHERE achievement_id = ?", achievementID); err != nil { return fmt.Errorf("failed to get connection: %w", err)
return fmt.Errorf("failed to delete achievement: %w", err) }
} defer pool.Put(conn)
// Delete requirements err = sqlitex.Execute(conn, "BEGIN", nil)
if err := tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", achievementID); err != nil { if err != nil {
return fmt.Errorf("failed to delete requirements: %w", err) return fmt.Errorf("failed to begin transaction: %w", err)
} }
defer sqlitex.Execute(conn, "ROLLBACK", nil)
// Delete rewards // Delete main achievement
if err := tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", achievementID); err != nil { err = sqlitex.Execute(conn, "DELETE FROM achievements WHERE achievement_id = ?", &sqlitex.ExecOptions{
return fmt.Errorf("failed to delete rewards: %w", err) Args: []any{achievementID},
}
// Delete player progress (optional - might want to preserve history)
if err := tx.Exec("DELETE FROM character_achievements WHERE achievement_id = ?", achievementID); err != nil {
return fmt.Errorf("failed to delete player achievements: %w", err)
}
if err := tx.Exec("DELETE FROM character_achievements_items WHERE achievement_id = ?", achievementID); err != nil {
return fmt.Errorf("failed to delete player achievement items: %w", err)
}
return nil
}) })
if err != nil {
return fmt.Errorf("failed to delete achievement: %w", err)
}
// Delete requirements
err = sqlitex.Execute(conn, "DELETE FROM achievements_requirements WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievementID},
})
if err != nil {
return fmt.Errorf("failed to delete requirements: %w", err)
}
// Delete rewards
err = sqlitex.Execute(conn, "DELETE FROM achievements_rewards WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievementID},
})
if err != nil {
return fmt.Errorf("failed to delete rewards: %w", err)
}
// Delete player progress (optional - might want to preserve history)
err = sqlitex.Execute(conn, "DELETE FROM character_achievements WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievementID},
})
if err != nil {
return fmt.Errorf("failed to delete player achievements: %w", err)
}
err = sqlitex.Execute(conn, "DELETE FROM character_achievements_items WHERE achievement_id = ?", &sqlitex.ExecOptions{
Args: []any{achievementID},
})
if err != nil {
return fmt.Errorf("failed to delete player achievement items: %w", err)
}
return sqlitex.Execute(conn, "COMMIT", nil)
} }

View File

@ -1,180 +0,0 @@
package database
import (
"os"
"testing"
)
func TestOpen(t *testing.T) {
// Create a temporary database file
tempFile := "test.db"
defer os.Remove(tempFile)
db, err := Open(tempFile)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
if db == nil {
t.Fatal("Database instance is nil")
}
}
func TestExec(t *testing.T) {
tempFile := "test_exec.db"
defer os.Remove(tempFile)
db, err := Open(tempFile)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Test table creation
err = db.Exec(`CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)`)
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}
// Test data insertion
err = db.Exec(`INSERT INTO test_table (name) VALUES (?)`, "test_name")
if err != nil {
t.Fatalf("Failed to insert data: %v", err)
}
}
func TestQueryRow(t *testing.T) {
tempFile := "test_query.db"
defer os.Remove(tempFile)
db, err := Open(tempFile)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Setup test data
err = db.Exec(`CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)`)
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}
err = db.Exec(`INSERT INTO test_table (name, value) VALUES (?, ?)`, "test", 42)
if err != nil {
t.Fatalf("Failed to insert data: %v", err)
}
// Test query
row, err := db.QueryRow("SELECT name, value FROM test_table WHERE id = ?", 1)
if err != nil {
t.Fatalf("Failed to query row: %v", err)
}
if row == nil {
t.Fatal("Row is nil")
}
defer row.Close()
name := row.Text(0)
value := row.Int(1)
if name != "test" {
t.Errorf("Expected name 'test', got '%s'", name)
}
if value != 42 {
t.Errorf("Expected value 42, got %d", value)
}
}
func TestQuery(t *testing.T) {
tempFile := "test_query_all.db"
defer os.Remove(tempFile)
db, err := Open(tempFile)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Setup test data
err = db.Exec(`CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)`)
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}
names := []string{"test1", "test2", "test3"}
for _, name := range names {
err = db.Exec(`INSERT INTO test_table (name) VALUES (?)`, name)
if err != nil {
t.Fatalf("Failed to insert data: %v", err)
}
}
// Test query with callback
var results []string
err = db.Query("SELECT name FROM test_table ORDER BY id", func(row *Row) error {
results = append(results, row.Text(0))
return nil
})
if err != nil {
t.Fatalf("Failed to query: %v", err)
}
if len(results) != 3 {
t.Errorf("Expected 3 results, got %d", len(results))
}
for i, expected := range names {
if i < len(results) && results[i] != expected {
t.Errorf("Expected result[%d] = '%s', got '%s'", i, expected, results[i])
}
}
}
func TestTransaction(t *testing.T) {
tempFile := "test_transaction.db"
defer os.Remove(tempFile)
db, err := Open(tempFile)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Setup
err = db.Exec(`CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)`)
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}
// Test successful transaction
err = db.Transaction(func(txDB *DB) error {
err := txDB.Exec(`INSERT INTO test_table (name) VALUES (?)`, "tx_test1")
if err != nil {
return err
}
return txDB.Exec(`INSERT INTO test_table (name) VALUES (?)`, "tx_test2")
})
if err != nil {
t.Fatalf("Transaction failed: %v", err)
}
// Verify data was committed
var count int
row, err := db.QueryRow("SELECT COUNT(*) FROM test_table")
if err != nil {
t.Fatalf("Failed to count rows: %v", err)
}
if row != nil {
count = row.Int(0)
row.Close()
}
if count != 2 {
t.Errorf("Expected 2 rows, got %d", count)
}
}

View File

@ -1,262 +0,0 @@
package database
import (
"fmt"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// DB wraps sqlite.Conn with simplified query methods
type DB struct {
conn *sqlite.Conn
}
// Row represents a single database row with easy column access
type Row struct {
stmt *sqlite.Stmt
}
// QueryFunc processes each row in a result set
type QueryFunc func(*Row) error
// Open creates a new database connection with common settings
func Open(path string) (*DB, error) {
conn, err := sqlite.OpenConn(path, sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Enable foreign keys and WAL mode for better performance
if err := sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = ON", nil); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
}
if err := sqlitex.ExecuteTransient(conn, "PRAGMA journal_mode = WAL", nil); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to enable WAL mode: %w", err)
}
return &DB{conn: conn}, nil
}
// Close closes the database connection
func (db *DB) Close() error {
return db.conn.Close()
}
// Exec executes a statement with parameters
func (db *DB) Exec(query string, args ...any) error {
return sqlitex.Execute(db.conn, query, &sqlitex.ExecOptions{
Args: args,
})
}
// QueryRow executes a query expecting a single row result
func (db *DB) QueryRow(query string, args ...any) (*Row, error) {
stmt, err := db.conn.Prepare(query)
if err != nil {
return nil, fmt.Errorf("prepare failed: %w", err)
}
// Bind parameters
for i, arg := range args {
if err := bindParam(stmt, i+1, arg); err != nil {
stmt.Finalize()
return nil, err
}
}
hasRow, err := stmt.Step()
if err != nil {
stmt.Finalize()
return nil, fmt.Errorf("query failed: %w", err)
}
if !hasRow {
stmt.Finalize()
return nil, nil // No row found
}
return &Row{stmt: stmt}, nil
}
// Query executes a query and calls fn for each row
func (db *DB) Query(query string, fn QueryFunc, args ...any) error {
stmt, err := db.conn.Prepare(query)
if err != nil {
return fmt.Errorf("prepare failed: %w", err)
}
defer stmt.Finalize()
// Bind parameters
for i, arg := range args {
if err := bindParam(stmt, i+1, arg); err != nil {
return err
}
}
row := &Row{stmt: stmt}
for {
hasRow, err := stmt.Step()
if err != nil {
return fmt.Errorf("query failed: %w", err)
}
if !hasRow {
break
}
if err := fn(row); err != nil {
return err
}
}
return nil
}
// QuerySlice executes a query and returns all rows in a slice
func (db *DB) QuerySlice(query string, args ...any) ([]*Row, error) {
var rows []*Row
stmt, err := db.conn.Prepare(query)
if err != nil {
return nil, fmt.Errorf("prepare failed: %w", err)
}
defer stmt.Finalize()
// Bind parameters
for i, arg := range args {
if err := bindParam(stmt, i+1, arg); err != nil {
return nil, err
}
}
for {
hasRow, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
if !hasRow {
break
}
// Create a snapshot of the current row
rowData := &Row{stmt: stmt}
rows = append(rows, rowData)
}
return rows, nil
}
// LastInsertID returns the last inserted row ID
func (db *DB) LastInsertID() int64 {
return db.conn.LastInsertRowID()
}
// Changes returns the number of rows affected by the last statement
func (db *DB) Changes() int {
return db.conn.Changes()
}
// Transaction executes fn within a database transaction
func (db *DB) Transaction(fn func(*DB) error) error {
if err := sqlitex.ExecuteTransient(db.conn, "BEGIN", nil); err != nil {
return fmt.Errorf("begin transaction failed: %w", err)
}
if err := fn(db); err != nil {
sqlitex.ExecuteTransient(db.conn, "ROLLBACK", nil)
return err
}
if err := sqlitex.ExecuteTransient(db.conn, "COMMIT", nil); err != nil {
return fmt.Errorf("commit transaction failed: %w", err)
}
return nil
}
// Row column access methods
// Close releases the row's statement
func (r *Row) Close() {
if r.stmt != nil {
r.stmt.Finalize()
r.stmt = nil
}
}
// Int returns column as int
func (r *Row) Int(col int) int {
return r.stmt.ColumnInt(col)
}
// Int64 returns column as int64
func (r *Row) Int64(col int) int64 {
return r.stmt.ColumnInt64(col)
}
// Text returns column as string
func (r *Row) Text(col int) string {
return r.stmt.ColumnText(col)
}
// Bool returns column as bool (0 = false, non-zero = true)
func (r *Row) Bool(col int) bool {
return r.stmt.ColumnInt(col) != 0
}
// Float returns column as float64
func (r *Row) Float(col int) float64 {
return r.stmt.ColumnFloat(col)
}
// IsNull checks if column is NULL
func (r *Row) IsNull(col int) bool {
return r.stmt.ColumnType(col) == sqlite.TypeNull
}
// bindParam binds a parameter to a statement at the given index
func bindParam(stmt *sqlite.Stmt, index int, value any) error {
switch v := value.(type) {
case nil:
stmt.BindNull(index)
case int:
stmt.BindInt64(index, int64(v))
case int8:
stmt.BindInt64(index, int64(v))
case int16:
stmt.BindInt64(index, int64(v))
case int32:
stmt.BindInt64(index, int64(v))
case int64:
stmt.BindInt64(index, v)
case uint:
stmt.BindInt64(index, int64(v))
case uint8:
stmt.BindInt64(index, int64(v))
case uint16:
stmt.BindInt64(index, int64(v))
case uint32:
stmt.BindInt64(index, int64(v))
case uint64:
stmt.BindInt64(index, int64(v))
case float32:
stmt.BindFloat(index, float64(v))
case float64:
stmt.BindFloat(index, v)
case bool:
if v {
stmt.BindInt64(index, 1)
} else {
stmt.BindInt64(index, 0)
}
case string:
stmt.BindText(index, v)
case []byte:
stmt.BindBytes(index, v)
default:
return fmt.Errorf("unsupported parameter type: %T", value)
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,608 @@
package guilds
import (
"context"
"fmt"
"path/filepath"
"testing"
"time"
"eq2emu/internal/database"
)
// createTestDB creates a temporary test database
func createTestDB(t *testing.T) *database.DB {
// Create temporary directory for test database
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "test_guilds.db")
// Create and initialize database
db, err := database.Open(dbPath)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
// Create guild tables for testing
err = createGuildTables(db)
if err != nil {
t.Fatalf("Failed to create guild tables: %v", err)
}
return db
}
// createGuildTables creates the necessary tables for guild testing
func createGuildTables(db *database.DB) error {
tables := []string{
`CREATE TABLE IF NOT EXISTS guilds (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
motd TEXT,
level INTEGER DEFAULT 1,
xp INTEGER DEFAULT 111,
xp_needed INTEGER DEFAULT 2521,
formed_on INTEGER DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS guild_members (
char_id INTEGER PRIMARY KEY,
guild_id INTEGER NOT NULL,
account_id INTEGER NOT NULL,
recruiter_id INTEGER DEFAULT 0,
name TEXT NOT NULL,
guild_status INTEGER DEFAULT 0,
points REAL DEFAULT 0.0,
adventure_class INTEGER DEFAULT 0,
adventure_level INTEGER DEFAULT 1,
tradeskill_class INTEGER DEFAULT 0,
tradeskill_level INTEGER DEFAULT 1,
rank INTEGER DEFAULT 7,
member_flags INTEGER DEFAULT 0,
zone TEXT DEFAULT '',
join_date INTEGER DEFAULT 0,
last_login_date INTEGER DEFAULT 0,
note TEXT DEFAULT '',
officer_note TEXT DEFAULT '',
recruiter_description TEXT DEFAULT '',
recruiter_picture_data BLOB,
recruiting_show_adventure_class INTEGER DEFAULT 0,
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_events (
event_id INTEGER PRIMARY KEY,
guild_id INTEGER NOT NULL,
date INTEGER NOT NULL,
type INTEGER NOT NULL,
description TEXT NOT NULL,
locked INTEGER DEFAULT 0,
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_ranks (
guild_id INTEGER NOT NULL,
rank INTEGER NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (guild_id, rank),
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_permissions (
guild_id INTEGER NOT NULL,
rank INTEGER NOT NULL,
permission INTEGER NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, rank, permission),
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_event_filters (
guild_id INTEGER NOT NULL,
event_id INTEGER NOT NULL,
category INTEGER NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, event_id, category),
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_recruiting (
guild_id INTEGER NOT NULL,
flag INTEGER NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, flag),
FOREIGN KEY (guild_id) REFERENCES guilds(id)
)`,
`CREATE TABLE IF NOT EXISTS guild_point_history (
character_id INTEGER NOT NULL,
date INTEGER NOT NULL,
modified_by TEXT NOT NULL,
comment TEXT NOT NULL,
points REAL NOT NULL,
FOREIGN KEY (character_id) REFERENCES guild_members(char_id)
)`,
}
for _, sql := range tables {
if err := db.Exec(sql); err != nil {
return err
}
}
return nil
}
// TestDatabaseGuildManager_LoadGuilds tests loading guilds from database
func TestDatabaseGuildManager_LoadGuilds(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test data
insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on)
VALUES (1, 'Test Guild', 'Welcome!', 5, 1000, 5000, ?)`
formedTime := time.Now().Unix()
err := db.Exec(insertGuildSQL, formedTime)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
// Test loading guilds
guilds, err := dgm.LoadGuilds(ctx)
if err != nil {
t.Fatalf("Failed to load guilds: %v", err)
}
if len(guilds) != 1 {
t.Errorf("Expected 1 guild, got %d", len(guilds))
}
guild := guilds[0]
if guild.ID != 1 {
t.Errorf("Expected guild ID 1, got %d", guild.ID)
}
if guild.Name != "Test Guild" {
t.Errorf("Expected guild name 'Test Guild', got '%s'", guild.Name)
}
if guild.MOTD != "Welcome!" {
t.Errorf("Expected MOTD 'Welcome!', got '%s'", guild.MOTD)
}
if guild.Level != 5 {
t.Errorf("Expected level 5, got %d", guild.Level)
}
if guild.EXPCurrent != 1000 {
t.Errorf("Expected current exp 1000, got %d", guild.EXPCurrent)
}
if guild.EXPToNextLevel != 5000 {
t.Errorf("Expected next level exp 5000, got %d", guild.EXPToNextLevel)
}
}
// TestDatabaseGuildManager_LoadGuild tests loading a specific guild
func TestDatabaseGuildManager_LoadGuild(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test data
insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on)
VALUES (123, 'Specific Guild', 'Test MOTD', 10, 2000, 8000, ?)`
formedTime := time.Now().Unix()
err := db.Exec(insertGuildSQL, formedTime)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
// Test loading specific guild
guild, err := dgm.LoadGuild(ctx, 123)
if err != nil {
t.Fatalf("Failed to load guild: %v", err)
}
if guild.ID != 123 {
t.Errorf("Expected guild ID 123, got %d", guild.ID)
}
if guild.Name != "Specific Guild" {
t.Errorf("Expected guild name 'Specific Guild', got '%s'", guild.Name)
}
if guild.MOTD != "Test MOTD" {
t.Errorf("Expected MOTD 'Test MOTD', got '%s'", guild.MOTD)
}
// Test loading non-existent guild
_, err = dgm.LoadGuild(ctx, 999)
if err == nil {
t.Error("Expected error when loading non-existent guild")
}
}
// TestDatabaseGuildManager_LoadGuildMembers tests loading guild members
func TestDatabaseGuildManager_LoadGuildMembers(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test guild
err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
// Insert test members
joinTime := time.Now().Unix()
memberSQL := `INSERT INTO guild_members
(char_id, guild_id, account_id, name, rank, adventure_level, join_date, last_login_date)
VALUES (?, 1, 100, ?, ?, 50, ?, ?)`
err = db.Exec(memberSQL, 1, "Player1", RankLeader, joinTime, joinTime)
if err != nil {
t.Fatalf("Failed to insert member 1: %v", err)
}
err = db.Exec(memberSQL, 2, "Player2", RankMember, joinTime, joinTime)
if err != nil {
t.Fatalf("Failed to insert member 2: %v", err)
}
// Test loading members
members, err := dgm.LoadGuildMembers(ctx, 1)
if err != nil {
t.Fatalf("Failed to load guild members: %v", err)
}
if len(members) != 2 {
t.Errorf("Expected 2 members, got %d", len(members))
}
// Verify first member
member1 := members[0]
if member1.CharacterID != 1 {
t.Errorf("Expected character ID 1, got %d", member1.CharacterID)
}
if member1.Name != "Player1" {
t.Errorf("Expected name 'Player1', got '%s'", member1.Name)
}
if member1.Rank != RankLeader {
t.Errorf("Expected rank %d, got %d", RankLeader, member1.Rank)
}
}
// TestDatabaseGuildManager_SaveGuild tests saving guild data
func TestDatabaseGuildManager_SaveGuild(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Create test guild
guild := NewGuild()
guild.SetID(1)
guild.SetName("Saved Guild", false)
guild.SetLevel(15, false)
guild.SetMOTD("Saved MOTD", false)
guild.SetEXPCurrent(3000, false)
guild.SetEXPToNextLevel(9000, false)
guild.SetFormedDate(time.Now())
// Test saving guild
err := dgm.SaveGuild(ctx, guild)
if err != nil {
t.Fatalf("Failed to save guild: %v", err)
}
// Verify the guild was saved
savedGuild, err := dgm.LoadGuild(ctx, 1)
if err != nil {
t.Fatalf("Failed to load saved guild: %v", err)
}
if savedGuild.Name != "Saved Guild" {
t.Errorf("Expected saved name 'Saved Guild', got '%s'", savedGuild.Name)
}
if savedGuild.Level != 15 {
t.Errorf("Expected saved level 15, got %d", savedGuild.Level)
}
if savedGuild.MOTD != "Saved MOTD" {
t.Errorf("Expected saved MOTD 'Saved MOTD', got '%s'", savedGuild.MOTD)
}
}
// TestDatabaseGuildManager_CreateGuild tests creating a new guild
func TestDatabaseGuildManager_CreateGuild(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Create guild data
guildData := GuildData{
Name: "New Guild",
Level: 1,
FormedDate: time.Now(),
MOTD: "New guild MOTD",
EXPCurrent: 111,
EXPToNextLevel: 2521,
RecruitingShortDesc: "Short description",
RecruitingFullDesc: "Full description",
RecruitingMinLevel: 1,
RecruitingPlayStyle: 0,
}
// Test creating guild
guildID, err := dgm.CreateGuild(ctx, guildData)
if err != nil {
t.Fatalf("Failed to create guild: %v", err)
}
if guildID <= 0 {
t.Errorf("Expected positive guild ID, got %d", guildID)
}
// Verify the guild was created
createdGuild, err := dgm.LoadGuild(ctx, guildID)
if err != nil {
t.Fatalf("Failed to load created guild: %v", err)
}
if createdGuild.Name != "New Guild" {
t.Errorf("Expected created name 'New Guild', got '%s'", createdGuild.Name)
}
}
// TestDatabaseGuildManager_DeleteGuild tests deleting a guild
func TestDatabaseGuildManager_DeleteGuild(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test guild
err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'To Delete')`)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
// Insert test member
err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (1, 1, 100, 'Member')`)
if err != nil {
t.Fatalf("Failed to insert test member: %v", err)
}
// Verify guild exists
_, err = dgm.LoadGuild(ctx, 1)
if err != nil {
t.Fatalf("Guild should exist before deletion: %v", err)
}
// Test deleting guild
err = dgm.DeleteGuild(ctx, 1)
if err != nil {
t.Fatalf("Failed to delete guild: %v", err)
}
// Verify guild was deleted
_, err = dgm.LoadGuild(ctx, 1)
if err == nil {
t.Error("Guild should not exist after deletion")
}
// Verify members were deleted
members, err := dgm.LoadGuildMembers(ctx, 1)
if err != nil {
t.Fatalf("Failed to check members after guild deletion: %v", err)
}
if len(members) != 0 {
t.Errorf("Expected 0 members after guild deletion, got %d", len(members))
}
}
// TestDatabaseGuildManager_GetGuildIDByCharacterID tests getting guild ID by character ID
func TestDatabaseGuildManager_GetGuildIDByCharacterID(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test guild
err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
// Insert test member
err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (123, 1, 100, 'TestPlayer')`)
if err != nil {
t.Fatalf("Failed to insert test member: %v", err)
}
// Test getting guild ID for character
guildID, err := dgm.GetGuildIDByCharacterID(ctx, 123)
if err != nil {
t.Fatalf("Failed to get guild ID by character ID: %v", err)
}
if guildID != 1 {
t.Errorf("Expected guild ID 1, got %d", guildID)
}
// Test getting guild ID for non-existent character
_, err = dgm.GetGuildIDByCharacterID(ctx, 999)
if err == nil {
t.Error("Expected error for non-existent character")
}
}
// TestDatabaseGuildManager_ConcurrentOperations tests concurrent database operations
// Now enabled with sqlitex.Pool for proper connection management
func TestDatabaseGuildManager_ConcurrentOperations(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert initial guild
err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Concurrent Test')`)
if err != nil {
t.Fatalf("Failed to insert test guild: %v", err)
}
const numGoroutines = 5 // Reduce concurrency to avoid SQLite issues
done := make(chan error, numGoroutines)
// Test concurrent reads
for i := 0; i < numGoroutines; i++ {
go func(id int) {
// Add small delay to reduce contention
time.Sleep(time.Duration(id) * time.Millisecond)
_, err := dgm.LoadGuild(ctx, 1)
done <- err
}(i)
}
// Wait for all reads to complete
for i := 0; i < numGoroutines; i++ {
if err := <-done; err != nil {
t.Errorf("Concurrent read failed: %v", err)
}
}
// Test concurrent member additions
for i := 0; i < numGoroutines; i++ {
go func(id int) {
memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (?, 1, 100, ?)`
err := db.Exec(memberSQL, 100+id, fmt.Sprintf("Player%d", id))
done <- err
}(i)
}
// Wait for all insertions
successCount := 0
for i := 0; i < numGoroutines; i++ {
if err := <-done; err == nil {
successCount++
}
}
if successCount != numGoroutines {
t.Logf("Only %d out of %d concurrent insertions succeeded (some conflicts expected)", successCount, numGoroutines)
}
}
// TestDatabaseGuildManager_TransactionRollback tests transaction rollback on errors
func TestDatabaseGuildManager_TransactionRollback(t *testing.T) {
db := createTestDB(t)
defer db.Close()
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Try to create a guild with invalid data that should trigger a rollback
invalidGuildData := GuildData{
Name: "", // Empty name should cause validation error
Level: 1,
FormedDate: time.Now(),
}
// This should fail but we test that it handles errors gracefully
_, err := dgm.CreateGuild(ctx, invalidGuildData)
// We don't expect specific error behavior here since the implementation
// may or may not have validation, but we test that it doesn't crash
t.Logf("Create guild with invalid data returned: %v", err)
// Verify no partial data was left behind
guilds, err := dgm.LoadGuilds(ctx)
if err != nil {
t.Fatalf("Failed to load guilds after rollback test: %v", err)
}
// Should have no guilds (or any existing test data, but no new invalid guild)
t.Logf("Found %d guilds after invalid creation attempt", len(guilds))
}
// BenchmarkDatabaseGuildManager_LoadGuilds benchmarks loading guilds
func BenchmarkDatabaseGuildManager_LoadGuilds(b *testing.B) {
// Create test database in temp directory
tempDir := b.TempDir()
dbPath := filepath.Join(tempDir, "bench_guilds.db")
db, err := database.Open(dbPath)
if err != nil {
b.Fatalf("Failed to create benchmark database: %v", err)
}
defer db.Close()
if err := createGuildTables(db); err != nil {
b.Fatalf("Failed to create guild tables: %v", err)
}
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test guilds
for i := 0; i < 100; i++ {
insertSQL := `INSERT INTO guilds (id, name, level, xp, xp_needed, formed_on) VALUES (?, ?, 1, 111, 2521, ?)`
formedTime := time.Now().Unix()
if err := db.Exec(insertSQL, i+1, fmt.Sprintf("Guild%d", i+1), formedTime); err != nil {
b.Fatalf("Failed to insert test guild %d: %v", i+1, err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := dgm.LoadGuilds(ctx)
if err != nil {
b.Fatalf("Failed to load guilds in benchmark: %v", err)
}
}
}
// BenchmarkDatabaseGuildManager_LoadGuildMembers benchmarks loading guild members
func BenchmarkDatabaseGuildManager_LoadGuildMembers(b *testing.B) {
// Create test database in temp directory
tempDir := b.TempDir()
dbPath := filepath.Join(tempDir, "bench_members.db")
db, err := database.Open(dbPath)
if err != nil {
b.Fatalf("Failed to create benchmark database: %v", err)
}
defer db.Close()
if err := createGuildTables(db); err != nil {
b.Fatalf("Failed to create guild tables: %v", err)
}
dgm := NewDatabaseGuildManager(db)
ctx := context.Background()
// Insert test guild
if err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Benchmark Guild')`); err != nil {
b.Fatalf("Failed to insert benchmark guild: %v", err)
}
// Insert test members
joinTime := time.Now().Unix()
for i := 0; i < 100; i++ {
memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name, rank, join_date, last_login_date)
VALUES (?, 1, 100, ?, ?, ?, ?)`
if err := db.Exec(memberSQL, i+1, fmt.Sprintf("Player%d", i+1), RankMember, joinTime, joinTime); err != nil {
b.Fatalf("Failed to insert test member %d: %v", i+1, err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := dgm.LoadGuildMembers(ctx, 1)
if err != nil {
b.Fatalf("Failed to load guild members in benchmark: %v", err)
}
}
}

View File

@ -0,0 +1,529 @@
package guilds
import (
"fmt"
"testing"
"time"
)
// TestNewGuild tests guild creation with default values
func TestNewGuild(t *testing.T) {
guild := NewGuild()
// Test initial state
if guild.GetLevel() != 1 {
t.Errorf("Expected initial level 1, got %d", guild.GetLevel())
}
if guild.GetEXPCurrent() != 111 {
t.Errorf("Expected initial expCurrent 111, got %d", guild.GetEXPCurrent())
}
if guild.GetEXPToNextLevel() != 2521 {
t.Errorf("Expected initial expToNextLevel 2521, got %d", guild.GetEXPToNextLevel())
}
if guild.GetRecruitingMinLevel() != 1 {
t.Errorf("Expected initial recruitingMinLevel 1, got %d", guild.GetRecruitingMinLevel())
}
if guild.GetRecruitingPlayStyle() != RecruitingPlayStyleNone {
t.Errorf("Expected initial recruitingPlayStyle %d, got %d", RecruitingPlayStyleNone, guild.GetRecruitingPlayStyle())
}
if guild.GetNextEventID() != 1 {
t.Errorf("Expected initial nextEventID 1, got %d", guild.GetNextEventID())
}
// Test recruiting flags are initialized
if guild.GetRecruitingFlag(RecruitingFlagTraining) != 0 {
t.Error("Training recruiting flag should be initialized to 0")
}
if guild.GetRecruitingFlag(RecruitingFlagFighters) != 0 {
t.Error("Fighters recruiting flag should be initialized to 0")
}
// Test description tags are initialized
if guild.GetRecruitingDescTag(0) != RecruitingDescTagNone {
t.Error("Description tag 0 should be initialized to None")
}
// Test default rank names are set
if guild.GetRankName(RankLeader) != "Leader" {
t.Errorf("Expected leader rank name 'Leader', got '%s'", guild.GetRankName(RankLeader))
}
if guild.GetRankName(RankRecruit) != "Recruit" {
t.Errorf("Expected recruit rank name 'Recruit', got '%s'", guild.GetRankName(RankRecruit))
}
}
// TestGuildBasicOperations tests basic guild getter/setter operations
func TestGuildBasicOperations(t *testing.T) {
guild := NewGuild()
// Test ID operations
testID := int32(12345)
guild.SetID(testID)
if guild.GetID() != testID {
t.Errorf("Expected ID %d, got %d", testID, guild.GetID())
}
// Test name operations
testName := "Test Guild"
guild.SetName(testName, false)
if guild.GetName() != testName {
t.Errorf("Expected name '%s', got '%s'", testName, guild.GetName())
}
// Test level operations
testLevel := int8(10)
guild.SetLevel(testLevel, false)
if guild.GetLevel() != testLevel {
t.Errorf("Expected level %d, got %d", testLevel, guild.GetLevel())
}
// Test MOTD operations
testMOTD := "Welcome to our guild!"
guild.SetMOTD(testMOTD, false)
if guild.GetMOTD() != testMOTD {
t.Errorf("Expected MOTD '%s', got '%s'", testMOTD, guild.GetMOTD())
}
// Test formed date operations
testDate := time.Now().Add(-24 * time.Hour)
guild.SetFormedDate(testDate)
if !guild.GetFormedDate().Equal(testDate) {
t.Errorf("Expected formed date %v, got %v", testDate, guild.GetFormedDate())
}
// Test experience operations
testExpCurrent := int64(5000)
testExpNext := int64(10000)
guild.SetEXPCurrent(testExpCurrent, false)
guild.SetEXPToNextLevel(testExpNext, false)
current := guild.GetEXPCurrent()
next := guild.GetEXPToNextLevel()
if current != testExpCurrent {
t.Errorf("Expected current exp %d, got %d", testExpCurrent, current)
}
if next != testExpNext {
t.Errorf("Expected next level exp %d, got %d", testExpNext, next)
}
}
// TestGuildMemberOperations tests guild member management
func TestGuildMemberOperations(t *testing.T) {
guild := NewGuild()
guild.SetID(1)
// Test adding members using the actual method
characterID1 := int32(1)
characterID2 := int32(2)
inviterName := "TestInviter"
joinDate := time.Now()
// Add first member
success := guild.AddNewGuildMember(characterID1, inviterName, joinDate, RankRecruit)
if !success {
t.Error("Should be able to add first member")
}
// Add second member
success = guild.AddNewGuildMember(characterID2, inviterName, joinDate, RankMember)
if !success {
t.Error("Should be able to add second member")
}
// Test getting member by ID
member1 := guild.GetGuildMember(characterID1)
if member1 == nil {
t.Error("Should be able to retrieve member by ID")
}
if member1.CharacterID != characterID1 {
t.Errorf("Expected member ID %d, got %d", characterID1, member1.CharacterID)
}
// Test getting all members
allMembers := guild.GetAllMembers()
if len(allMembers) != 2 {
t.Errorf("Expected 2 members, got %d", len(allMembers))
}
// Test removing member
guild.RemoveGuildMember(characterID1, false)
member1After := guild.GetGuildMember(characterID1)
if member1After != nil {
t.Error("Member should be nil after removal")
}
allMembersAfter := guild.GetAllMembers()
if len(allMembersAfter) != 1 {
t.Errorf("Expected 1 member after removal, got %d", len(allMembersAfter))
}
}
// TestGuildEventOperations tests guild event management
func TestGuildEventOperations(t *testing.T) {
guild := NewGuild()
// Test adding events
eventType := int32(EventMemberJoins)
description := "Member joined guild"
eventDate := time.Now()
// Add first event (should get ID 1)
guild.AddNewGuildEvent(eventType, description, eventDate, false)
// Add another event (should get ID 2)
guild.AddNewGuildEvent(EventMemberLeaves, "Member left guild", eventDate, false)
// Test getting next event ID (should be 3 now)
nextID := guild.GetNextEventID()
if nextID != 3 {
t.Errorf("Expected next event ID 3, got %d", nextID)
}
// Test getting specific event (first event should have ID 1)
event := guild.GetGuildEvent(1)
if event == nil {
t.Error("Should be able to retrieve event by ID")
}
if event != nil && event.Description != description {
t.Errorf("Expected event description '%s', got '%s'", description, event.Description)
}
}
// TestGuildRankOperations tests guild rank management
func TestGuildRankOperations(t *testing.T) {
guild := NewGuild()
// Test setting custom rank name
customRankName := "Elite Member"
success := guild.SetRankName(RankMember, customRankName, false)
if !success {
t.Error("Should be able to set rank name")
}
rankName := guild.GetRankName(RankMember)
if rankName != customRankName {
t.Errorf("Expected rank name '%s', got '%s'", customRankName, rankName)
}
// Test getting default rank names
leaderName := guild.GetRankName(RankLeader)
if leaderName != "Leader" {
t.Errorf("Expected leader rank name 'Leader', got '%s'", leaderName)
}
}
// TestGuildRecruitingOperations tests guild recruiting settings
func TestGuildRecruitingOperations(t *testing.T) {
guild := NewGuild()
// Test recruiting descriptions
shortDesc := "Looking for members"
fullDesc := "We are a friendly guild looking for active members"
guild.SetRecruitingShortDesc(shortDesc, false)
guild.SetRecruitingFullDesc(fullDesc, false)
short := guild.GetRecruitingShortDesc()
full := guild.GetRecruitingFullDesc()
if short != shortDesc {
t.Errorf("Expected short description '%s', got '%s'", shortDesc, short)
}
if full != fullDesc {
t.Errorf("Expected full description '%s', got '%s'", fullDesc, full)
}
// Test recruiting settings
minLevel := int8(20)
playStyle := int8(2)
guild.SetRecruitingMinLevel(minLevel, false)
guild.SetRecruitingPlayStyle(playStyle, false)
getMinLevel := guild.GetRecruitingMinLevel()
getPlayStyle := guild.GetRecruitingPlayStyle()
if getMinLevel != minLevel {
t.Errorf("Expected min level %d, got %d", minLevel, getMinLevel)
}
if getPlayStyle != playStyle {
t.Errorf("Expected play style %d, got %d", playStyle, getPlayStyle)
}
// Test recruiting flags
success := guild.SetRecruitingFlag(RecruitingFlagFighters, 1, false)
if !success {
t.Error("Should be able to set recruiting flag")
}
flag := guild.GetRecruitingFlag(RecruitingFlagFighters)
if flag != 1 {
t.Errorf("Expected recruiting flag 1, got %d", flag)
}
// Test recruiting description tags
success = guild.SetRecruitingDescTag(0, RecruitingDescTagRoleplay, false)
if !success {
t.Error("Should be able to set recruiting desc tag")
}
tag := guild.GetRecruitingDescTag(0)
if tag != RecruitingDescTagRoleplay {
t.Errorf("Expected description tag %d, got %d", RecruitingDescTagRoleplay, tag)
}
}
// TestGuildPermissions tests guild permission system
func TestGuildPermissions(t *testing.T) {
guild := NewGuild()
// Test setting permissions
rank := int8(RankMember)
permission := int8(PermissionInvite)
value := int8(1)
success := guild.SetPermission(rank, permission, value, false, false)
if !success {
t.Error("Should be able to set permission")
}
getValue := guild.GetPermission(rank, permission)
if getValue != value {
t.Errorf("Expected permission value %d, got %d", value, getValue)
}
// Test removing permission
success = guild.SetPermission(rank, permission, 0, false, false)
if !success {
t.Error("Should be able to remove permission")
}
getValue = guild.GetPermission(rank, permission)
if getValue != 0 {
t.Errorf("Expected permission value 0 after removal, got %d", getValue)
}
}
// TestGuildSaveFlags tests the save flag system
func TestGuildSaveFlags(t *testing.T) {
guild := NewGuild()
// Test initial state
if guild.GetSaveNeeded() {
t.Error("Guild should not need save initially")
}
// Test marking save needed
guild.SetSaveNeeded(true)
if !guild.GetSaveNeeded() {
t.Error("Guild should need save after marking")
}
// Test clearing save needed
guild.SetSaveNeeded(false)
if guild.GetSaveNeeded() {
t.Error("Guild should not need save after clearing")
}
}
// TestGuildMemberPromotionDemotion tests member rank changes
func TestGuildMemberPromotionDemotion(t *testing.T) {
guild := NewGuild()
guild.SetID(1)
// Add a member
characterID := int32(1)
inviterName := "TestInviter"
joinDate := time.Now()
success := guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit)
if !success {
t.Error("Should be able to add member")
}
// Get member and check initial rank
member := guild.GetGuildMember(characterID)
if member == nil {
t.Fatal("Member should exist")
}
if member.Rank != RankRecruit {
t.Errorf("Expected initial rank %d, got %d", RankRecruit, member.Rank)
}
// Test promotion
promoterName := "TestPromoter"
success = guild.PromoteGuildMember(characterID, promoterName, false)
if !success {
t.Error("Should be able to promote member")
}
member = guild.GetGuildMember(characterID)
if member.Rank != RankInitiate {
t.Errorf("Expected rank after promotion %d, got %d", RankInitiate, member.Rank)
}
// Test demotion
demoterName := "TestDemoter"
success = guild.DemoteGuildMember(characterID, demoterName, false)
if !success {
t.Error("Should be able to demote member")
}
member = guild.GetGuildMember(characterID)
if member.Rank != RankRecruit {
t.Errorf("Expected rank after demotion %d, got %d", RankRecruit, member.Rank)
}
}
// TestGuildPointsSystem tests the guild points system
func TestGuildPointsSystem(t *testing.T) {
guild := NewGuild()
guild.SetID(1)
// Add a member
characterID := int32(1)
inviterName := "TestInviter"
joinDate := time.Now()
success := guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit)
if !success {
t.Error("Should be able to add member")
}
// Get initial points
member := guild.GetGuildMember(characterID)
if member == nil {
t.Fatal("Member should exist")
}
initialPoints := member.Points
// Add points
pointsToAdd := 100.0
modifiedBy := "TestAdmin"
comment := "Test point award"
success = guild.AddPointsToGuildMember(characterID, pointsToAdd, modifiedBy, comment, false)
if !success {
t.Error("Should be able to add points to member")
}
// Check points were added
member = guild.GetGuildMember(characterID)
expectedPoints := initialPoints + pointsToAdd
if member.Points != expectedPoints {
t.Errorf("Expected points %f, got %f", expectedPoints, member.Points)
}
}
// TestGuildConcurrency tests thread safety of guild operations
func TestGuildConcurrency(t *testing.T) {
guild := NewGuild()
guild.SetID(1)
guild.SetName("Concurrent Test Guild", false)
const numGoroutines = 20
done := make(chan bool, numGoroutines)
// Test concurrent reads
for i := 0; i < numGoroutines; i++ {
go func(id int) {
_ = guild.GetID()
_ = guild.GetName()
_ = guild.GetLevel()
_ = guild.GetMOTD()
_ = guild.GetEXPCurrent()
_ = guild.GetRecruitingShortDesc()
_ = guild.GetRankName(RankMember)
_ = guild.GetPermission(RankMember, PermissionInvite)
done <- true
}(i)
}
// Wait for all read operations
for i := 0; i < numGoroutines; i++ {
<-done
}
// Test concurrent member additions (smaller number to avoid conflicts)
const memberGoroutines = 10
for i := 0; i < memberGoroutines; i++ {
go func(id int) {
inviterName := fmt.Sprintf("Inviter%d", id)
joinDate := time.Now()
characterID := int32(100 + id)
guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit)
done <- true
}(i)
}
// Wait for all member additions
for i := 0; i < memberGoroutines; i++ {
<-done
}
// Verify members were added
members := guild.GetAllMembers()
if len(members) != memberGoroutines {
t.Logf("Expected %d members, got %d (some concurrent additions may have failed, which is acceptable)", memberGoroutines, len(members))
}
}
// TestGuildEventFilters tests guild event filter system
func TestGuildEventFilters(t *testing.T) {
guild := NewGuild()
// Test setting event filters
eventID := int8(EventMemberJoins)
category := int8(EventFilterCategoryBroadcast)
value := int8(1)
success := guild.SetEventFilter(eventID, category, value, false, false)
if !success {
t.Error("Should be able to set event filter")
}
getValue := guild.GetEventFilter(eventID, category)
if getValue != value {
t.Errorf("Expected event filter value %d, got %d", value, getValue)
}
// Test removing event filter
success = guild.SetEventFilter(eventID, category, 0, false, false)
if !success {
t.Error("Should be able to remove event filter")
}
getValue = guild.GetEventFilter(eventID, category)
if getValue != 0 {
t.Errorf("Expected event filter value 0 after removal, got %d", getValue)
}
}
// TestGuildInfo tests the guild info structure
func TestGuildInfo(t *testing.T) {
guild := NewGuild()
guild.SetID(123)
guild.SetName("Test Guild Info", false)
guild.SetLevel(25, false)
guild.SetMOTD("Test MOTD", false)
info := guild.GetGuildInfo()
if info.ID != 123 {
t.Errorf("Expected guild info ID 123, got %d", info.ID)
}
if info.Name != "Test Guild Info" {
t.Errorf("Expected guild info name 'Test Guild Info', got '%s'", info.Name)
}
if info.Level != 25 {
t.Errorf("Expected guild info level 25, got %d", info.Level)
}
if info.MOTD != "Test MOTD" {
t.Errorf("Expected guild info MOTD 'Test MOTD', got '%s'", info.MOTD)
}
}