using System; using System.Collections.Generic; using System.Linq; using log4net; using ACE.Common; using ACE.Common.Extensions; using ACE.Database; using ACE.Database.Models.Shard; using ACE.Entity.Enum; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.WorldObjects; using ACE.Server.Entity; namespace ACE.Server.Managers { public class QuestManager { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// This is almost always a Player /// /// however there are some rare cases of Creatures having quests /// such as 'chickencrossingroad' /// public Creature Creature { get; } public Fellowship Fellowship { get; } private ICollection runtimeQuests { get; set; } = new HashSet(); public string Name { get { if (Creature != null) return Creature.Name; else return $"Fellowship({Fellowship.FellowshipName})"; } } public uint IDtoUseForQuestRegistry { get { if (Creature != null) return Creature.Guid.Full; else return 1; //return Fellowship.FellowshipLeaderGuid; } } public static bool Debug = false; /// /// Constructs a new QuestManager for a Player / Creature /// public QuestManager(Creature creature) { Creature = creature; } /// /// Constructs a new QuestManager for a Fellowship /// public QuestManager(Fellowship fellowship) { Fellowship = fellowship; } /// /// This will return a clone of the quests collection. You should not mutate the results. /// This is mostly used for information/debugging /// /// public ICollection GetQuests() { if (Creature is Player player) return player.Character.GetQuests(player.CharacterDatabaseLock); // Not a player return runtimeQuests; } /// /// Returns TRUE if a player has started a particular quest /// public bool HasQuest(string questFormat) { var questName = GetQuestName(questFormat); var hasQuest = GetQuest(questName) != null; if (Debug) Console.WriteLine($"{Name}.QuestManager.HasQuest({questFormat}): {hasQuest}"); return hasQuest; } public bool HasQuestCompletes(string questName) { if (Debug) Console.WriteLine($"{Name}.QuestManager.HasQuestCompletes({questName})"); if (!questName.Contains("@")) return HasQuest(questName); var pieces = questName.Split('@'); if (pieces.Length != 2) { Console.WriteLine($"{Name}.QuestManager.HasQuestCompletes({questName}): error parsing quest name"); return false; } var name = pieces[0]; if (!Int32.TryParse(pieces[1], out var numCompletes)) { Console.WriteLine($"{Name}.QuestManager.HasQuestCompletes({questName}): unknown quest format"); return HasQuest(questName); } var quest = GetQuest(name); if (quest == null) return false; var success = quest.NumTimesCompleted == numCompletes; // minimum or exact? if (Debug) Console.WriteLine(success); return success; } /// /// Returns an active or completed quest for this player /// public CharacterPropertiesQuestRegistry GetQuest(string questName) { if (Creature is Player player) return player.Character.GetQuest(questName, player.CharacterDatabaseLock); // Not a player return runtimeQuests.FirstOrDefault(q => q.QuestName.Equals(questName, StringComparison.OrdinalIgnoreCase)); } private CharacterPropertiesQuestRegistry GetOrCreateQuest(string questName, out bool questRegistryWasCreated) { if (Creature is Player player) return player.Character.GetOrCreateQuest(questName, player.CharacterDatabaseLock, out questRegistryWasCreated); // Not a player var existing = runtimeQuests.FirstOrDefault(q => q.QuestName.Equals(questName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { existing = new CharacterPropertiesQuestRegistry { QuestName = questName, }; runtimeQuests.Add(existing); questRegistryWasCreated = true; } else questRegistryWasCreated = false; return existing; } /// /// Adds or updates a quest completion to the player's registry /// public void Update(string questFormat) { var questName = GetQuestName(questFormat); var quest = GetOrCreateQuest(questName, out var questRegistryWasCreated); if (questRegistryWasCreated) { quest.LastTimeCompleted = (uint) Time.GetUnixTime(); quest.NumTimesCompleted = 1; // initial add / first solve quest.CharacterId = IDtoUseForQuestRegistry; if (Debug) Console.WriteLine($"{Name}.QuestManager.Update({quest}): added quest"); if (Creature is Player player) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(quest.QuestName); } } else { if (IsMaxSolves(questName)) { if (Debug) Console.WriteLine($"{Name}.QuestManager.Update({quest}): can not update existing quest. IsMaxSolves({questName}) is true."); return; } // update existing quest quest.LastTimeCompleted = (uint)Time.GetUnixTime(); quest.NumTimesCompleted++; if (Debug) Console.WriteLine($"{Name}.QuestManager.Update({quest}): updated quest ({quest.NumTimesCompleted})"); if (Creature is Player player) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(quest.QuestName); } } } /// /// Initialize a quest completion with the provided number to the player's registry /// public void SetQuestCompletions(string questFormat, int questCompletions = 0) { var questName = GetQuestName(questFormat); var maxSolves = GetMaxSolves(questName); var numTimesCompleted = maxSolves > -1 ? Math.Min(questCompletions, maxSolves) : Math.Abs(questCompletions); var quest = GetOrCreateQuest(questName, out var questRegistryWasCreated); if (questRegistryWasCreated) { quest.LastTimeCompleted = (uint) Time.GetUnixTime(); quest.NumTimesCompleted = numTimesCompleted; // initialize the quest to the given completions quest.CharacterId = IDtoUseForQuestRegistry; if (Debug) Console.WriteLine($"{Name}.QuestManager.SetQuestCompletions({questFormat}): initialized quest to {quest.NumTimesCompleted}"); if (Creature is Player player) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(quest.QuestName); } } else { // update existing quest quest.LastTimeCompleted = (uint)Time.GetUnixTime(); quest.NumTimesCompleted = numTimesCompleted; if (Debug) Console.WriteLine($"{Name}.QuestManager.SetQuestCompletions({questFormat}): initialized quest to {quest.NumTimesCompleted}"); if (Creature is Player player) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(quest.QuestName); } } } /// /// Returns TRUE if player can solve this quest now /// public bool CanSolve(string questFormat) { var questName = GetQuestName(questFormat); // verify max solves / quest timer var nextSolveTime = GetNextSolveTime(questName); var canSolve = nextSolveTime == TimeSpan.MinValue; if (Debug) Console.WriteLine($"{Name}.QuestManager.CanSolve({questName}): {canSolve}"); return canSolve; } /// /// Returns TRUE if player has reached the maximum # of solves for this quest /// public bool IsMaxSolves(string questName) { var quest = DatabaseManager.World.GetCachedQuest(questName); if (quest == null) return false; var playerQuest = GetQuest(questName); if (playerQuest == null) return false; // player hasn't completed this quest yet // return TRUE if quest has solve limit, and it has been reached return quest.MaxSolves > -1 && playerQuest.NumTimesCompleted >= quest.MaxSolves; } /// /// Returns the maximum # of solves for this quest /// public int GetMaxSolves(string questFormat) { var questName = GetQuestName(questFormat); var quest = DatabaseManager.World.GetCachedQuest(questName); if (quest == null) return 0; return quest.MaxSolves; } /// /// Returns the current # of solves for this quest /// public int GetCurrentSolves(string questFormat) { var questName = GetQuestName(questFormat); var quest = GetQuest(questName); if (quest == null) return 0; return quest.NumTimesCompleted; } /// /// Some quests we do not want to scale MinDelta if "quest_mindelta_rate" has been set. /// They may be things that are races against time, like Colo /// public static bool CanScaleQuestMinDelta(Database.Models.World.Quest quest) { if (quest.Name.StartsWith("ColoArena")) return false; return true; } /// /// Returns the time remaining until the player can solve this quest again /// public TimeSpan GetNextSolveTime(string questFormat) { var questName = GetQuestName(questFormat); var quest = DatabaseManager.World.GetCachedQuest(questName); if (quest == null) return TimeSpan.MaxValue; // world quest not found - cannot solve it var playerQuest = GetQuest(questName); if (playerQuest == null) return TimeSpan.MinValue; // player hasn't completed this quest yet - can solve immediately if (quest.MaxSolves > -1 && playerQuest.NumTimesCompleted >= quest.MaxSolves) return TimeSpan.MaxValue; // cannot solve this quest again - max solves reached / exceeded var currentTime = (uint)Time.GetUnixTime(); uint nextSolveTime; if (CanScaleQuestMinDelta(quest)) nextSolveTime = playerQuest.LastTimeCompleted + (uint)(quest.MinDelta * PropertyManager.GetDouble("quest_mindelta_rate", 1).Item); else nextSolveTime = playerQuest.LastTimeCompleted + quest.MinDelta; if (currentTime >= nextSolveTime) return TimeSpan.MinValue; // can solve again now - next solve time expired // return the time remaining on the player's quest timer return TimeSpan.FromSeconds(nextSolveTime - currentTime); } /// /// Increment the number of times completed for a quest /// public void Increment(string questName, int amount = 1) { for (var i = 0; i < amount; i++) Update(questName); } /// /// Decrement the number of times completed for a quest /// public void Decrement(string quest, int amount = 1) { var questName = GetQuestName(quest); var existing = GetQuest(questName); if (existing != null) { //if (existing.NumTimesCompleted == 0) //{ // if (Debug) Console.WriteLine($"{Name}.QuestManager.Decrement({quest}): can not Decrement existing quest. {questName}.NumTimesCompleted is already 0."); // return; //} // update existing quest existing.LastTimeCompleted = (uint)Time.GetUnixTime(); existing.NumTimesCompleted -= amount; if (Debug) Console.WriteLine($"{Name}.QuestManager.Decrement({quest}): updated quest ({existing.NumTimesCompleted})"); if (Creature is Player player) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(existing.QuestName); } } } /// /// Removes an existing quest from the Player's registry /// public void Erase(string questFormat) { if (Debug) Console.WriteLine($"{Name}.QuestManager.Erase({questFormat})"); var questName = GetQuestName(questFormat); if (Creature is Player player) { if (player.Character.EraseQuest(questName, player.CharacterDatabaseLock)) { player.CharacterChangesDetected = true; player.ContractManager.NotifyOfQuestUpdate(questName); } } else { // Not a player var quests = runtimeQuests.Where(q => q.QuestName.Equals(questName, StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var quest in quests) runtimeQuests.Remove(quest); } } /// /// Removes an all quests from registry /// public void EraseAll() { if (Debug) Console.WriteLine($"{Name}.QuestManager.EraseAll"); if (Creature is Player player) { player.Character.EraseAllQuests(out var questNamesErased, player.CharacterDatabaseLock); if (questNamesErased.Count > 0) { player.CharacterChangesDetected = true; foreach (var questName in questNamesErased) player.ContractManager.NotifyOfQuestUpdate(questName); } } else { // Not a player runtimeQuests.Clear(); } } /// /// Shows the current quests in progress for a Player /// public void ShowQuests(Player player) { Console.WriteLine("ShowQuests"); var quests = GetQuests(); if (quests.Count == 0) { Console.WriteLine("No quests in progress for " + Name); return; } foreach (var quest in quests) { Console.WriteLine("Quest Name: " + quest.QuestName); Console.WriteLine("Times Completed: " + quest.NumTimesCompleted); Console.WriteLine("Last Time Completed: " + quest.LastTimeCompleted); Console.WriteLine("Player ID: " + quest.CharacterId.ToString("X8")); Console.WriteLine("----"); } } public void Stamp(string questFormat) { var questName = GetQuestName(questFormat); Update(questName); // ?? } /// /// Returns the quest name without the @ comment /// /// A quest name with an optional @comment on the end public static string GetQuestName(string questFormat) { var idx = questFormat.IndexOf('@'); // strip comment if (idx == -1) return questFormat; var questName = questFormat.Substring(0, idx); return questName; } /// /// Returns TRUE if player has solved this quest between min-max times /// public bool HasQuestSolves(string questFormat, int? _min, int? _max) { var questName = GetQuestName(questFormat); // strip optional @comment var quest = GetQuest(questName); var numSolves = quest != null ? quest.NumTimesCompleted : 0; int min = _min ?? int.MinValue; // use defaults? int max = _max ?? int.MaxValue; var hasQuestSolves = numSolves >= min && numSolves <= max; // verify: can either of these be -1? if (Debug) Console.WriteLine($"{Name}.QuestManager.HasQuestSolves({questFormat}, {_min}, {_max}): {hasQuestSolves}"); return hasQuestSolves; } /// /// Called when a player hasn't started a quest yet /// public void HandleNoQuestError(WorldObject wo) { var player = Creature as Player; if (player == null) return; var error = new GameEventInventoryServerSaveFailed(player.Session, wo.Guid.Full, WeenieError.ItemRequiresQuestToBePickedUp); player.Session.Network.EnqueueSend(error); } public void HandlePortalQuestError(string questName) { var player = Creature as Player; if (player == null) return; if (!HasQuest(questName)) { player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.YouMustCompleteQuestToUsePortal)); } else if (CanSolve(questName)) { var error = new GameEventWeenieError(player.Session, WeenieError.QuestSolvedTooLongAgo); var text = new GameMessageSystemChat("You completed the quest this portal requires too long ago!", ChatMessageType.Magic); // This msg wasn't sent in retail PCAP, leading to a completely silent fail when using the portal with an expired flag. player.Session.Network.EnqueueSend(text, error); } } /// /// Called when either the player has completed the quest too recently, or max solves has been reached. /// public void HandleSolveError(string questName) { var player = Creature as Player; if (player == null) return; if (IsMaxSolves(questName)) { var error = new GameEventInventoryServerSaveFailed(player.Session, 0, WeenieError.YouHaveSolvedThisQuestTooManyTimes); var text = new GameMessageSystemChat("You have solved this quest too many times!", ChatMessageType.Broadcast); player.Session.Network.EnqueueSend(text, error); } else { var error = new GameEventInventoryServerSaveFailed(player.Session, 0, WeenieError.YouHaveSolvedThisQuestTooRecently); var text = new GameMessageSystemChat("You have solved this quest too recently!", ChatMessageType.Broadcast); var remainStr = GetNextSolveTime(questName).GetFriendlyString(); var remain = new GameMessageSystemChat($"You may complete this quest again in {remainStr}.", ChatMessageType.Broadcast); player.Session.Network.EnqueueSend(text, remain, error); } } /// /// Increments the counter for a kill task for a player /// public void HandleKillTask(string killQuestName, WorldObject killedCreature) { var player = Creature as Player; if (player == null) return; // http://acpedia.org/wiki/Announcements_-_2012/12_-_A_Growing_Twilight#Release_Notes if (killedCreature == null) { log.ErrorFormat("{0}.QuestManager.HandleKillTask({1}): input object is null!", Name, killQuestName); return; } var questName = GetQuestName(killQuestName); var quest = DatabaseManager.World.GetCachedQuest(questName); if (quest == null) { log.ErrorFormat("{0}.QuestManager.HandleKillTask({1}): couldn't find kill task {2} in database", Name, killQuestName, questName); return; } if (!HasQuest(questName)) return; Stamp(killQuestName); var playerQuest = GetQuest(questName); if (playerQuest == null) { // this should be impossible log.ErrorFormat("{0}.QuestManager.HandleKillTask({1}): couldn't find kill task {2} in player quests", Name, killQuestName, questName); return; } var msg = $"You have killed {playerQuest.NumTimesCompleted} {killedCreature.GetPluralName()}!"; if (IsMaxSolves(questName)) msg += $" Your task is complete!"; else msg += $" You must kill {quest.MaxSolves} to complete your task."; player.Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast)); } /// /// Called when a player kills Creature /// public void OnDeath(WorldObject killer) { } public bool HasQuestBits(string questFormat, int bits) { var questName = GetQuestName(questFormat); var quest = GetQuest(questName); if (quest == null) return false; var hasQuestBits = (quest.NumTimesCompleted & bits) == bits; if (Debug) Console.WriteLine($"{Name}.QuestManager.HasQuestBits({questFormat}, 0x{bits:X}): {hasQuestBits}"); return hasQuestBits; } public bool HasNoQuestBits(string questFormat, int bits) { var questName = GetQuestName(questFormat); var quest = GetQuest(questName); if (quest == null) return true; var hasNoQuestBits = (quest.NumTimesCompleted & bits) == 0; if (Debug) Console.WriteLine($"{Name}.QuestManager.HasNoQuestBits({questFormat}, 0x{bits:X}): {hasNoQuestBits}"); return hasNoQuestBits; } public void SetQuestBits(string questFormat, int bits, bool on = true) { var questName = GetQuestName(questFormat); var quest = GetQuest(questName); var questBits = 0; if (quest != null) questBits = quest.NumTimesCompleted; if (on) questBits |= bits; else questBits &= ~bits; if (Debug) Console.WriteLine($"{Name}.QuestManager.SetQuestBits({questFormat}, 0x{bits:X}): {on}"); SetQuestCompletions(questFormat, questBits); } } }