package world import ( "fmt" "sync" "time" "eq2emu/internal/achievements" "eq2emu/internal/database" "eq2emu/internal/packets" ) // AchievementManager manages achievements for the world server type AchievementManager struct { masterList *achievements.MasterList playerManagers map[int32]*achievements.PlayerManager // CharacterID -> PlayerManager database *database.Database world *World // Reference to world server for notifications mutex sync.RWMutex } // NewAchievementManager creates a new achievement manager func NewAchievementManager(db *database.Database) *AchievementManager { return &AchievementManager{ masterList: achievements.NewMasterList(), playerManagers: make(map[int32]*achievements.PlayerManager), database: db, world: nil, // Set by world server after creation } } // SetWorld sets the world server reference for notifications func (am *AchievementManager) SetWorld(world *World) { am.world = world } // LoadAchievements loads all achievements from database func (am *AchievementManager) LoadAchievements() error { fmt.Println("Loading master achievement list...") err := achievements.LoadAllAchievements(am.database, am.masterList) if err != nil { return fmt.Errorf("failed to load achievements: %w", err) } fmt.Printf("Loaded %d achievements\n", am.masterList.Size()) return nil } // GetPlayerManager gets or creates a player achievement manager func (am *AchievementManager) GetPlayerManager(characterID int32) *achievements.PlayerManager { am.mutex.RLock() playerMgr, exists := am.playerManagers[characterID] am.mutex.RUnlock() if exists { return playerMgr } // Create new player manager and load data am.mutex.Lock() defer am.mutex.Unlock() // Double-check after acquiring write lock if playerMgr, exists := am.playerManagers[characterID]; exists { return playerMgr } playerMgr = achievements.NewPlayerManager() am.playerManagers[characterID] = playerMgr // Load player achievement data from database go am.loadPlayerAchievements(characterID, playerMgr) return playerMgr } // loadPlayerAchievements loads achievement data for a specific player func (am *AchievementManager) loadPlayerAchievements(characterID int32, playerMgr *achievements.PlayerManager) { // Load player achievements err := achievements.LoadPlayerAchievements(am.database, uint32(characterID), playerMgr.Achievements) if err != nil { fmt.Printf("Error loading achievements for character %d: %v\n", characterID, err) } // Load player progress err = achievements.LoadPlayerAchievementUpdates(am.database, uint32(characterID), playerMgr.Updates) if err != nil { fmt.Printf("Error loading achievement progress for character %d: %v\n", characterID, err) } } // UpdateProgress updates player progress for an achievement func (am *AchievementManager) UpdateProgress(characterID int32, achievementID uint32, progress uint32) error { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return fmt.Errorf("failed to get player manager for character %d", characterID) } // Update progress playerMgr.Updates.UpdateProgress(achievementID, progress) // Check if achievement is completed achievement := am.masterList.GetAchievement(achievementID) if achievement != nil { completed, err := playerMgr.CheckRequirements(achievement) if err != nil { return fmt.Errorf("failed to check requirements: %w", err) } if completed && !playerMgr.Updates.IsCompleted(achievementID) { // Complete the achievement playerMgr.Updates.CompleteAchievement(achievementID) // Save progress to database go am.savePlayerProgress(characterID, achievementID, playerMgr) // Trigger achievement completion event go am.onAchievementCompleted(characterID, achievement) // Send achievement update packet to client go am.sendAchievementUpdateToClient(characterID) fmt.Printf("Character %d completed achievement: %s\n", characterID, achievement.Title) } else if progress > 0 { // Save progress update to database go am.savePlayerProgress(characterID, achievementID, playerMgr) // Send achievement update packet for progress update go am.sendAchievementUpdateToClient(characterID) } } return nil } // savePlayerProgress saves player achievement progress to database func (am *AchievementManager) savePlayerProgress(characterID int32, achievementID uint32, playerMgr *achievements.PlayerManager) { update := playerMgr.Updates.GetUpdate(achievementID) if update == nil { return } err := achievements.SavePlayerAchievementUpdate(am.database, uint32(characterID), update) if err != nil { fmt.Printf("Error saving achievement progress for character %d, achievement %d: %v\n", characterID, achievementID, err) } } // onAchievementCompleted handles achievement completion events func (am *AchievementManager) onAchievementCompleted(characterID int32, achievement *achievements.Achievement) { // Award points if achievement.PointValue > 0 { // Increment player's achievement points fmt.Printf("Character %d earned %d achievement points\n", characterID, achievement.PointValue) } // Process rewards for _, reward := range achievement.Rewards { am.processReward(characterID, reward) } // Notify other systems about achievement completion am.notifyAchievementCompleted(characterID, achievement.ID) } // notifyAchievementCompleted notifies other systems about achievement completion func (am *AchievementManager) notifyAchievementCompleted(characterID int32, achievementID uint32) { // Notify title system if available if am.world != nil && am.world.titleMgr != nil { integrationMgr := am.world.titleMgr.GetIntegrationManager() if integrationMgr != nil { achievementIntegration := integrationMgr.GetAchievementIntegration() if achievementIntegration != nil { err := achievementIntegration.OnAchievementCompleted(characterID, achievementID) if err != nil { fmt.Printf("Error processing achievement completion for titles: %v\n", err) } } } } } // processReward processes an achievement reward func (am *AchievementManager) processReward(characterID int32, reward achievements.Reward) { // Basic reward processing - extend based on reward types switch reward.Reward { case "title": // Award title fmt.Printf("Character %d earned a title reward\n", characterID) case "item": // Award item fmt.Printf("Character %d earned an item reward\n", characterID) case "experience": // Award experience fmt.Printf("Character %d earned experience reward\n", characterID) default: fmt.Printf("Character %d earned reward: %s\n", characterID, reward.Reward) } } // GetAchievement gets an achievement by ID from master list func (am *AchievementManager) GetAchievement(achievementID uint32) *achievements.Achievement { return am.masterList.GetAchievement(achievementID) } // GetAchievementsByCategory gets achievements filtered by category func (am *AchievementManager) GetAchievementsByCategory(category string) []*achievements.Achievement { return am.masterList.GetAchievementsByCategory(category) } // GetAchievementsByExpansion gets achievements filtered by expansion func (am *AchievementManager) GetAchievementsByExpansion(expansion string) []*achievements.Achievement { return am.masterList.GetAchievementsByExpansion(expansion) } // GetPlayerProgress gets player's progress for an achievement func (am *AchievementManager) GetPlayerProgress(characterID int32, achievementID uint32) uint32 { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return 0 } return playerMgr.Updates.GetProgress(achievementID) } // IsPlayerCompleted checks if player has completed an achievement func (am *AchievementManager) IsPlayerCompleted(characterID int32, achievementID uint32) bool { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return false } return playerMgr.Updates.IsCompleted(achievementID) } // GetPlayerCompletedAchievements gets all completed achievement IDs for a player func (am *AchievementManager) GetPlayerCompletedAchievements(characterID int32) []uint32 { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return nil } return playerMgr.Updates.GetCompletedAchievements() } // GetPlayerInProgressAchievements gets all in-progress achievement IDs for a player func (am *AchievementManager) GetPlayerInProgressAchievements(characterID int32) []uint32 { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return nil } return playerMgr.Updates.GetInProgressAchievements() } // GetCompletionPercentage gets completion percentage for player's achievement func (am *AchievementManager) GetCompletionPercentage(characterID int32, achievementID uint32) float64 { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return 0.0 } achievement := am.masterList.GetAchievement(achievementID) if achievement == nil { return 0.0 } return playerMgr.GetCompletionStatus(achievement) } // RemovePlayerManager removes a player manager (called when player logs out) func (am *AchievementManager) RemovePlayerManager(characterID int32) { am.mutex.Lock() defer am.mutex.Unlock() delete(am.playerManagers, characterID) } // GetMasterList returns the master achievement list func (am *AchievementManager) GetMasterList() *achievements.MasterList { return am.masterList } // GetStatistics returns achievement system statistics func (am *AchievementManager) GetStatistics() map[string]any { am.mutex.RLock() defer am.mutex.RUnlock() stats := map[string]any{ "total_achievements": am.masterList.Size(), "online_players": len(am.playerManagers), "categories": am.masterList.GetCategories(), "expansions": am.masterList.GetExpansions(), } return stats } // CreateAchievementUpdatePacket creates an achievement update packet for a player func (am *AchievementManager) CreateAchievementUpdatePacket(characterID int32, version uint32) ([]byte, error) { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return nil, fmt.Errorf("player manager not found for character %d", characterID) } updates := playerMgr.Updates.GetAllUpdates() // Build the packet data map according to the AchievementUpdate.xml structure achievementArray := make([]map[string]any, 0, len(updates)) for achievementID, update := range updates { var completedDate uint32 if !update.CompletedDate.IsZero() { completedDate = uint32(update.CompletedDate.Unix()) } // Build item array for this achievement itemArray := make([]map[string]any, 0, len(update.UpdateItems)) for _, item := range update.UpdateItems { itemArray = append(itemArray, map[string]any{ "item_update": item.ItemUpdate, }) } achievementData := map[string]any{ "achievement_id": achievementID, "completed_date": completedDate, "num_items": uint8(len(update.UpdateItems)), "item_array": itemArray, } achievementArray = append(achievementArray, achievementData) } packetData := map[string]any{ "unknown1": uint8(0), "num_achievements": uint16(len(updates)), "achievement_array": achievementArray, } // Build the packet using the packet system packetBytes, err := packets.BuildPacket("AchievementUpdate", packetData, version, 0) if err != nil { return nil, fmt.Errorf("failed to build achievement update packet: %w", err) } return packetBytes, nil } // SendAchievementUpdateToPlayer sends achievement update packet to a player func (am *AchievementManager) SendAchievementUpdateToPlayer(characterID int32, clientVersion int32) error { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return fmt.Errorf("player manager not found for character %d", characterID) } // Create the packet data packetData, err := am.CreateAchievementUpdatePacket(characterID, uint32(clientVersion)) if err != nil { return fmt.Errorf("failed to create achievement update packet: %w", err) } // TODO: Send packet to player through world server client connection // This would typically use the world server's client manager if am.world != nil { // Get client opcode for this version clientOpcode := packets.InternalToClient(packets.OP_AchievementUpdateMsg, clientVersion) if clientOpcode == 0 { return fmt.Errorf("no client opcode mapping for achievement update in version %d", clientVersion) } fmt.Printf("Would send achievement update packet to character %d (opcode: %d, size: %d bytes)\n", characterID, clientOpcode, len(packetData)) // In a real implementation: // return am.world.SendPacketToClient(characterID, clientOpcode, packetData) } return nil } // CreateCharacterAchievementsPacket creates a character achievements packet (master list) for a player func (am *AchievementManager) CreateCharacterAchievementsPacket(characterID int32, version uint32) ([]byte, error) { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return nil, fmt.Errorf("player manager not found for character %d", characterID) } // Get all achievements from master list allAchievements := am.masterList.GetAllAchievements() // Build achievement array according to CharacterAchievements.xml structure achievementArray := make([]map[string]any, 0, len(allAchievements)) for _, achievement := range allAchievements { // Build requirements array itemArray := make([]map[string]any, 0, len(achievement.Requirements)) for _, req := range achievement.Requirements { itemArray = append(itemArray, map[string]any{ "item_name": req.Name, "item_qty_req": req.QtyRequired, }) } // Build rewards array rewardArray := make([]map[string]any, 0, len(achievement.Rewards)) for _, reward := range achievement.Rewards { rewardData := map[string]any{ "reward_item": reward.Reward, } // Add unknown4 field for version 57032+ if version >= 57032 { rewardData["unknown4"] = uint32(0) } rewardArray = append(rewardArray, rewardData) } // Build achievement data based on version achievementData := map[string]any{ "achievement_id": achievement.AchievementID, "title": achievement.Title, "uncompleted_text": achievement.UncompletedText, "completed_text": achievement.CompletedText, "category": achievement.Category, "expansion": achievement.Expansion, "icon": achievement.Icon, "point_value": achievement.PointValue, "qty_req": achievement.QtyRequired, "hide_achievement": uint8(0), // Convert bool to uint8 } if achievement.Hide { achievementData["hide_achievement"] = uint8(1) } // Handle version-specific fields switch { case version >= 57032: achievementData["unknown3"] = [2]uint32{achievement.Unknown3A, achievement.Unknown3B} achievementData["num_items"] = uint8(len(achievement.Requirements)) achievementData["item_array"] = itemArray achievementData["num_rewards"] = uint8(len(achievement.Rewards)) achievementData["reward_array"] = rewardArray achievementData["num_reward_links"] = uint8(0) // TODO: Implement reward links if needed achievementData["reward_link_array"] = []map[string]any{} case version >= 1096: achievementData["unknown3"] = [2]uint32{achievement.Unknown3A, achievement.Unknown3B} achievementData["num_items"] = uint8(len(achievement.Requirements)) achievementData["item_array"] = itemArray achievementData["num_rewards"] = uint8(len(achievement.Rewards)) achievementData["reward_array"] = rewardArray case version >= 603: achievementData["unknown3a"] = achievement.Unknown3A achievementData["unknown3b"] = achievement.Unknown3B achievementData["guild"] = uint8(0) // TODO: Implement guild achievements if needed achievementData["num_items"] = uint8(len(achievement.Requirements)) achievementData["item_array"] = itemArray achievementData["num_reward_links"] = uint8(0) // TODO: Implement reward links if needed achievementData["reward_link_array"] = []map[string]any{} } achievementArray = append(achievementArray, achievementData) } packetData := map[string]any{ "num_achievements": uint16(len(allAchievements)), "achievement_array": achievementArray, } // Build the packet using the packet system packetBytes, err := packets.BuildPacket("CharacterAchievements", packetData, version, 0) if err != nil { return nil, fmt.Errorf("failed to build character achievements packet: %w", err) } return packetBytes, nil } // SendCharacterAchievementsToPlayer sends the master achievement list to a player func (am *AchievementManager) SendCharacterAchievementsToPlayer(characterID int32, clientVersion int32) error { // Create the packet data packetData, err := am.CreateCharacterAchievementsPacket(characterID, uint32(clientVersion)) if err != nil { return fmt.Errorf("failed to create character achievements packet: %w", err) } // TODO: Send packet to player through world server client connection if am.world != nil { // Get client opcode for this version clientOpcode := packets.InternalToClient(packets.OP_CharacterAchievements, clientVersion) if clientOpcode == 0 { return fmt.Errorf("no client opcode mapping for character achievements in version %d", clientVersion) } fmt.Printf("Would send character achievements packet to character %d (opcode: %d, size: %d bytes)\n", characterID, clientOpcode, len(packetData)) // In a real implementation: // return am.world.SendPacketToClient(characterID, clientOpcode, packetData) } return nil } // AwardAchievementPoints awards achievement points to a player func (am *AchievementManager) AwardAchievementPoints(characterID int32, points uint32) error { // TODO: Integrate with player character system to award achievement points // This would typically update the player's total achievement points fmt.Printf("Character %d awarded %d achievement points\n", characterID, points) return nil } // ProcessAchievementTrigger processes an achievement trigger for a player func (am *AchievementManager) ProcessAchievementTrigger(characterID int32, triggerType string, value uint32) error { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return fmt.Errorf("player manager not found for character %d", characterID) } // Get all achievements and check if any match the trigger allAchievements := am.masterList.GetAllAchievements() for _, achievement := range allAchievements { // Check requirements to see if any match the trigger for _, requirement := range achievement.Requirements { if requirement.Name == triggerType { // Update progress for this achievement currentProgress := playerMgr.Updates.GetProgress(achievement.AchievementID) newProgress := currentProgress + value // Ensure we don't exceed the requirement if newProgress > requirement.QtyRequired { newProgress = requirement.QtyRequired } // Update the progress err := am.UpdateProgress(characterID, achievement.AchievementID, newProgress) if err != nil { fmt.Printf("Error updating progress for achievement %d: %v\n", achievement.AchievementID, err) } } } } return nil } // GetPlayerAchievementPoints returns total achievement points for a player func (am *AchievementManager) GetPlayerAchievementPoints(characterID int32) uint32 { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return 0 } var totalPoints uint32 completedAchievements := playerMgr.Updates.GetCompletedAchievements() for _, achievementID := range completedAchievements { achievement := am.masterList.GetAchievement(achievementID) if achievement != nil { totalPoints += achievement.PointValue } } return totalPoints } // RefreshPlayerAchievements refreshes a player's achievement data func (am *AchievementManager) RefreshPlayerAchievements(characterID int32) error { // Remove existing player manager am.RemovePlayerManager(characterID) // This will create a new manager and load fresh data am.GetPlayerManager(characterID) return nil } // GetAchievementProgress returns detailed progress information for a player's achievement func (am *AchievementManager) GetAchievementProgress(characterID int32, achievementID uint32) map[string]any { playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return nil } achievement := am.masterList.GetAchievement(achievementID) if achievement == nil { return nil } progress := playerMgr.Updates.GetProgress(achievementID) completed := playerMgr.Updates.IsCompleted(achievementID) completionPercentage := am.GetCompletionPercentage(characterID, achievementID) result := map[string]any{ "achievement_id": achievementID, "title": achievement.Title, "category": achievement.Category, "current_progress": progress, "required_progress": achievement.QtyRequired, "completed": completed, "completion_percentage": completionPercentage, "point_value": achievement.PointValue, } if completed { completedDate := playerMgr.Updates.GetCompletedDate(achievementID) if !completedDate.IsZero() { result["completed_date"] = completedDate.Format(time.RFC3339) } } return result } // sendAchievementUpdateToClient sends achievement update to client with version detection func (am *AchievementManager) sendAchievementUpdateToClient(characterID int32) { // TODO: Get client version from world server client connection // For now, use a common version (1096 is a common EQ2 client version) defaultClientVersion := int32(1096) err := am.SendAchievementUpdateToPlayer(characterID, defaultClientVersion) if err != nil { fmt.Printf("Failed to send achievement update to character %d: %v\n", characterID, err) } } // LoadAndSendInitialAchievements loads and sends all achievements to a newly connected player func (am *AchievementManager) LoadAndSendInitialAchievements(characterID int32, clientVersion int32) error { // Ensure player manager is loaded playerMgr := am.GetPlayerManager(characterID) if playerMgr == nil { return fmt.Errorf("failed to create player manager for character %d", characterID) } // Send master achievement list first err := am.SendCharacterAchievementsToPlayer(characterID, clientVersion) if err != nil { return fmt.Errorf("failed to send character achievements: %w", err) } // Then send current progress err = am.SendAchievementUpdateToPlayer(characterID, clientVersion) if err != nil { return fmt.Errorf("failed to send achievement updates: %w", err) } fmt.Printf("Sent initial achievement data to character %d\n", characterID) return nil } // HandleAchievementTriggerEvent processes achievement triggers from game events func (am *AchievementManager) HandleAchievementTriggerEvent(characterID int32, triggerType string, value uint32) { err := am.ProcessAchievementTrigger(characterID, triggerType, value) if err != nil { fmt.Printf("Error processing achievement trigger for character %d: %v\n", characterID, err) } } // Shutdown gracefully shuts down the achievement manager func (am *AchievementManager) Shutdown() { fmt.Println("Shutting down achievement manager...") am.mutex.Lock() defer am.mutex.Unlock() // Save all player progress before shutdown for characterID, playerMgr := range am.playerManagers { for _, achievementID := range playerMgr.Updates.GetInProgressAchievements() { am.savePlayerProgress(characterID, achievementID, playerMgr) } } // Clear player managers am.playerManagers = make(map[int32]*achievements.PlayerManager) fmt.Println("Achievement manager shutdown complete") }