724 lines
22 KiB
Go
724 lines
22 KiB
Go
package achievements
|
|
|
|
import (
|
|
"context"
|
|
"eq2emu/internal/database"
|
|
"eq2emu/internal/packets"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Achievement represents an achievement definition
|
|
type Achievement struct {
|
|
mu sync.RWMutex
|
|
ID uint32
|
|
AchievementID uint32
|
|
Title string
|
|
UncompletedText string
|
|
CompletedText string
|
|
Category string
|
|
Expansion string
|
|
Icon uint16
|
|
PointValue uint32
|
|
QtyRequired uint32
|
|
Hide bool
|
|
Unknown3A uint32
|
|
Unknown3B uint32
|
|
MaxVersion uint32
|
|
Requirements []Requirement
|
|
Rewards []Reward
|
|
}
|
|
|
|
// Requirement represents a requirement for an achievement
|
|
type Requirement struct {
|
|
AchievementID uint32
|
|
Name string
|
|
QtyRequired uint32
|
|
}
|
|
|
|
// Reward represents a reward for completing an achievement
|
|
type Reward struct {
|
|
AchievementID uint32
|
|
Reward string
|
|
}
|
|
|
|
// PlayerAchievement represents a player's progress on an achievement
|
|
type PlayerAchievement struct {
|
|
mu sync.RWMutex
|
|
CharacterID uint32
|
|
AchievementID uint32
|
|
Progress uint32
|
|
CompletedDate time.Time
|
|
UpdateItems []UpdateItem
|
|
}
|
|
|
|
// UpdateItem represents progress update data for an achievement
|
|
type UpdateItem struct {
|
|
AchievementID uint32
|
|
ItemUpdate uint32
|
|
}
|
|
|
|
// AchievementManager manages the achievement system
|
|
type AchievementManager struct {
|
|
mu sync.RWMutex
|
|
db *database.Database
|
|
achievements map[uint32]*Achievement // All achievements by ID
|
|
categoryIndex map[string][]*Achievement // Achievements by category
|
|
expansionIndex map[string][]*Achievement // Achievements by expansion
|
|
playerAchievements map[uint32]map[uint32]*PlayerAchievement // characterID -> achievementID -> PlayerAchievement
|
|
logger Logger
|
|
config AchievementConfig
|
|
}
|
|
|
|
// Logger interface for achievement system logging
|
|
type Logger interface {
|
|
LogInfo(system, format string, args ...any)
|
|
LogError(system, format string, args ...any)
|
|
LogDebug(system, format string, args ...any)
|
|
LogWarning(system, format string, args ...any)
|
|
}
|
|
|
|
// AchievementConfig contains achievement system configuration
|
|
type AchievementConfig struct {
|
|
EnablePacketUpdates bool
|
|
AutoCompleteOnReached bool
|
|
EnableStatistics bool
|
|
MaxCachedPlayers int
|
|
}
|
|
|
|
// NewAchievementManager creates a new achievement manager
|
|
func NewAchievementManager(db *database.Database, logger Logger, config AchievementConfig) *AchievementManager {
|
|
return &AchievementManager{
|
|
db: db,
|
|
achievements: make(map[uint32]*Achievement),
|
|
categoryIndex: make(map[string][]*Achievement),
|
|
expansionIndex: make(map[string][]*Achievement),
|
|
playerAchievements: make(map[uint32]map[uint32]*PlayerAchievement),
|
|
logger: logger,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Initialize loads achievement data and starts background processes
|
|
func (am *AchievementManager) Initialize(ctx context.Context) error {
|
|
am.mu.Lock()
|
|
defer am.mu.Unlock()
|
|
|
|
// If no database, initialize with empty data
|
|
if am.db == nil {
|
|
am.logger.LogInfo("achievements", "Loaded %d achievements", len(am.achievements))
|
|
return nil
|
|
}
|
|
|
|
// Load all achievements from database
|
|
achievements, err := am.loadAchievementsFromDB(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load achievements: %w", err)
|
|
}
|
|
|
|
for _, achievement := range achievements {
|
|
am.achievements[achievement.AchievementID] = achievement
|
|
|
|
// Build category index
|
|
if achievement.Category != "" {
|
|
am.categoryIndex[achievement.Category] = append(am.categoryIndex[achievement.Category], achievement)
|
|
}
|
|
|
|
// Build expansion index
|
|
if achievement.Expansion != "" {
|
|
am.expansionIndex[achievement.Expansion] = append(am.expansionIndex[achievement.Expansion], achievement)
|
|
}
|
|
}
|
|
|
|
am.logger.LogInfo("achievements", "Loaded %d achievements", len(am.achievements))
|
|
return nil
|
|
}
|
|
|
|
// GetAchievement returns an achievement by ID
|
|
func (am *AchievementManager) GetAchievement(achievementID uint32) (*Achievement, bool) {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
achievement, exists := am.achievements[achievementID]
|
|
return achievement, exists
|
|
}
|
|
|
|
// GetAllAchievements returns all achievements
|
|
func (am *AchievementManager) GetAllAchievements() []*Achievement {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
|
|
achievements := make([]*Achievement, 0, len(am.achievements))
|
|
for _, achievement := range am.achievements {
|
|
achievements = append(achievements, achievement)
|
|
}
|
|
return achievements
|
|
}
|
|
|
|
// GetAchievementsByCategory returns all achievements in a category
|
|
func (am *AchievementManager) GetAchievementsByCategory(category string) []*Achievement {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
return am.categoryIndex[category]
|
|
}
|
|
|
|
// GetAchievementsByExpansion returns all achievements in an expansion
|
|
func (am *AchievementManager) GetAchievementsByExpansion(expansion string) []*Achievement {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
return am.expansionIndex[expansion]
|
|
}
|
|
|
|
// GetCategories returns all unique categories
|
|
func (am *AchievementManager) GetCategories() []string {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
|
|
categories := make([]string, 0, len(am.categoryIndex))
|
|
for category := range am.categoryIndex {
|
|
categories = append(categories, category)
|
|
}
|
|
return categories
|
|
}
|
|
|
|
// GetExpansions returns all unique expansions
|
|
func (am *AchievementManager) GetExpansions() []string {
|
|
am.mu.RLock()
|
|
defer am.mu.RUnlock()
|
|
|
|
expansions := make([]string, 0, len(am.expansionIndex))
|
|
for expansion := range am.expansionIndex {
|
|
expansions = append(expansions, expansion)
|
|
}
|
|
return expansions
|
|
}
|
|
|
|
// GetPlayerAchievements returns all achievements for a character
|
|
func (am *AchievementManager) GetPlayerAchievements(characterID uint32) (map[uint32]*PlayerAchievement, error) {
|
|
am.mu.RLock()
|
|
playerAchievements, exists := am.playerAchievements[characterID]
|
|
am.mu.RUnlock()
|
|
|
|
if !exists {
|
|
// If no database, return empty map
|
|
if am.db == nil {
|
|
return make(map[uint32]*PlayerAchievement), nil
|
|
}
|
|
|
|
// Load from database
|
|
playerAchievements, err := am.loadPlayerAchievementsFromDB(context.Background(), characterID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load player achievements: %w", err)
|
|
}
|
|
|
|
am.mu.Lock()
|
|
am.playerAchievements[characterID] = playerAchievements
|
|
am.mu.Unlock()
|
|
|
|
return playerAchievements, nil
|
|
}
|
|
|
|
return playerAchievements, nil
|
|
}
|
|
|
|
// GetPlayerAchievement returns a specific player achievement
|
|
func (am *AchievementManager) GetPlayerAchievement(characterID, achievementID uint32) (*PlayerAchievement, error) {
|
|
playerAchievements, err := am.GetPlayerAchievements(characterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
playerAchievement, exists := playerAchievements[achievementID]
|
|
if !exists {
|
|
return nil, nil // Not found, but no error
|
|
}
|
|
|
|
return playerAchievement, nil
|
|
}
|
|
|
|
// UpdatePlayerProgress updates a player's progress on an achievement
|
|
func (am *AchievementManager) UpdatePlayerProgress(ctx context.Context, characterID, achievementID, progress uint32) error {
|
|
achievement, exists := am.GetAchievement(achievementID)
|
|
if !exists {
|
|
return fmt.Errorf("achievement %d not found", achievementID)
|
|
}
|
|
|
|
am.mu.Lock()
|
|
defer am.mu.Unlock()
|
|
|
|
// Get or create player achievements map
|
|
if am.playerAchievements[characterID] == nil {
|
|
am.playerAchievements[characterID] = make(map[uint32]*PlayerAchievement)
|
|
}
|
|
|
|
// Get or create player achievement
|
|
playerAchievement := am.playerAchievements[characterID][achievementID]
|
|
if playerAchievement == nil {
|
|
playerAchievement = &PlayerAchievement{
|
|
CharacterID: characterID,
|
|
AchievementID: achievementID,
|
|
Progress: 0,
|
|
UpdateItems: []UpdateItem{},
|
|
}
|
|
am.playerAchievements[characterID][achievementID] = playerAchievement
|
|
}
|
|
|
|
// Update progress
|
|
playerAchievement.mu.Lock()
|
|
oldProgress := playerAchievement.Progress
|
|
playerAchievement.Progress = progress
|
|
|
|
// Check if achievement should be completed
|
|
if am.config.AutoCompleteOnReached && progress >= achievement.QtyRequired && playerAchievement.CompletedDate.IsZero() {
|
|
playerAchievement.CompletedDate = time.Now()
|
|
am.logger.LogInfo("achievements", "Character %d completed achievement %d", characterID, achievementID)
|
|
}
|
|
playerAchievement.mu.Unlock()
|
|
|
|
// Save to database if available and progress changed
|
|
if am.db != nil && oldProgress != progress {
|
|
if err := am.savePlayerAchievementToDBInternal(ctx, playerAchievement); err != nil {
|
|
return fmt.Errorf("failed to save player achievement progress: %w", err)
|
|
}
|
|
}
|
|
|
|
am.logger.LogDebug("achievements", "Updated progress for character %d, achievement %d: %d/%d",
|
|
characterID, achievementID, progress, achievement.QtyRequired)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompletePlayerAchievement marks an achievement as completed for a player
|
|
func (am *AchievementManager) CompletePlayerAchievement(ctx context.Context, characterID, achievementID uint32) error {
|
|
_, exists := am.GetAchievement(achievementID)
|
|
if !exists {
|
|
return fmt.Errorf("achievement %d not found", achievementID)
|
|
}
|
|
|
|
am.mu.Lock()
|
|
defer am.mu.Unlock()
|
|
|
|
// Get or create player achievements map
|
|
if am.playerAchievements[characterID] == nil {
|
|
am.playerAchievements[characterID] = make(map[uint32]*PlayerAchievement)
|
|
}
|
|
|
|
// Get or create player achievement
|
|
playerAchievement := am.playerAchievements[characterID][achievementID]
|
|
if playerAchievement == nil {
|
|
playerAchievement = &PlayerAchievement{
|
|
CharacterID: characterID,
|
|
AchievementID: achievementID,
|
|
Progress: 0,
|
|
UpdateItems: []UpdateItem{},
|
|
}
|
|
am.playerAchievements[characterID][achievementID] = playerAchievement
|
|
}
|
|
|
|
// Mark as completed
|
|
playerAchievement.mu.Lock()
|
|
wasCompleted := !playerAchievement.CompletedDate.IsZero()
|
|
playerAchievement.CompletedDate = time.Now()
|
|
playerAchievement.mu.Unlock()
|
|
|
|
// Save to database if available and wasn't already completed
|
|
if am.db != nil && !wasCompleted {
|
|
if err := am.savePlayerAchievementToDBInternal(ctx, playerAchievement); err != nil {
|
|
return fmt.Errorf("failed to save player achievement completion: %w", err)
|
|
}
|
|
}
|
|
|
|
if !wasCompleted {
|
|
am.logger.LogInfo("achievements", "Character %d completed achievement %d", characterID, achievementID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsPlayerAchievementCompleted checks if a player has completed an achievement
|
|
func (am *AchievementManager) IsPlayerAchievementCompleted(characterID, achievementID uint32) (bool, error) {
|
|
playerAchievement, err := am.GetPlayerAchievement(characterID, achievementID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if playerAchievement == nil {
|
|
return false, nil
|
|
}
|
|
return !playerAchievement.CompletedDate.IsZero(), nil
|
|
}
|
|
|
|
// GetPlayerAchievementProgress returns a player's progress on an achievement
|
|
func (am *AchievementManager) GetPlayerAchievementProgress(characterID, achievementID uint32) (uint32, error) {
|
|
playerAchievement, err := am.GetPlayerAchievement(characterID, achievementID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if playerAchievement == nil {
|
|
return 0, nil
|
|
}
|
|
return playerAchievement.Progress, nil
|
|
}
|
|
|
|
// SendPlayerAchievementsPacket sends a player's achievement list to client
|
|
func (am *AchievementManager) SendPlayerAchievementsPacket(characterID uint32, clientVersion int32) error {
|
|
if !am.config.EnablePacketUpdates {
|
|
return nil // Packet updates disabled
|
|
}
|
|
|
|
playerAchievements, err := am.GetPlayerAchievements(characterID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get player achievements: %w", err)
|
|
}
|
|
|
|
def, exists := packets.GetPacket("CharacterAchievements")
|
|
if !exists {
|
|
return fmt.Errorf("CharacterAchievements packet definition not found")
|
|
}
|
|
|
|
builder := packets.NewPacketBuilder(def, uint32(clientVersion), 0)
|
|
|
|
// Build achievement array for packet
|
|
achievementArray := make([]map[string]any, 0, len(playerAchievements))
|
|
for achievementID, playerAchievement := range playerAchievements {
|
|
achievement, exists := am.GetAchievement(achievementID)
|
|
if !exists {
|
|
continue // Skip if achievement definition not found
|
|
}
|
|
|
|
achievementData := map[string]any{
|
|
"achievement_id": uint32(achievement.AchievementID),
|
|
"title": achievement.Title,
|
|
"completed_text": achievement.CompletedText,
|
|
"uncompleted_text": achievement.UncompletedText,
|
|
"category": achievement.Category,
|
|
"expansion": achievement.Expansion,
|
|
"icon": uint32(achievement.Icon),
|
|
"point_value": achievement.PointValue,
|
|
"progress": playerAchievement.Progress,
|
|
"qty_required": achievement.QtyRequired,
|
|
"completed": !playerAchievement.CompletedDate.IsZero(),
|
|
"completed_date": uint32(playerAchievement.CompletedDate.Unix()),
|
|
"hide_achievement": achievement.Hide,
|
|
}
|
|
|
|
achievementArray = append(achievementArray, achievementData)
|
|
}
|
|
|
|
packetData := map[string]any{
|
|
"num_achievements": uint16(len(achievementArray)),
|
|
"achievement_array": achievementArray,
|
|
}
|
|
|
|
packet, err := builder.Build(packetData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build packet: %w", err)
|
|
}
|
|
|
|
// TODO: Send packet to client when client interface is available
|
|
_ = packet
|
|
am.logger.LogDebug("achievements", "Built achievement list packet for character %d (%d achievements)",
|
|
characterID, len(achievementArray))
|
|
return nil
|
|
}
|
|
|
|
// SendAchievementUpdatePacket sends an achievement update to a client
|
|
func (am *AchievementManager) SendAchievementUpdatePacket(characterID, achievementID uint32, clientVersion int32) error {
|
|
if !am.config.EnablePacketUpdates {
|
|
return nil // Packet updates disabled
|
|
}
|
|
|
|
playerAchievement, err := am.GetPlayerAchievement(characterID, achievementID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get player achievement: %w", err)
|
|
}
|
|
if playerAchievement == nil {
|
|
return fmt.Errorf("player achievement not found")
|
|
}
|
|
|
|
achievement, exists := am.GetAchievement(achievementID)
|
|
if !exists {
|
|
return fmt.Errorf("achievement definition not found")
|
|
}
|
|
|
|
def, exists := packets.GetPacket("AchievementUpdateMsg")
|
|
if !exists {
|
|
return fmt.Errorf("AchievementUpdateMsg packet definition not found")
|
|
}
|
|
|
|
builder := packets.NewPacketBuilder(def, uint32(clientVersion), 0)
|
|
|
|
packetData := map[string]any{
|
|
"achievement_id": uint32(achievement.AchievementID),
|
|
"progress": playerAchievement.Progress,
|
|
"qty_required": achievement.QtyRequired,
|
|
"completed": !playerAchievement.CompletedDate.IsZero(),
|
|
"completed_date": uint32(playerAchievement.CompletedDate.Unix()),
|
|
}
|
|
|
|
packet, err := builder.Build(packetData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build packet: %w", err)
|
|
}
|
|
|
|
// TODO: Send packet to client when client interface is available
|
|
_ = packet
|
|
am.logger.LogDebug("achievements", "Built achievement update packet for character %d, achievement %d",
|
|
characterID, achievementID)
|
|
return nil
|
|
}
|
|
|
|
// GetPlayerStatistics returns achievement statistics for a player
|
|
func (am *AchievementManager) GetPlayerStatistics(characterID uint32) (*PlayerAchievementStatistics, error) {
|
|
playerAchievements, err := am.GetPlayerAchievements(characterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats := &PlayerAchievementStatistics{
|
|
CharacterID: characterID,
|
|
TotalAchievements: uint32(len(am.achievements)),
|
|
CompletedCount: 0,
|
|
InProgressCount: 0,
|
|
TotalPointsEarned: 0,
|
|
TotalPointsAvailable: 0,
|
|
CompletedByCategory: make(map[string]uint32),
|
|
}
|
|
|
|
// Calculate total points available
|
|
for _, achievement := range am.achievements {
|
|
stats.TotalPointsAvailable += achievement.PointValue
|
|
}
|
|
|
|
// Calculate player statistics
|
|
for achievementID, playerAchievement := range playerAchievements {
|
|
achievement, exists := am.GetAchievement(achievementID)
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
if !playerAchievement.CompletedDate.IsZero() {
|
|
stats.CompletedCount++
|
|
stats.TotalPointsEarned += achievement.PointValue
|
|
stats.CompletedByCategory[achievement.Category]++
|
|
} else if playerAchievement.Progress > 0 {
|
|
stats.InProgressCount++
|
|
}
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// PlayerAchievementStatistics contains achievement statistics for a player
|
|
type PlayerAchievementStatistics struct {
|
|
CharacterID uint32
|
|
TotalAchievements uint32
|
|
CompletedCount uint32
|
|
InProgressCount uint32
|
|
TotalPointsEarned uint32
|
|
TotalPointsAvailable uint32
|
|
CompletedByCategory map[string]uint32
|
|
}
|
|
|
|
// Database operations (internal)
|
|
|
|
func (am *AchievementManager) loadAchievementsFromDB(ctx context.Context) ([]*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 := am.db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query achievements: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
achievements := make([]*Achievement, 0)
|
|
for rows.Next() {
|
|
achievement := &Achievement{
|
|
Requirements: []Requirement{},
|
|
Rewards: []Reward{},
|
|
}
|
|
|
|
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 := am.loadAchievementRequirementsFromDB(ctx, achievement); err != nil {
|
|
return nil, fmt.Errorf("failed to load requirements for achievement %d: %w", achievement.AchievementID, err)
|
|
}
|
|
|
|
if err := am.loadAchievementRewardsFromDB(ctx, achievement); err != nil {
|
|
return nil, fmt.Errorf("failed to load rewards for achievement %d: %w", achievement.AchievementID, err)
|
|
}
|
|
|
|
achievements = append(achievements, achievement)
|
|
}
|
|
|
|
return achievements, nil
|
|
}
|
|
|
|
func (am *AchievementManager) loadAchievementRequirementsFromDB(_ context.Context, achievement *Achievement) error {
|
|
query := `
|
|
SELECT achievement_id, name, qty_req
|
|
FROM achievements_requirements
|
|
WHERE achievement_id = ?
|
|
`
|
|
|
|
rows, err := am.db.Query(query, achievement.AchievementID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var req Requirement
|
|
err := rows.Scan(&req.AchievementID, &req.Name, &req.QtyRequired)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
achievement.Requirements = append(achievement.Requirements, req)
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
func (am *AchievementManager) loadAchievementRewardsFromDB(_ context.Context, achievement *Achievement) error {
|
|
query := `
|
|
SELECT achievement_id, reward
|
|
FROM achievements_rewards
|
|
WHERE achievement_id = ?
|
|
`
|
|
|
|
rows, err := am.db.Query(query, achievement.AchievementID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var reward Reward
|
|
err := rows.Scan(&reward.AchievementID, &reward.Reward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
achievement.Rewards = append(achievement.Rewards, reward)
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
func (am *AchievementManager) loadPlayerAchievementsFromDB(ctx context.Context, characterID uint32) (map[uint32]*PlayerAchievement, error) {
|
|
query := `
|
|
SELECT achievement_id, completed_date
|
|
FROM character_achievements
|
|
WHERE char_id = ?
|
|
`
|
|
|
|
rows, err := am.db.Query(query, characterID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query player achievements: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
playerAchievements := make(map[uint32]*PlayerAchievement)
|
|
for rows.Next() {
|
|
var achievementID uint32
|
|
var completedDate int64
|
|
|
|
err := rows.Scan(&achievementID, &completedDate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan player achievement: %w", err)
|
|
}
|
|
|
|
playerAchievement := &PlayerAchievement{
|
|
CharacterID: characterID,
|
|
AchievementID: achievementID,
|
|
Progress: 0,
|
|
UpdateItems: []UpdateItem{},
|
|
}
|
|
|
|
if completedDate > 0 {
|
|
playerAchievement.CompletedDate = time.Unix(completedDate, 0)
|
|
}
|
|
|
|
// Load update items
|
|
if err := am.loadPlayerAchievementUpdateItemsFromDB(ctx, characterID, achievementID, playerAchievement); err != nil {
|
|
return nil, fmt.Errorf("failed to load update items for character %d, achievement %d: %w", characterID, achievementID, err)
|
|
}
|
|
|
|
playerAchievements[achievementID] = playerAchievement
|
|
}
|
|
|
|
return playerAchievements, nil
|
|
}
|
|
|
|
func (am *AchievementManager) loadPlayerAchievementUpdateItemsFromDB(_ context.Context, characterID, achievementID uint32, playerAchievement *PlayerAchievement) error {
|
|
query := `
|
|
SELECT achievement_id, items
|
|
FROM character_achievements_items
|
|
WHERE char_id = ? AND achievement_id = ?
|
|
`
|
|
|
|
rows, err := am.db.Query(query, characterID, achievementID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var totalProgress uint32
|
|
for rows.Next() {
|
|
var updateItem UpdateItem
|
|
err := rows.Scan(&updateItem.AchievementID, &updateItem.ItemUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
playerAchievement.UpdateItems = append(playerAchievement.UpdateItems, updateItem)
|
|
totalProgress += updateItem.ItemUpdate
|
|
}
|
|
|
|
playerAchievement.Progress = totalProgress
|
|
return rows.Err()
|
|
}
|
|
|
|
func (am *AchievementManager) savePlayerAchievementToDBInternal(_ context.Context, playerAchievement *PlayerAchievement) error {
|
|
var completedDate int64
|
|
if !playerAchievement.CompletedDate.IsZero() {
|
|
completedDate = playerAchievement.CompletedDate.Unix()
|
|
}
|
|
|
|
// Insert or update achievement progress
|
|
query := `
|
|
INSERT OR REPLACE INTO character_achievements (char_id, achievement_id, completed_date)
|
|
VALUES (?, ?, ?)
|
|
`
|
|
|
|
_, err := am.db.Exec(query, playerAchievement.CharacterID, playerAchievement.AchievementID, completedDate)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player achievement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the achievement manager
|
|
func (am *AchievementManager) Shutdown(ctx context.Context) error {
|
|
am.logger.LogInfo("achievements", "Shutting down achievement manager")
|
|
// Any cleanup would go here
|
|
return nil
|
|
} |