423 lines
14 KiB
Go
423 lines
14 KiB
Go
package achievements
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
"zombiezen.com/go/sqlite/sqlitex"
|
|
)
|
|
|
|
// LoadAllAchievements loads all achievements from database into master list
|
|
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,
|
|
category, expansion, icon, point_value, qty_req, hide_achievement,
|
|
unknown3a, unknown3b FROM achievements`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
achievement := NewAchievement()
|
|
achievement.ID = uint32(stmt.ColumnInt64(0))
|
|
achievement.Title = stmt.ColumnText(1)
|
|
achievement.UncompletedText = stmt.ColumnText(2)
|
|
achievement.CompletedText = stmt.ColumnText(3)
|
|
achievement.Category = stmt.ColumnText(4)
|
|
achievement.Expansion = stmt.ColumnText(5)
|
|
achievement.Icon = uint16(stmt.ColumnInt64(6))
|
|
achievement.PointValue = uint32(stmt.ColumnInt32(7))
|
|
achievement.QtyRequired = uint32(stmt.ColumnInt64(8))
|
|
achievement.Hide = stmt.ColumnInt64(9) != 0
|
|
achievement.Unknown3A = uint32(stmt.ColumnInt64(10))
|
|
achievement.Unknown3B = uint32(stmt.ColumnInt64(11))
|
|
|
|
// Load requirements and rewards
|
|
if err := loadAchievementRequirements(conn, achievement); err != nil {
|
|
return fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.ID, err)
|
|
}
|
|
|
|
if err := loadAchievementRewards(conn, achievement); err != nil {
|
|
return fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.ID, err)
|
|
}
|
|
|
|
if !masterList.AddAchievement(achievement) {
|
|
return fmt.Errorf("duplicate achievement ID: %d", achievement.ID)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// loadAchievementRequirements loads requirements for a specific achievement
|
|
func loadAchievementRequirements(conn *sqlite.Conn, achievement *Achievement) error {
|
|
query := `SELECT achievement_id, name, qty_req
|
|
FROM achievements_requirements
|
|
WHERE achievement_id = ?`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{achievement.ID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
req := Requirement{
|
|
AchievementID: uint32(stmt.ColumnInt64(0)),
|
|
Name: stmt.ColumnText(1),
|
|
QtyRequired: uint32(stmt.ColumnInt64(2)),
|
|
}
|
|
achievement.AddRequirement(req)
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// loadAchievementRewards loads rewards for a specific achievement
|
|
func loadAchievementRewards(conn *sqlite.Conn, achievement *Achievement) error {
|
|
query := `SELECT achievement_id, reward
|
|
FROM achievements_rewards
|
|
WHERE achievement_id = ?`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{achievement.ID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
reward := Reward{
|
|
AchievementID: uint32(stmt.ColumnInt64(0)),
|
|
Reward: stmt.ColumnText(1),
|
|
}
|
|
achievement.AddReward(reward)
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// LoadPlayerAchievements loads player achievements from database
|
|
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,
|
|
category, expansion, icon, point_value, qty_req, hide_achievement,
|
|
unknown3a, unknown3b FROM achievements`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
achievement := NewAchievement()
|
|
achievement.ID = uint32(stmt.ColumnInt64(0))
|
|
achievement.Title = stmt.ColumnText(1)
|
|
achievement.UncompletedText = stmt.ColumnText(2)
|
|
achievement.CompletedText = stmt.ColumnText(3)
|
|
achievement.Category = stmt.ColumnText(4)
|
|
achievement.Expansion = stmt.ColumnText(5)
|
|
achievement.Icon = uint16(stmt.ColumnInt64(6))
|
|
achievement.PointValue = uint32(stmt.ColumnInt64(7))
|
|
achievement.QtyRequired = uint32(stmt.ColumnInt64(8))
|
|
achievement.Hide = stmt.ColumnInt64(9) != 0
|
|
achievement.Unknown3A = uint32(stmt.ColumnInt64(10))
|
|
achievement.Unknown3B = uint32(stmt.ColumnInt64(11))
|
|
|
|
// Load requirements and rewards
|
|
if err := loadAchievementRequirements(conn, achievement); err != nil {
|
|
return fmt.Errorf("failed to load requirements: %w", err)
|
|
}
|
|
|
|
if err := loadAchievementRewards(conn, achievement); err != nil {
|
|
return fmt.Errorf("failed to load rewards: %w", err)
|
|
}
|
|
|
|
if !playerList.AddAchievement(achievement) {
|
|
return fmt.Errorf("duplicate achievement ID: %d", achievement.ID)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// LoadPlayerAchievementUpdates loads player achievement progress from database
|
|
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
|
|
FROM character_achievements
|
|
WHERE char_id = ?`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{playerID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
update := NewUpdate()
|
|
update.ID = uint32(stmt.ColumnInt64(1))
|
|
|
|
// Convert completed_date from Unix timestamp
|
|
if stmt.ColumnType(2) != sqlite.TypeNull {
|
|
timestamp := stmt.ColumnInt64(2)
|
|
update.CompletedDate = time.Unix(timestamp, 0)
|
|
}
|
|
|
|
// Load update items
|
|
if err := loadPlayerAchievementUpdateItems(conn, playerID, update); err != nil {
|
|
return fmt.Errorf("failed to load update items: %w", err)
|
|
}
|
|
|
|
if !updateList.AddUpdate(update) {
|
|
return fmt.Errorf("duplicate achievement update ID: %d", update.ID)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// loadPlayerAchievementUpdateItems loads progress items for an achievement update
|
|
func loadPlayerAchievementUpdateItems(conn *sqlite.Conn, playerID uint32, update *Update) error {
|
|
query := `SELECT achievement_id, items
|
|
FROM character_achievements_items
|
|
WHERE char_id = ? AND achievement_id = ?`
|
|
|
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{playerID, update.ID},
|
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
item := UpdateItem{
|
|
AchievementID: uint32(stmt.ColumnInt64(0)),
|
|
ItemUpdate: uint32(stmt.ColumnInt64(1)),
|
|
}
|
|
update.AddUpdateItem(item)
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// SavePlayerAchievementUpdate saves or updates player achievement progress
|
|
func SavePlayerAchievementUpdate(pool *sqlitex.Pool, playerID uint32, update *Update) error {
|
|
conn, err := pool.Take(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get connection: %w", err)
|
|
}
|
|
defer pool.Put(conn)
|
|
|
|
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 or update main achievement record
|
|
query := `INSERT OR REPLACE INTO character_achievements
|
|
(char_id, achievement_id, completed_date) VALUES (?, ?, ?)`
|
|
|
|
var completedDate any
|
|
if !update.CompletedDate.IsZero() {
|
|
completedDate = update.CompletedDate.Unix()
|
|
}
|
|
|
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{playerID, update.ID, completedDate},
|
|
})
|
|
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
|
|
func DeletePlayerAchievementUpdate(pool *sqlitex.Pool, playerID uint32, achievementID uint32) error {
|
|
conn, err := pool.Take(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get connection: %w", err)
|
|
}
|
|
defer pool.Put(conn)
|
|
|
|
err = sqlitex.Execute(conn, "BEGIN", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer sqlitex.Execute(conn, "ROLLBACK", 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
|
|
func SaveAchievement(pool *sqlitex.Pool, achievement *Achievement) error {
|
|
conn, err := pool.Take(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get connection: %w", err)
|
|
}
|
|
defer pool.Put(conn)
|
|
|
|
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.Category, achievement.Expansion, achievement.Icon,
|
|
achievement.PointValue, achievement.QtyRequired, achievement.Hide,
|
|
achievement.Unknown3A, achievement.Unknown3B,
|
|
},
|
|
})
|
|
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
|
|
func DeleteAchievement(pool *sqlitex.Pool, achievementID uint32) error {
|
|
conn, err := pool.Take(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get connection: %w", err)
|
|
}
|
|
defer pool.Put(conn)
|
|
|
|
err = sqlitex.Execute(conn, "BEGIN", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
|
|
|
// Delete main achievement
|
|
err = sqlitex.Execute(conn, "DELETE FROM achievements WHERE achievement_id = ?", &sqlitex.ExecOptions{
|
|
Args: []any{achievementID},
|
|
})
|
|
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)
|
|
}
|