package achievements import ( "database/sql" "fmt" "time" "eq2emu/internal/database" ) // Achievement represents a complete achievement with database operations type Achievement struct { // Database fields ID uint32 `json:"id" db:"id"` AchievementID uint32 `json:"achievement_id" db:"achievement_id"` Title string `json:"title" db:"title"` UncompletedText string `json:"uncompleted_text" db:"uncompleted_text"` CompletedText string `json:"completed_text" db:"completed_text"` Category string `json:"category" db:"category"` Expansion string `json:"expansion" db:"expansion"` Icon uint16 `json:"icon" db:"icon"` PointValue uint32 `json:"point_value" db:"point_value"` QtyRequired uint32 `json:"qty_req" db:"qty_req"` Hide bool `json:"hide_achievement" db:"hide_achievement"` Unknown3A uint32 `json:"unknown3a" db:"unknown3a"` Unknown3B uint32 `json:"unknown3b" db:"unknown3b"` MaxVersion uint32 `json:"max_version" db:"max_version"` // Associated data Requirements []Requirement `json:"requirements"` Rewards []Reward `json:"rewards"` // Database connection db *database.Database isNew bool } // New creates a new achievement with database connection func New(db *database.Database) *Achievement { return &Achievement{ Requirements: make([]Requirement, 0), Rewards: make([]Reward, 0), db: db, isNew: true, } } // Load loads an achievement by achievement_id func Load(db *database.Database, achievementID uint32) (*Achievement, error) { achievement := &Achievement{ db: db, isNew: false, } query := `SELECT id, achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version FROM achievements WHERE achievement_id = ?` var hideInt int err := db.QueryRow(query, achievementID).Scan( &achievement.ID, &achievement.AchievementID, &achievement.Title, &achievement.UncompletedText, &achievement.CompletedText, &achievement.Category, &achievement.Expansion, &achievement.Icon, &achievement.PointValue, &achievement.QtyRequired, &hideInt, &achievement.Unknown3A, &achievement.Unknown3B, &achievement.MaxVersion, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("achievement not found: %d", achievementID) } return nil, fmt.Errorf("failed to load achievement: %w", err) } achievement.Hide = hideInt != 0 // Load requirements and rewards if err := achievement.loadRequirements(); err != nil { return nil, fmt.Errorf("failed to load requirements: %w", err) } if err := achievement.loadRewards(); err != nil { return nil, fmt.Errorf("failed to load rewards: %w", err) } return achievement, nil } // LoadAll loads all achievements from database func LoadAll(db *database.Database) ([]*Achievement, error) { query := `SELECT id, achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version FROM achievements ORDER BY achievement_id` rows, err := db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query achievements: %w", err) } defer rows.Close() var achievements []*Achievement for rows.Next() { achievement := &Achievement{ db: db, isNew: false, } var hideInt int err := rows.Scan( &achievement.ID, &achievement.AchievementID, &achievement.Title, &achievement.UncompletedText, &achievement.CompletedText, &achievement.Category, &achievement.Expansion, &achievement.Icon, &achievement.PointValue, &achievement.QtyRequired, &hideInt, &achievement.Unknown3A, &achievement.Unknown3B, &achievement.MaxVersion, ) if err != nil { return nil, fmt.Errorf("failed to scan achievement: %w", err) } achievement.Hide = hideInt != 0 // Load requirements and rewards if err := achievement.loadRequirements(); err != nil { return nil, fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.AchievementID, err) } if err := achievement.loadRewards(); err != nil { return nil, fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.AchievementID, err) } achievements = append(achievements, achievement) } return achievements, rows.Err() } // Save saves the achievement to the database (insert if new, update if existing) func (a *Achievement) Save() error { if a.db == nil { return fmt.Errorf("no database connection") } tx, err := a.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() if a.isNew { err = a.insert(tx) } else { err = a.update(tx) } if err != nil { return err } // Save requirements and rewards if err := a.saveRequirements(tx); err != nil { return fmt.Errorf("failed to save requirements: %w", err) } if err := a.saveRewards(tx); err != nil { return fmt.Errorf("failed to save rewards: %w", err) } return tx.Commit() } // Delete removes the achievement and all associated data from the database func (a *Achievement) Delete() error { if a.db == nil { return fmt.Errorf("no database connection") } if a.isNew { return fmt.Errorf("cannot delete unsaved achievement") } tx, err := a.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete requirements (foreign key should cascade, but be explicit) _, err = tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", a.AchievementID) if err != nil { return fmt.Errorf("failed to delete requirements: %w", err) } // Delete rewards _, err = tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", a.AchievementID) if err != nil { return fmt.Errorf("failed to delete rewards: %w", err) } // Delete achievement _, err = tx.Exec("DELETE FROM achievements WHERE achievement_id = ?", a.AchievementID) if err != nil { return fmt.Errorf("failed to delete achievement: %w", err) } return tx.Commit() } // Reload reloads the achievement from the database func (a *Achievement) Reload() error { if a.db == nil { return fmt.Errorf("no database connection") } if a.isNew { return fmt.Errorf("cannot reload unsaved achievement") } reloaded, err := Load(a.db, a.AchievementID) if err != nil { return err } // Copy all fields from reloaded achievement *a = *reloaded return nil } // AddRequirement adds a requirement to this achievement func (a *Achievement) AddRequirement(name string, qtyRequired uint32) { req := Requirement{ AchievementID: a.AchievementID, Name: name, QtyRequired: qtyRequired, } a.Requirements = append(a.Requirements, req) } // AddReward adds a reward to this achievement func (a *Achievement) AddReward(reward string) { r := Reward{ AchievementID: a.AchievementID, Reward: reward, } a.Rewards = append(a.Rewards, r) } // IsNew returns true if this is a new (unsaved) achievement func (a *Achievement) IsNew() bool { return a.isNew } // GetID returns the achievement ID (implements common.Identifiable interface) func (a *Achievement) GetID() uint32 { return a.AchievementID } // Clone creates a deep copy of the achievement func (a *Achievement) Clone() *Achievement { clone := &Achievement{ ID: a.ID, AchievementID: a.AchievementID, Title: a.Title, UncompletedText: a.UncompletedText, CompletedText: a.CompletedText, Category: a.Category, Expansion: a.Expansion, Icon: a.Icon, PointValue: a.PointValue, QtyRequired: a.QtyRequired, Hide: a.Hide, Unknown3A: a.Unknown3A, Unknown3B: a.Unknown3B, MaxVersion: a.MaxVersion, Requirements: make([]Requirement, len(a.Requirements)), Rewards: make([]Reward, len(a.Rewards)), db: a.db, isNew: false, } copy(clone.Requirements, a.Requirements) copy(clone.Rewards, a.Rewards) return clone } // Private helper methods func (a *Achievement) insert(tx *sql.Tx) error { var query string if a.db.GetType() == database.MySQL { query = `INSERT INTO achievements (achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` } else { query = `INSERT INTO achievements (achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` } result, err := tx.Exec(query, a.AchievementID, a.Title, a.UncompletedText, a.CompletedText, a.Category, a.Expansion, a.Icon, a.PointValue, a.QtyRequired, a.Hide, a.Unknown3A, a.Unknown3B, a.MaxVersion) if err != nil { return fmt.Errorf("failed to insert achievement: %w", err) } // Get the auto-generated ID if a.db.GetType() == database.MySQL { id, err := result.LastInsertId() if err == nil { a.ID = uint32(id) } } a.isNew = false return nil } func (a *Achievement) update(tx *sql.Tx) error { query := `UPDATE achievements SET title = ?, uncompleted_text = ?, completed_text = ?, category = ?, expansion = ?, icon = ?, point_value = ?, qty_req = ?, hide_achievement = ?, unknown3a = ?, unknown3b = ?, max_version = ? WHERE achievement_id = ?` _, err := tx.Exec(query, a.Title, a.UncompletedText, a.CompletedText, a.Category, a.Expansion, a.Icon, a.PointValue, a.QtyRequired, a.Hide, a.Unknown3A, a.Unknown3B, a.MaxVersion, a.AchievementID) if err != nil { return fmt.Errorf("failed to update achievement: %w", err) } return nil } func (a *Achievement) loadRequirements() error { query := `SELECT achievement_id, name, qty_req FROM achievements_requirements WHERE achievement_id = ?` rows, err := a.db.Query(query, a.AchievementID) if err != nil { return err } defer rows.Close() a.Requirements = make([]Requirement, 0) for rows.Next() { var req Requirement err := rows.Scan(&req.AchievementID, &req.Name, &req.QtyRequired) if err != nil { return err } a.Requirements = append(a.Requirements, req) } return rows.Err() } func (a *Achievement) loadRewards() error { query := `SELECT achievement_id, reward FROM achievements_rewards WHERE achievement_id = ?` rows, err := a.db.Query(query, a.AchievementID) if err != nil { return err } defer rows.Close() a.Rewards = make([]Reward, 0) for rows.Next() { var reward Reward err := rows.Scan(&reward.AchievementID, &reward.Reward) if err != nil { return err } a.Rewards = append(a.Rewards, reward) } return rows.Err() } func (a *Achievement) saveRequirements(tx *sql.Tx) error { // Delete existing requirements _, err := tx.Exec("DELETE FROM achievements_requirements WHERE achievement_id = ?", a.AchievementID) if err != nil { return err } // Insert new requirements if len(a.Requirements) > 0 { query := `INSERT INTO achievements_requirements (achievement_id, name, qty_req) VALUES (?, ?, ?)` for _, req := range a.Requirements { _, err = tx.Exec(query, a.AchievementID, req.Name, req.QtyRequired) if err != nil { return err } } } return nil } func (a *Achievement) saveRewards(tx *sql.Tx) error { // Delete existing rewards _, err := tx.Exec("DELETE FROM achievements_rewards WHERE achievement_id = ?", a.AchievementID) if err != nil { return err } // Insert new rewards if len(a.Rewards) > 0 { query := `INSERT INTO achievements_rewards (achievement_id, reward) VALUES (?, ?)` for _, reward := range a.Rewards { _, err = tx.Exec(query, a.AchievementID, reward.Reward) if err != nil { return err } } } return nil } // LoadAllAchievements loads all achievements from database into a master list func LoadAllAchievements(db *database.Database, masterList *MasterList) error { query := `SELECT id, achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version FROM achievements ORDER BY achievement_id` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() var achievements []*Achievement for rows.Next() { achievement := &Achievement{ db: db, isNew: false, } var hideInt int err := rows.Scan( &achievement.ID, &achievement.AchievementID, &achievement.Title, &achievement.UncompletedText, &achievement.CompletedText, &achievement.Category, &achievement.Expansion, &achievement.Icon, &achievement.PointValue, &achievement.QtyRequired, &hideInt, &achievement.Unknown3A, &achievement.Unknown3B, &achievement.MaxVersion, ) if err != nil { return fmt.Errorf("failed to scan achievement: %w", err) } achievement.Hide = hideInt != 0 achievements = append(achievements, achievement) } if err := rows.Err(); err != nil { return fmt.Errorf("failed to iterate rows: %w", err) } // Load requirements and rewards for each achievement for _, achievement := range achievements { if err := achievement.loadRequirements(); err != nil { return fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.AchievementID, err) } if err := achievement.loadRewards(); err != nil { return fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.AchievementID, err) } // Add to master list masterList.AddAchievement(achievement) } return nil } // LoadPlayerAchievements loads all achievements for a specific player func LoadPlayerAchievements(db *database.Database, characterID uint32, playerList *PlayerList) error { // For now, we load all achievements for the player (matching C++ behavior) // In the future, this could be optimized to only load unlocked achievements query := `SELECT id, achievement_id, title, uncompleted_text, completed_text, category, expansion, icon, point_value, qty_req, hide_achievement, unknown3a, unknown3b, max_version FROM achievements ORDER BY achievement_id` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() for rows.Next() { achievement := &Achievement{ db: db, isNew: false, } var hideInt int err := rows.Scan( &achievement.ID, &achievement.AchievementID, &achievement.Title, &achievement.UncompletedText, &achievement.CompletedText, &achievement.Category, &achievement.Expansion, &achievement.Icon, &achievement.PointValue, &achievement.QtyRequired, &hideInt, &achievement.Unknown3A, &achievement.Unknown3B, &achievement.MaxVersion, ) if err != nil { return fmt.Errorf("failed to scan achievement: %w", err) } achievement.Hide = hideInt != 0 // Load requirements and rewards if err := achievement.loadRequirements(); err != nil { return fmt.Errorf("failed to load requirements: %w", err) } if err := achievement.loadRewards(); err != nil { return fmt.Errorf("failed to load rewards: %w", err) } // Add to player list playerList.AddAchievement(achievement) } return rows.Err() } // LoadPlayerAchievementUpdates loads player achievement progress from database func LoadPlayerAchievementUpdates(db *database.Database, characterID uint32, updateList *PlayerUpdateList) error { query := `SELECT achievement_id, completed_date FROM character_achievements WHERE char_id = ?` rows, err := db.Query(query, characterID) if err != nil { return fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() for rows.Next() { update := NewUpdate() var completedDate int64 err := rows.Scan(&update.ID, &completedDate) if err != nil { return fmt.Errorf("failed to scan update: %w", err) } if completedDate > 0 { update.CompletedDate = time.Unix(completedDate, 0) } // Load update items for this achievement if err := loadPlayerAchievementUpdateItems(db, characterID, update); err != nil { return fmt.Errorf("failed to load update items: %w", err) } updateList.AddUpdate(update) } return rows.Err() } // loadPlayerAchievementUpdateItems loads update items for a specific achievement update func loadPlayerAchievementUpdateItems(db *database.Database, characterID uint32, update *Update) error { query := `SELECT achievement_id, items FROM character_achievements_items WHERE char_id = ? AND achievement_id = ?` rows, err := db.Query(query, characterID, update.ID) if err != nil { return fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() for rows.Next() { var updateItem UpdateItem err := rows.Scan(&updateItem.AchievementID, &updateItem.ItemUpdate) if err != nil { return fmt.Errorf("failed to scan update item: %w", err) } update.AddUpdateItem(updateItem) } return rows.Err() } // SavePlayerAchievementUpdate saves a player's achievement progress to database func SavePlayerAchievementUpdate(db *database.Database, characterID uint32, update *Update) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() var completedDate int64 if !update.CompletedDate.IsZero() { completedDate = update.CompletedDate.Unix() } // Insert or update achievement progress query := `INSERT OR REPLACE INTO character_achievements (char_id, achievement_id, completed_date) VALUES (?, ?, ?)` _, err = tx.Exec(query, characterID, 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 = tx.Exec(deleteQuery, characterID, update.ID) if err != nil { return fmt.Errorf("failed to delete existing update items: %w", err) } // Insert new update items if len(update.UpdateItems) > 0 { insertQuery := `INSERT INTO character_achievements_items (char_id, achievement_id, items) VALUES (?, ?, ?)` for _, item := range update.UpdateItems { _, err = tx.Exec(insertQuery, characterID, item.AchievementID, item.ItemUpdate) if err != nil { return fmt.Errorf("failed to insert update item: %w", err) } } } // Commit transaction return tx.Commit() }