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) }