eq2go/internal/achievements/achievement.go

438 lines
12 KiB
Go

package achievements
import (
"database/sql"
"fmt"
"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
}