// Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using Framework.Constants; using Framework.Database; using Game.AI; using Game.Conditions; using Game.DataStorage; using Game.Groups; using Game.Mails; using Game.Maps; using Game.Misc; using Game.Networking.Packets; using Game.Spells; using System; using System.Collections.Generic; using System.Linq; namespace Game.Entities { public partial class Player { public uint GetSharedQuestID() { return m_sharedQuestId; } public ObjectGuid GetPlayerSharingQuest() { return m_playerSharingQuest; } public void SetQuestSharingInfo(ObjectGuid guid, uint id) { m_playerSharingQuest = guid; m_sharedQuestId = id; } public void ClearQuestSharingInfo() { m_playerSharingQuest = ObjectGuid.Empty; m_sharedQuestId = 0; } uint GetInGameTime() { return m_ingametime; } public void SetInGameTime(uint time) { m_ingametime = time; } void AddTimedQuest(uint questId) { m_timedquests.Add(questId); } public void RemoveTimedQuest(uint questId) { m_timedquests.Remove(questId); } public List GetRewardedQuests() { return m_RewardedQuests; } Dictionary GetQuestStatusMap() { return m_QuestStatus; } public int GetQuestMinLevel(Quest quest) { return GetQuestMinLevel(quest.ContentTuningId); } int GetQuestMinLevel(uint contentTuningId) { var questLevels = Global.DB2Mgr.GetContentTuningData(contentTuningId, m_playerData.CtrOptions.GetValue().ConditionalFlags); if (questLevels.HasValue) { ChrRacesRecord race = CliDB.ChrRacesStorage.LookupByKey(GetRace()); FactionTemplateRecord raceFaction = CliDB.FactionTemplateStorage.LookupByKey(race.FactionID); int questFactionGroup = CliDB.ContentTuningStorage.LookupByKey(contentTuningId).GetScalingFactionGroup(); if (questFactionGroup != 0 && raceFaction.FactionGroup != questFactionGroup) return questLevels.Value.MaxLevel; return questLevels.Value.MinLevelWithDelta; } return 0; } public int GetQuestLevel(Quest quest) { if (quest == null) return 0; return GetQuestLevel(quest.ContentTuningId); } public int GetQuestLevel(uint contentTuningId) { var questLevels = Global.DB2Mgr.GetContentTuningData(contentTuningId, m_playerData.CtrOptions.GetValue().ConditionalFlags); if (questLevels.HasValue) { int minLevel = GetQuestMinLevel(contentTuningId); int maxLevel = questLevels.Value.MaxLevel; int level = (int)GetLevel(); if (level >= minLevel) return Math.Min(level, maxLevel); return minLevel; } return 0; } public int GetRewardedQuestCount() { return m_RewardedQuests.Count; } public void LearnQuestRewardedSpells(Quest quest) { //wtf why is rewardspell a uint if it can me -1 int spell_id = Convert.ToInt32(quest.RewardSpell); uint src_spell_id = quest.SourceSpellID; // skip quests without rewarded spell if (spell_id == 0) return; // if RewSpellCast = -1 we remove aura do to SrcSpell from player. if (spell_id == -1 && src_spell_id != 0) { RemoveAurasDueToSpell(src_spell_id); return; } SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)spell_id, Difficulty.None); if (spellInfo == null) return; // check learned spells state bool found = false; foreach (var spellEffectInfo in spellInfo.GetEffects()) { if (spellEffectInfo.IsEffect(SpellEffectName.LearnSpell) && !HasSpell(spellEffectInfo.TriggerSpell)) { found = true; break; } } // skip quests with not teaching spell or already known spell if (!found) return; SpellEffectInfo effect = spellInfo.GetEffect(0); uint learned_0 = effect.TriggerSpell; if (!HasSpell(learned_0)) { found = false; var skills = Global.SpellMgr.GetSkillLineAbilityMapBounds(learned_0); foreach (var skillLine in skills) { if (skillLine.AcquireMethod == AbilityLearnType.RewardedFromQuest) { found = true; break; } } // profession specialization can be re-learned from npc if (!found) return; } CastSpell(this, (uint)spell_id, true); } public void LearnQuestRewardedSpells() { // learn spells received from quest completing foreach (var questId in m_RewardedQuests) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; LearnQuestRewardedSpells(quest); } } public void DailyReset() { foreach (uint questId in m_activePlayerData.DailyQuestsCompleted) { uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, false); } DailyQuestsReset dailyQuestsReset = new(); dailyQuestsReset.Count = m_activePlayerData.DailyQuestsCompleted.Size(); SendPacket(dailyQuestsReset); ClearDynamicUpdateFieldValues(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.DailyQuestsCompleted)); m_DFQuests.Clear(); // Dungeon Finder Quests. for (ushort slot = 0; slot < SharedConst.MaxQuestLogSize; ++slot) { uint questId = GetQuestSlotQuestId(slot); if (questId == 0) continue; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null || !quest.IsDaily() || !quest.HasFlagEx(QuestFlagsEx.RemoveOnPeriodicReset)) continue; SetQuestSlot(slot, 0); AbandonQuest(questId); RemoveActiveQuest(questId); DespawnPersonalSummonsForQuest(questId); if (quest.LimitTime != 0) RemoveTimedQuest(questId); SendPacket(new QuestForceRemoved(questId)); } // DB data deleted in caller m_DailyQuestChanged = false; m_lastDailyQuestTime = 0; if (_garrison != null) _garrison.ResetFollowerActivationLimit(); FailCriteria(CriteriaFailEvent.DailyQuestsCleared, 0); } public void ResetWeeklyQuestStatus() { if (m_weeklyquests.Empty()) return; foreach (uint questId in m_weeklyquests) { uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, false); } for (ushort slot = 0; slot < SharedConst.MaxQuestLogSize; ++slot) { uint questId = GetQuestSlotQuestId(slot); if (questId == 0) continue; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null || !quest.IsWeekly() || !quest.HasFlagEx(QuestFlagsEx.RemoveOnWeeklyReset)) continue; SetQuestSlot(slot, 0); AbandonQuest(questId); RemoveActiveQuest(questId); DespawnPersonalSummonsForQuest(questId); if (quest.LimitTime != 0) RemoveTimedQuest(questId); SendPacket(new QuestForceRemoved(questId)); } m_weeklyquests.Clear(); // DB data deleted in caller m_WeeklyQuestChanged = false; } public void ResetSeasonalQuestStatus(ushort event_id, long eventStartTime) { // DB data deleted in caller m_SeasonalQuestChanged = false; var eventList = m_seasonalquests.LookupByKey(event_id); if (eventList == null) return; foreach (var (questId, completedTime) in eventList.ToList()) { if (completedTime < eventStartTime) { uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, false); eventList.Remove(questId); } } if (eventList.Empty()) m_seasonalquests.Remove(event_id); } public void ResetMonthlyQuestStatus() { if (m_monthlyquests.Empty()) return; foreach (uint questId in m_monthlyquests) { uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, false); } m_monthlyquests.Clear(); // DB data deleted in caller m_MonthlyQuestChanged = false; } public bool CanInteractWithQuestGiver(WorldObject questGiver) { switch (questGiver.GetTypeId()) { case TypeId.Unit: return GetNPCIfCanInteractWith(questGiver.GetGUID(), NPCFlags.QuestGiver, NPCFlags2.None) != null; case TypeId.GameObject: return GetGameObjectIfCanInteractWith(questGiver.GetGUID(), GameObjectTypes.QuestGiver) != null; case TypeId.Player: return IsAlive() && questGiver.ToPlayer().IsAlive(); case TypeId.Item: return IsAlive(); default: break; } return false; } public bool IsQuestRewarded(uint quest_id) { return m_RewardedQuests.Contains(quest_id); } public void PrepareQuestMenu(ObjectGuid guid) { QuestRelationResult questRelations; QuestRelationResult questInvolvedRelations; // pets also can have quests Creature creature = ObjectAccessor.GetCreatureOrPetOrVehicle(this, guid); if (creature != null) { questRelations = Global.ObjectMgr.GetCreatureQuestRelations(creature.GetEntry()); questInvolvedRelations = Global.ObjectMgr.GetCreatureQuestInvolvedRelations(creature.GetEntry()); } else { //we should obtain map from GetMap() in 99% of cases. Special case //only for quests which cast teleport spells on player Map _map = IsInWorld ? GetMap() : Global.MapMgr.FindMap(GetMapId(), GetInstanceId()); Cypher.Assert(_map != null); GameObject gameObject = _map.GetGameObject(guid); if (gameObject != null) { questRelations = Global.ObjectMgr.GetGOQuestRelations(gameObject.GetEntry()); questInvolvedRelations = Global.ObjectMgr.GetGOQuestInvolvedRelations(gameObject.GetEntry()); } else return; } QuestMenu qm = PlayerTalkClass.GetQuestMenu(); qm.ClearMenu(); foreach (var questId in questInvolvedRelations) { QuestStatus status = GetQuestStatus(questId); if (status == QuestStatus.Complete) qm.AddMenuItem(questId, 4); else if (status == QuestStatus.Incomplete) qm.AddMenuItem(questId, 4); } foreach (var questId in questRelations) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; if (!CanTakeQuest(quest, false)) continue; if (quest.IsTurnIn() && (!quest.IsRepeatable() || quest.IsDaily() || quest.IsWeekly() || quest.IsMonthly())) qm.AddMenuItem(questId, 0); else if (quest.IsTurnIn()) qm.AddMenuItem(questId, 4); else if (GetQuestStatus(questId) == QuestStatus.None) qm.AddMenuItem(questId, 2); } } public void SendPreparedQuest(WorldObject source) { QuestMenu questMenu = PlayerTalkClass.GetQuestMenu(); if (questMenu.IsEmpty()) return; // single element case if (questMenu.GetMenuItemCount() == 1) { QuestMenuItem qmi0 = questMenu.GetItem(0); uint questId = qmi0.QuestId; // Auto open Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) { if (qmi0.QuestIcon == 4) PlayerTalkClass.SendQuestGiverRequestItems(quest, source.GetGUID(), CanRewardQuest(quest, false), true); // Send completable on repeatable and autoCompletable quest if player don't have quest // @todo verify if check for !quest.IsDaily() is really correct (possibly not) else if (!source.HasQuest(questId) && !source.HasInvolvedQuest(questId)) PlayerTalkClass.SendCloseGossip(); else { if (quest.IsAutoAccept() && CanAddQuest(quest, true) && CanTakeQuest(quest, true)) AddQuestAndCheckCompletion(quest, source); if (quest.IsTurnIn() && quest.IsRepeatable() && !quest.IsDailyOrWeekly() && !quest.IsMonthly()) PlayerTalkClass.SendQuestGiverRequestItems(quest, source.GetGUID(), CanCompleteRepeatableQuest(quest), true); else if (quest.IsTurnIn() && !quest.IsDailyOrWeekly() && !quest.IsMonthly()) PlayerTalkClass.SendQuestGiverRequestItems(quest, source.GetGUID(), CanRewardQuest(quest, false), true); else PlayerTalkClass.SendQuestGiverQuestDetails(quest, source.GetGUID(), true, false); } return; } } PlayerTalkClass.SendQuestGiverQuestListMessage(source); } public bool IsActiveQuest(uint quest_id) { return m_QuestStatus.ContainsKey(quest_id); } public Quest GetNextQuest(WorldObject questGiver, Quest quest) { uint nextQuestID = quest.NextQuestInChain; if (nextQuestID == 0) return null; if (questGiver == this) { if (!quest.HasFlag(QuestFlags.AutoComplete)) return null; return Global.ObjectMgr.GetQuestTemplate(nextQuestID); } //we should obtain map pointer from GetMap() in 99% of cases. Special case //only for quests which cast teleport spells on player if (!IsInMap(questGiver)) return null; if (!questGiver.HasQuest(nextQuestID)) return null; return Global.ObjectMgr.GetQuestTemplate(nextQuestID); } public bool CanSeeStartQuest(Quest quest) { if (!Global.DisableMgr.IsDisabledFor(DisableType.Quest, quest.Id, this) && SatisfyQuestClass(quest, false) && SatisfyQuestRace(quest, false) && SatisfyQuestSkill(quest, false) && SatisfyQuestExclusiveGroup(quest, false) && SatisfyQuestReputation(quest, false) && SatisfyQuestDependentQuests(quest, false) && SatisfyQuestDay(quest, false) && SatisfyQuestWeek(quest, false) && SatisfyQuestMonth(quest, false) && SatisfyQuestSeasonal(quest, false)) { return GetLevel() + WorldConfig.GetIntValue(WorldCfg.QuestHighLevelHideDiff) >= GetQuestMinLevel(quest); } return false; } public bool CanTakeQuest(Quest quest, bool msg) { return !Global.DisableMgr.IsDisabledFor(DisableType.Quest, quest.Id, this) && SatisfyQuestStatus(quest, msg) && SatisfyQuestExclusiveGroup(quest, msg) && SatisfyQuestClass(quest, msg) && SatisfyQuestRace(quest, msg) && SatisfyQuestLevel(quest, msg) && SatisfyQuestSkill(quest, msg) && SatisfyQuestReputation(quest, msg) && SatisfyQuestDependentQuests(quest, msg) && SatisfyQuestTimed(quest, msg) && SatisfyQuestDay(quest, msg) && SatisfyQuestWeek(quest, msg) && SatisfyQuestMonth(quest, msg) && SatisfyQuestSeasonal(quest, msg) && SatisfyQuestConditions(quest, msg); } public bool CanAddQuest(Quest quest, bool msg) { if (!SatisfyQuestLog(msg)) return false; uint srcitem = quest.SourceItemId; if (srcitem > 0) { uint count = quest.SourceItemIdCount; List dest = new(); InventoryResult msg2 = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, srcitem, count); // player already have max number (in most case 1) source item, no additional item needed and quest can be added. if (msg2 == InventoryResult.ItemMaxCount) return true; if (msg2 != InventoryResult.Ok) { SendEquipError(msg2, null, null, srcitem); return false; } } return true; } public bool CanCompleteQuest(uint questId, uint ignoredQuestObjectiveId = 0) { if (questId != 0) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(questId); if (qInfo == null) return false; if (!qInfo.IsRepeatable() && GetQuestRewardStatus(questId)) return false; // not allow re-complete quest // auto complete quest if (qInfo.IsTurnIn() && CanTakeQuest(qInfo, false)) return true; var q_status = m_QuestStatus.LookupByKey(questId); if (q_status == null) return false; if (q_status.Status == QuestStatus.Incomplete) { foreach (QuestObjective obj in qInfo.Objectives) { if (ignoredQuestObjectiveId != 0 && obj.Id == ignoredQuestObjectiveId) continue; if (!obj.Flags.HasAnyFlag(QuestObjectiveFlags.Optional) && !obj.Flags.HasAnyFlag(QuestObjectiveFlags.PartOfProgressBar)) { if (!IsQuestObjectiveComplete(q_status.Slot, qInfo, obj)) return false; } } if ((qInfo.HasFlag(QuestFlags.CompletionEvent) || qInfo.HasFlag(QuestFlags.CompletionAreaTrigger)) && !q_status.Explored) return false; if (qInfo.LimitTime != 0 && q_status.Timer == 0) return false; return true; } } return false; } public bool CanCompleteRepeatableQuest(Quest quest) { // Solve problem that player don't have the quest and try complete it. // if repeatable she must be able to complete event if player don't have it. // Seem that all repeatable quest are DELIVER Flag so, no need to add more. if (!CanTakeQuest(quest, false)) return false; if (!CanRewardQuest(quest, false)) return false; return true; } public bool CanRewardQuest(Quest quest, bool msg) { // quest is disabled if (Global.DisableMgr.IsDisabledFor(DisableType.Quest, quest.Id, this)) return false; // not auto complete quest and not completed quest (only cheating case, then ignore without message) if (!quest.IsDFQuest() && !quest.IsTurnIn() && GetQuestStatus(quest.Id) != QuestStatus.Complete) return false; // daily quest can't be rewarded (25 daily quest already completed) if (!SatisfyQuestDay(quest, msg) || !SatisfyQuestWeek(quest, msg) || !SatisfyQuestMonth(quest, msg) || !SatisfyQuestSeasonal(quest, msg)) return false; // player no longer satisfies the quest's requirements (skill level etc.) if (!SatisfyQuestLevel(quest, msg) || !SatisfyQuestSkill(quest, msg) || !SatisfyQuestReputation(quest, msg)) return false; // rewarded and not repeatable quest (only cheating case, then ignore without message) if (GetQuestRewardStatus(quest.Id)) return false; // prevent receive reward with quest items in bank if (quest.HasQuestObjectiveType(QuestObjectiveType.Item)) { foreach (QuestObjective obj in quest.Objectives) { if (obj.Type != QuestObjectiveType.Item || obj.Flags2.HasAnyFlag(QuestObjectiveFlags2.QuestBoundItem)) continue; if (GetItemCount((uint)obj.ObjectID) < obj.Amount) { if (msg) SendEquipError(InventoryResult.ItemNotFound, null, null, (uint)obj.ObjectID); return false; } } } foreach (QuestObjective obj in quest.Objectives) { switch (obj.Type) { case QuestObjectiveType.Currency: if (!HasCurrency((uint)obj.ObjectID, (uint)obj.Amount)) return false; break; case QuestObjectiveType.Money: if (!HasEnoughMoney(obj.Amount)) return false; break; } } return true; } public bool CanRewardQuest(Quest quest, LootItemType rewardType, uint rewardId, bool msg) { List dest = new(); if (quest.GetRewChoiceItemsCount() > 0) { switch (rewardType) { case LootItemType.Item: for (uint i = 0; i < SharedConst.QuestRewardChoicesCount; ++i) { if (quest.RewardChoiceItemId[i] != 0 && quest.RewardChoiceItemType[i] == LootItemType.Item && quest.RewardChoiceItemId[i] == rewardId) { InventoryResult res = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, quest.RewardChoiceItemId[i], quest.RewardChoiceItemCount[i]); if (res != InventoryResult.Ok) { if (msg) SendQuestFailed(quest.Id, res); return false; } } } break; case LootItemType.Currency: break; } } if (quest.GetRewItemsCount() > 0) { for (uint i = 0; i < quest.GetRewItemsCount(); ++i) { if (quest.RewardItemId[i] != 0) { InventoryResult res = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, quest.RewardItemId[i], quest.RewardItemCount[i]); if (res != InventoryResult.Ok) { if (msg) SendQuestFailed(quest.Id, res); return false; } } } } // QuestPackageItem.db2 if (quest.PackageID != 0) { bool hasFilteredQuestPackageReward = false; var questPackageItems = Global.DB2Mgr.GetQuestPackageItems(quest.PackageID); if (questPackageItems != null) { foreach (var questPackageItem in questPackageItems) { if (questPackageItem.ItemID != rewardId) continue; if (CanSelectQuestPackageItem(questPackageItem)) { hasFilteredQuestPackageReward = true; InventoryResult res = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, questPackageItem.ItemID, questPackageItem.ItemQuantity); if (res != InventoryResult.Ok) { SendEquipError(res, null, null, questPackageItem.ItemID); return false; } } } } if (!hasFilteredQuestPackageReward) { List questPackageItems1 = Global.DB2Mgr.GetQuestPackageItemsFallback(quest.PackageID); if (questPackageItems1 != null) { foreach (QuestPackageItemRecord questPackageItem in questPackageItems1) { if (questPackageItem.ItemID != rewardId) continue; InventoryResult res = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, questPackageItem.ItemID, questPackageItem.ItemQuantity); if (res != InventoryResult.Ok) { SendEquipError(res, null, null, questPackageItem.ItemID); return false; } } } } } return true; } public void AddQuestAndCheckCompletion(Quest quest, WorldObject questGiver) { AddQuest(quest, questGiver); if (CanCompleteQuest(quest.Id)) CompleteQuest(quest.Id); if (questGiver == null) return; switch (questGiver.GetTypeId()) { case TypeId.Unit: PlayerTalkClass.ClearMenus(); questGiver.ToCreature().GetAI().OnQuestAccept(this, quest); break; case TypeId.Item: case TypeId.Container: case TypeId.AzeriteItem: case TypeId.AzeriteEmpoweredItem: { Item item = (Item)questGiver; Global.ScriptMgr.OnQuestAccept(this, item, quest); // There are two cases where the source item is not destroyed when the quest is accepted: // - It is required to finish the quest, and is an unique item // - It is the same item present in the source item field (item that would be given on quest accept) bool destroyItem = true; foreach (QuestObjective obj in quest.Objectives) { if (obj.Type == QuestObjectiveType.Item && obj.ObjectID == item.GetEntry() && item.GetTemplate().GetMaxCount() > 0) { destroyItem = false; break; } } if (quest.SourceItemId == item.GetEntry()) destroyItem = false; if (destroyItem) DestroyItem(item.GetBagSlot(), item.GetSlot(), true); break; } case TypeId.GameObject: PlayerTalkClass.ClearMenus(); questGiver.ToGameObject().GetAI().OnQuestAccept(this, quest); break; default: break; } } public void AddQuest(Quest quest, WorldObject questGiver) { ushort logSlot = FindQuestSlot(0); if (logSlot >= SharedConst.MaxQuestLogSize) // Player does not have any free slot in the quest log return; uint questId = quest.Id; // if not exist then created with set uState == NEW and rewarded=false if (!m_QuestStatus.ContainsKey(questId)) m_QuestStatus[questId] = new QuestStatusData(); QuestStatusData questStatusData = m_QuestStatus.LookupByKey(questId); QuestStatus oldStatus = questStatusData.Status; // check for repeatable quests status reset SetQuestSlot(logSlot, questId); questStatusData.Slot = logSlot; questStatusData.Status = QuestStatus.Incomplete; questStatusData.Explored = false; foreach (QuestObjective obj in quest.Objectives) { m_questObjectiveStatus.Add((obj.Type, obj.ObjectID), new QuestObjectiveStatusData() { QuestStatusPair = (questId, questStatusData), ObjectiveId = obj.Id }); switch (obj.Type) { case QuestObjectiveType.MinReputation: case QuestObjectiveType.MaxReputation: FactionRecord factionEntry = CliDB.FactionStorage.LookupByKey(obj.ObjectID); if (factionEntry != null) GetReputationMgr().SetVisible(factionEntry); break; case QuestObjectiveType.CriteriaTree: m_questObjectiveCriteriaMgr.ResetCriteriaTree((uint)obj.ObjectID); break; default: break; } } GiveQuestSourceItem(quest); AdjustQuestObjectiveProgress(quest); long endTime = 0; uint limittime = (uint)quest.LimitTime; if (limittime != 0) { // shared timed quest if (questGiver != null && questGiver.IsTypeId(TypeId.Player)) limittime = questGiver.ToPlayer().m_QuestStatus[questId].Timer / Time.InMilliseconds; AddTimedQuest(questId); questStatusData.Timer = limittime * Time.InMilliseconds; endTime = GameTime.GetGameTime() + limittime; } else questStatusData.Timer = 0; if (quest.HasFlag(QuestFlags.Pvp)) { pvpInfo.IsHostile = true; UpdatePvPState(); } if (quest.SourceSpellID > 0) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(quest.SourceSpellID, GetMap().GetDifficultyID()); Unit caster = this; if (questGiver != null && questGiver.IsUnit() && !quest.HasFlag(QuestFlags.PlayerCastAccept) && !spellInfo.HasTargetType(Targets.UnitCaster) && !spellInfo.HasTargetType(Targets.DestCasterSummon)) caster = questGiver.ToUnit(); caster.CastSpell(this, spellInfo.Id, new CastSpellExtraArgs(TriggerCastFlags.FullMask).SetCastDifficulty(spellInfo.Difficulty)); } SetQuestSlotEndTime(logSlot, endTime); questStatusData.AcceptTime = GameTime.GetGameTime(); m_QuestStatusSave[questId] = QuestSaveType.Default; StartCriteria(CriteriaStartEvent.AcceptQuest, questId); SendQuestUpdate(questId); bool updateVisibility = false; if (quest.HasFlag(QuestFlags.UpdatePhaseshift)) updateVisibility = PhasingHandler.OnConditionChange(this, false); if (updateVisibility) UpdateObjectVisibility(); Global.ScriptMgr.OnQuestStatusChange(this, questId); Global.ScriptMgr.OnQuestStatusChange(this, quest, oldStatus, questStatusData.Status); } public void CompleteQuest(uint quest_id) { if (quest_id != 0) { SetQuestStatus(quest_id, QuestStatus.Complete); QuestStatusData questStatus = m_QuestStatus.LookupByKey(quest_id); if (questStatus != null) SetQuestSlotState(questStatus.Slot, QuestSlotStateMask.Complete); Quest qInfo = Global.ObjectMgr.GetQuestTemplate(quest_id); if (qInfo != null) if (qInfo.HasFlag(QuestFlags.TrackingEvent)) RewardQuest(qInfo, LootItemType.Item, 0, this, false); } } public void IncompleteQuest(uint quest_id) { if (quest_id != 0) { SetQuestStatus(quest_id, QuestStatus.Incomplete); ushort log_slot = FindQuestSlot(quest_id); if (log_slot < SharedConst.MaxQuestLogSize) RemoveQuestSlotState(log_slot, QuestSlotStateMask.Complete); } } public uint GetQuestMoneyReward(Quest quest) { return (uint)(quest.MoneyValue(this) * WorldConfig.GetFloatValue(WorldCfg.RateMoneyQuest)); } public uint GetQuestXPReward(Quest quest) { bool rewarded = IsQuestRewarded(quest.Id) && !quest.IsDFQuest(); // Not give XP in case already completed once repeatable quest if (rewarded) return 0; uint XP = (uint)(quest.XPValue(this) * WorldConfig.GetFloatValue(WorldCfg.RateXpQuest)); // handle SPELL_AURA_MOD_XP_QUEST_PCT auras var ModXPPctAuras = GetAuraEffectsByType(AuraType.ModXpQuestPct); foreach (var eff in ModXPPctAuras) MathFunctions.AddPct(ref XP, eff.GetAmount()); return XP; } public bool CanSelectQuestPackageItem(QuestPackageItemRecord questPackageItem) { ItemTemplate rewardProto = Global.ObjectMgr.GetItemTemplate(questPackageItem.ItemID); if (rewardProto == null) return false; if ((rewardProto.HasFlag(ItemFlags2.FactionAlliance) && GetTeam() != Team.Alliance) || (rewardProto.HasFlag(ItemFlags2.FactionHorde) && GetTeam() != Team.Horde)) return false; switch (questPackageItem.DisplayType) { case QuestPackageFilter.LootSpecialization: return rewardProto.IsUsableByLootSpecialization(this, true); case QuestPackageFilter.Class: return rewardProto.ItemSpecClassMask == 0 || (rewardProto.ItemSpecClassMask & GetClassMask()) != 0; case QuestPackageFilter.Everyone: return true; default: break; } return false; } public void RewardQuestPackage(uint questPackageId, ItemContext context, uint onlyItemId = 0) { bool hasFilteredQuestPackageReward = false; var questPackageItems = Global.DB2Mgr.GetQuestPackageItems(questPackageId); if (questPackageItems != null) { foreach (QuestPackageItemRecord questPackageItem in questPackageItems) { if (onlyItemId != 0 && questPackageItem.ItemID != onlyItemId) continue; if (CanSelectQuestPackageItem(questPackageItem)) { hasFilteredQuestPackageReward = true; List dest = new(); if (CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, questPackageItem.ItemID, questPackageItem.ItemQuantity) == InventoryResult.Ok) { Item item = StoreNewItem(dest, questPackageItem.ItemID, true, ItemEnchantmentManager.GenerateItemRandomBonusListId(questPackageItem.ItemID), null, context); SendNewItem(item, questPackageItem.ItemQuantity, true, false); } } } } if (!hasFilteredQuestPackageReward) { var questPackageItemsFallback = Global.DB2Mgr.GetQuestPackageItemsFallback(questPackageId); if (questPackageItemsFallback != null) { foreach (QuestPackageItemRecord questPackageItem in questPackageItemsFallback) { if (onlyItemId != 0 && questPackageItem.ItemID != onlyItemId) continue; List dest = new(); if (CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, questPackageItem.ItemID, questPackageItem.ItemQuantity) == InventoryResult.Ok) { Item item = StoreNewItem(dest, questPackageItem.ItemID, true, ItemEnchantmentManager.GenerateItemRandomBonusListId(questPackageItem.ItemID), null, context); SendNewItem(item, questPackageItem.ItemQuantity, true, false); } } } } } public void RewardQuest(Quest quest, LootItemType rewardType, uint rewardId, WorldObject questGiver, bool announce = true) { //this THING should be here to protect code from quest, which cast on player far teleport as a reward //should work fine, cause far teleport will be executed in Update() SetCanDelayTeleport(true); uint questId = quest.Id; QuestStatus oldStatus = GetQuestStatus(questId); foreach (QuestObjective obj in quest.Objectives) { switch (obj.Type) { case QuestObjectiveType.Item: { int amountToDestroy = obj.Amount; if (quest.HasFlag(QuestFlags.RemoveSurplusItems)) amountToDestroy = int.MaxValue; DestroyItemCount((uint)obj.ObjectID, (uint)amountToDestroy, true); break; } case QuestObjectiveType.Currency: RemoveCurrency((uint)obj.ObjectID, obj.Amount, CurrencyDestroyReason.QuestTurnin); break; } } if (!quest.FlagsEx.HasAnyFlag(QuestFlagsEx.NoItemRemoval)) { for (byte i = 0; i < SharedConst.QuestItemDropCount; ++i) { if (quest.ItemDrop[i] != 0) { uint count = quest.ItemDropQuantity[i]; if (count == 0) count = uint.MaxValue; DestroyItemCount(quest.ItemDrop[i], count, true); } } } RemoveTimedQuest(questId); if (quest.GetRewItemsCount() > 0) { for (uint i = 0; i < quest.GetRewItemsCount(); ++i) { uint itemId = quest.RewardItemId[i]; if (itemId != 0) { List dest = new(); if (CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, itemId, quest.RewardItemCount[i]) == InventoryResult.Ok) { Item item = StoreNewItem(dest, itemId, true, ItemEnchantmentManager.GenerateItemRandomBonusListId(itemId), null, ItemContext.QuestReward); SendNewItem(item, quest.RewardItemCount[i], true, false); } else if (quest.IsDFQuest()) SendItemRetrievalMail(itemId, quest.RewardItemCount[i], ItemContext.QuestReward); } } } CurrencyGainSource currencyGainSource = CurrencyGainSource.QuestReward; if (quest.HasFlagEx(QuestFlagsEx.RewardsIgnoreCaps)) { if (quest.IsWorldQuest()) currencyGainSource = CurrencyGainSource.WorldQuestRewardIgnoreCaps; else currencyGainSource = CurrencyGainSource.QuestRewardIgnoreCaps; } else if (quest.IsDaily()) currencyGainSource = CurrencyGainSource.DailyQuestReward; else if (quest.IsWeekly()) currencyGainSource = CurrencyGainSource.WeeklyQuestReward; else if (quest.IsWorldQuest()) currencyGainSource = CurrencyGainSource.WorldQuestReward; switch (rewardType) { case LootItemType.Item: ItemTemplate rewardProto = Global.ObjectMgr.GetItemTemplate(rewardId); if (rewardProto != null && quest.GetRewChoiceItemsCount() != 0) { for (uint i = 0; i < SharedConst.QuestRewardChoicesCount; ++i) { if (quest.RewardChoiceItemId[i] != 0 && quest.RewardChoiceItemType[i] == LootItemType.Item && quest.RewardChoiceItemId[i] == rewardId) { List dest = new(); if (CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, rewardId, quest.RewardChoiceItemCount[i]) == InventoryResult.Ok) { Item item = StoreNewItem(dest, rewardId, true, ItemEnchantmentManager.GenerateItemRandomBonusListId(rewardId), null, ItemContext.QuestReward); SendNewItem(item, quest.RewardChoiceItemCount[i], true, false); } } } } // QuestPackageItem.db2 if (rewardProto != null && quest.PackageID != 0) RewardQuestPackage(quest.PackageID, ItemContext.QuestReward, rewardId); break; case LootItemType.Currency: if (CliDB.CurrencyTypesStorage.HasRecord(rewardId) && quest.GetRewChoiceItemsCount() != 0) { for (uint i = 0; i < SharedConst.QuestRewardChoicesCount; ++i) if (quest.RewardChoiceItemId[i] != 0 && quest.RewardChoiceItemType[i] == LootItemType.Currency && quest.RewardChoiceItemId[i] == rewardId) AddCurrency(quest.RewardChoiceItemId[i], quest.RewardChoiceItemCount[i], currencyGainSource); } break; } for (byte i = 0; i < SharedConst.QuestRewardCurrencyCount; ++i) { if (quest.RewardCurrencyId[i] != 0) AddCurrency(quest.RewardCurrencyId[i], quest.RewardCurrencyCount[i], currencyGainSource); } uint skill = quest.RewardSkillId; if (skill != 0) UpdateSkillPro(skill, 1000, quest.RewardSkillPoints); ushort log_slot = FindQuestSlot(questId); if (log_slot < SharedConst.MaxQuestLogSize) SetQuestSlot(log_slot, 0); uint XP = GetQuestXPReward(quest); int moneyRew = 0; if (!IsMaxLevel()) GiveXP(XP, null); else moneyRew = (int)(quest.GetRewMoneyMaxLevel() * WorldConfig.GetFloatValue(WorldCfg.RateDropMoney)); moneyRew += (int)GetQuestMoneyReward(quest); if (moneyRew != 0) { ModifyMoney(moneyRew); if (moneyRew > 0) UpdateCriteria(CriteriaType.MoneyEarnedFromQuesting, (uint)moneyRew); SendDisplayToast(0, DisplayToastType.Money, false, (uint)moneyRew, DisplayToastMethod.QuestComplete, questId); } // honor reward uint honor = quest.CalculateHonorGain(GetLevel()); if (honor != 0) RewardHonor(null, 0, (int)honor); // title reward if (quest.RewardTitleId != 0) { CharTitlesRecord titleEntry = CliDB.CharTitlesStorage.LookupByKey(quest.RewardTitleId); if (titleEntry != null) SetTitle(titleEntry); } // Send reward mail uint mail_template_id = quest.RewardMailTemplateId; if (mail_template_id != 0) { SQLTransaction trans = new(); // @todo Poor design of mail system uint questMailSender = quest.RewardMailSenderEntry; if (questMailSender != 0) new MailDraft(mail_template_id).SendMailTo(trans, this, new MailSender(questMailSender), MailCheckMask.HasBody, quest.RewardMailDelay); else new MailDraft(mail_template_id).SendMailTo(trans, this, new MailSender(questGiver), MailCheckMask.HasBody, quest.RewardMailDelay); DB.Characters.CommitTransaction(trans); } if (quest.IsDaily() || quest.IsDFQuest()) { SetDailyQuestStatus(questId); if (quest.IsDaily()) { StartCriteria(CriteriaStartEvent.CompleteDailyQuest, 0); UpdateCriteria(CriteriaType.CompleteDailyQuest, questId); UpdateCriteria(CriteriaType.CompleteAnyDailyQuestPerDay, questId); } } else if (quest.IsWeekly()) SetWeeklyQuestStatus(questId); else if (quest.IsMonthly()) SetMonthlyQuestStatus(questId); else if (quest.IsSeasonal()) SetSeasonalQuestStatus(questId); RemoveActiveQuest(questId, false); if (quest.CanIncreaseRewardedQuestCounters()) SetRewardedQuest(questId); SendQuestReward(quest, questGiver?.ToCreature(), XP, !announce); RewardReputation(quest); // cast spells after mark quest complete (some spells have quest completed state requirements in spell_area data) if (quest.RewardSpell > 0) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(quest.RewardSpell, GetMap().GetDifficultyID()); Unit caster = this; if (questGiver != null && questGiver.IsUnit() && !quest.HasFlag(QuestFlags.PlayerCastComplete) && !spellInfo.HasTargetType(Targets.UnitCaster)) caster = questGiver.ToUnit(); caster.CastSpell(this, spellInfo.Id, new CastSpellExtraArgs(TriggerCastFlags.FullMask).SetCastDifficulty(spellInfo.Difficulty)); } else { foreach (QuestRewardDisplaySpell displaySpell in quest.RewardDisplaySpell) { if (!ConditionManager.IsPlayerMeetingCondition(this, displaySpell.PlayerConditionId)) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(displaySpell.SpellId, GetMap().GetDifficultyID()); Unit caster = this; if (questGiver != null && questGiver.IsUnit() && !quest.HasFlag(QuestFlags.PlayerCastComplete) && !spellInfo.HasTargetType(Targets.UnitCaster)) caster = questGiver.ToUnit(); caster.CastSpell(this, spellInfo.Id, new CastSpellExtraArgs(TriggerCastFlags.FullMask).SetCastDifficulty(spellInfo.Difficulty)); } } if (quest.QuestSortID > 0) UpdateCriteria(CriteriaType.CompleteQuestsInZone, quest.Id); UpdateCriteria(CriteriaType.CompleteQuestsCount); UpdateCriteria(CriteriaType.CompleteQuest, quest.Id); UpdateCriteria(CriteriaType.CompleteAnyReplayQuest, 1); // make full db save SaveToDB(false); uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, true); if (quest.HasFlag(QuestFlags.Pvp)) { pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); UpdatePvPState(); } SendQuestUpdate(questId, true, true); bool updateVisibility = false; if (quest.HasFlag(QuestFlags.UpdatePhaseshift)) updateVisibility = PhasingHandler.OnConditionChange(this, false); //lets remove flag for delayed teleports SetCanDelayTeleport(false); if (questGiver != null && questGiver.IsWorldObject()) { //For AutoSubmition was added plr case there as it almost same exclute AI script cases. // Send next quest Quest nextQuest = GetNextQuest(questGiver, quest); if (nextQuest != null) { // Only send the quest to the player if the conditions are met if (CanTakeQuest(nextQuest, false)) { if (nextQuest.IsAutoAccept() && CanAddQuest(nextQuest, true)) AddQuestAndCheckCompletion(nextQuest, questGiver); PlayerTalkClass.SendQuestGiverQuestDetails(nextQuest, questGiver.GetGUID(), true, false); } } PlayerTalkClass.ClearMenus(); questGiver.ToCreature()?.GetAI().OnQuestReward(this, quest, rewardType, rewardId); questGiver.ToGameObject()?.GetAI().OnQuestReward(this, quest, rewardType, rewardId); } Global.ScriptMgr.OnQuestStatusChange(this, questId); Global.ScriptMgr.OnQuestStatusChange(this, quest, oldStatus, QuestStatus.Rewarded); if (updateVisibility) UpdateObjectVisibility(); } public void SetRewardedQuest(uint questId) { m_RewardedQuests.Add(questId); m_RewardedQuestsSave[questId] = QuestSaveType.Default; uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, true); } public void FailQuest(uint questId) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) { QuestStatus qStatus = GetQuestStatus(questId); // we can only fail incomplete quest or... if (qStatus != QuestStatus.Incomplete) { // completed timed quest with no requirements if (qStatus != QuestStatus.Complete || quest.LimitTime == 0 || !quest.Objectives.Empty()) return; } SetQuestStatus(questId, QuestStatus.Failed); ushort log_slot = FindQuestSlot(questId); if (log_slot < SharedConst.MaxQuestLogSize) SetQuestSlotState(log_slot, QuestSlotStateMask.Fail); if (quest.LimitTime != 0) { QuestStatusData q_status = m_QuestStatus[questId]; RemoveTimedQuest(questId); q_status.Timer = 0; SendQuestTimerFailed(questId); } else SendQuestFailed(questId); // Destroy quest items on quest failure. foreach (QuestObjective obj in quest.Objectives) { if (obj.Type == QuestObjectiveType.Item) { ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate((uint)obj.ObjectID); if (itemTemplate != null) if (itemTemplate.GetBonding() == ItemBondingType.Quest) DestroyItemCount((uint)obj.ObjectID, (uint)obj.Amount, true, true); } } // Destroy items received during the quest. for (byte i = 0; i < SharedConst.QuestItemDropCount; ++i) { ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(quest.ItemDrop[i]); if (itemTemplate != null) if (quest.ItemDropQuantity[i] != 0 && itemTemplate.GetBonding() == ItemBondingType.Quest) DestroyItemCount(quest.ItemDrop[i], quest.ItemDropQuantity[i], true, true); } } } public void FailQuestsWithFlag(QuestFlags flag) { for (ushort slot = 0; slot < SharedConst.MaxQuestLogSize; ++slot) { uint questId = GetQuestSlotQuestId(slot); if (questId == 0) continue; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) if (quest.HasFlag(flag)) FailQuest(questId); } } public void AbandonQuest(uint questId) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) { // Destroy quest items on quest abandon. foreach (QuestObjective obj in quest.Objectives) { if (obj.Type == QuestObjectiveType.Item) { ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate((uint)obj.ObjectID); if (itemTemplate != null) if (itemTemplate.GetBonding() == ItemBondingType.Quest) DestroyItemCount((uint)obj.ObjectID, (uint)obj.Amount, true, true); } } // Destroy items received during the quest. for (byte i = 0; i < SharedConst.QuestItemDropCount; ++i) { ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(quest.ItemDrop[i]); if (itemTemplate != null) if (quest.ItemDropQuantity[i] != 0 && itemTemplate.GetBonding() == ItemBondingType.Quest) DestroyItemCount(quest.ItemDrop[i], quest.ItemDropQuantity[i], true, true); } } } public bool SatisfyQuestSkill(Quest qInfo, bool msg) { uint skill = qInfo.RequiredSkillId; // skip 0 case RequiredSkill if (skill == 0) return true; // check skill value if (GetSkillValue((SkillType)skill) < qInfo.RequiredSkillPoints) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestSkill: Sent QuestFailedReason.None (questId: {0}) because player does not have required skill value.", qInfo.Id); } return false; } return true; } bool SatisfyQuestLevel(Quest qInfo, bool msg) { return SatisfyQuestMinLevel(qInfo, msg) && SatisfyQuestMaxLevel(qInfo, msg); } public bool SatisfyQuestMinLevel(Quest qInfo, bool msg) { if (GetLevel() < GetQuestMinLevel(qInfo)) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.FailedLowLevel); Log.outDebug(LogFilter.Server, "SatisfyQuestMinLevel: Sent QuestFailedReasons.FailedLowLevel (questId: {0}) because player does not have required (min) level.", qInfo.Id); } return false; } return true; } public bool SatisfyQuestMaxLevel(Quest qInfo, bool msg) { if (qInfo.MaxLevel > 0 && GetLevel() > qInfo.MaxLevel) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); // There doesn't seem to be a specific response for too high player level Log.outDebug(LogFilter.Server, "SatisfyQuestMaxLevel: Sent QuestFailedReasons.None (questId: {0}) because player does not have required (max) level.", qInfo.Id); } return false; } return true; } public bool SatisfyQuestLog(bool msg) { // exist free slot if (FindQuestSlot(0) < SharedConst.MaxQuestLogSize) return true; if (msg) SendPacket(new QuestLogFull()); return false; } public bool SatisfyQuestDependentQuests(Quest qInfo, bool msg) { return SatisfyQuestPreviousQuest(qInfo, msg) && SatisfyQuestDependentPreviousQuests(qInfo, msg) && SatisfyQuestBreadcrumbQuest(qInfo, msg) && SatisfyQuestDependentBreadcrumbQuests(qInfo, msg); } public bool SatisfyQuestPreviousQuest(Quest qInfo, bool msg) { // No previous quest (might be first quest in a series) if (qInfo.PrevQuestId == 0) return true; uint prevId = (uint)Math.Abs(qInfo.PrevQuestId); // If positive previous quest rewarded, return true if (qInfo.PrevQuestId > 0 && m_RewardedQuests.Contains(prevId)) return true; // If negative previous quest active, return true if (qInfo.PrevQuestId < 0 && GetQuestStatus(prevId) == QuestStatus.Incomplete) return true; // Has positive prev. quest in non-rewarded state // and negative prev. quest in non-active state if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestPreviousQuest: Sent QUEST_ERR_NONE (QuestID: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) doesn't have required quest {prevId}."); } return false; } bool SatisfyQuestDependentPreviousQuests(Quest qInfo, bool msg) { // No previous quest (might be first quest in a series) if (qInfo.DependentPreviousQuests.Empty()) return true; foreach (uint prevId in qInfo.DependentPreviousQuests) { // checked in startup Quest questInfo = Global.ObjectMgr.GetQuestTemplate(prevId); // If any of the previous quests completed, return true if (IsQuestRewarded(prevId)) { // skip one-from-all exclusive group if (questInfo.ExclusiveGroup >= 0) return true; // each-from-all exclusive group (< 0) // can be start if only all quests in prev quest exclusive group completed and rewarded var bounds = Global.ObjectMgr.GetExclusiveQuestGroupBounds(questInfo.ExclusiveGroup); foreach (var exclusiveQuestId in bounds) { // skip checked quest id, only state of other quests in group is interesting if (exclusiveQuestId == prevId) continue; // alternative quest from group also must be completed and rewarded (reported) if (!IsQuestRewarded(exclusiveQuestId)) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestDependentPreviousQuests: Sent QUEST_ERR_NONE (QuestID: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) doesn't have the required quest (1)."); } return false; } } return true; } } // Has only prev. quests in non-rewarded state if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestDependentPreviousQuests: Sent QUEST_ERR_NONE (QuestID: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) doesn't have required quest (2)."); } return false; } bool SatisfyQuestBreadcrumbQuest(Quest qInfo, bool msg) { uint breadcrumbTargetQuestId = (uint)Math.Abs(qInfo.BreadcrumbForQuestId); //If this is not a breadcrumb quest. if (breadcrumbTargetQuestId == 0) return true; // If the target quest is not available if (!CanTakeQuest(Global.ObjectMgr.GetQuestTemplate(breadcrumbTargetQuestId), false)) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestBreadcrumbQuest: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: {qInfo.Id}) because target quest (QuestID: {breadcrumbTargetQuestId}) is not available to player '{GetName()}' ({GetGUID()})."); } return false; } return true; } bool SatisfyQuestDependentBreadcrumbQuests(Quest qInfo, bool msg) { foreach (uint breadcrumbQuestId in qInfo.DependentBreadcrumbQuests) { QuestStatus status = GetQuestStatus(breadcrumbQuestId); // If any of the breadcrumb quests are in the quest log, return false. if (status == QuestStatus.Incomplete || status == QuestStatus.Complete || status == QuestStatus.Failed) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestDependentBreadcrumbQuests: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) has a breadcrumb quest towards this quest in the quest log."); } return false; } } return true; } public bool SatisfyQuestClass(Quest qInfo, bool msg) { uint reqClass = qInfo.AllowableClasses; if (reqClass == 0) return true; if ((reqClass & GetClassMask()) == 0) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestClass: Sent QuestFailedReason.None (questId: {0}) because player does not have required class.", qInfo.Id); } return false; } return true; } public bool SatisfyQuestRace(Quest qInfo, bool msg) { if (!qInfo.AllowableRaces.HasRace(GetRace())) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.FailedWrongRace); Log.outDebug(LogFilter.Server, $"SatisfyQuestRace: Sent QuestFailedReasons.FailedWrongRace (questId: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) does not have required race."); } return false; } return true; } public bool SatisfyQuestMinReputation(Quest qInfo, bool msg) { uint fIdMin = qInfo.RequiredMinRepFaction; //Min required rep if (fIdMin != 0 && GetReputationMgr().GetReputation(fIdMin) < qInfo.RequiredMinRepValue) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestReputation: Sent QuestFailedReason.None (questId: {0}) because player does not have required reputation (min).", qInfo.Id); } return false; } return true; } public bool SatisfyQuestMaxReputation(Quest qInfo, bool msg) { uint fIdMax = qInfo.RequiredMaxRepFaction; //Max required rep if (fIdMax != 0 && GetReputationMgr().GetReputation(fIdMax) >= qInfo.RequiredMaxRepValue) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestReputation: Sent QuestFailedReason.None (questId: {0}) because player does not have required reputation (max).", qInfo.Id); } return false; } return true; } bool SatisfyQuestReputation(Quest qInfo, bool msg) { return SatisfyQuestMinReputation(qInfo, msg) && SatisfyQuestMaxReputation(qInfo, msg); } public bool SatisfyQuestStatus(Quest qInfo, bool msg) { if (GetQuestStatus(qInfo.Id) == QuestStatus.Rewarded) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.AlreadyDone); Log.outDebug(LogFilter.Misc, "Player.SatisfyQuestStatus: Sent QUEST_STATUS_REWARDED (QuestID: {0}) because player '{1}' ({2}) quest status is already REWARDED.", qInfo.Id, GetName(), GetGUID().ToString()); } return false; } if (GetQuestStatus(qInfo.Id) != QuestStatus.None) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.AlreadyOn1); Log.outDebug(LogFilter.Server, "SatisfyQuestStatus: Sent QuestFailedReasons.AlreadyOn1 (questId: {0}) because player quest status is not NONE.", qInfo.Id); } return false; } return true; } public bool SatisfyQuestConditions(Quest qInfo, bool msg) { if (!Global.ConditionMgr.IsObjectMeetingNotGroupedConditions(ConditionSourceType.QuestAvailable, qInfo.Id, this)) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestConditions: Sent QuestFailedReason.None (questId: {0}) because player does not meet conditions.", qInfo.Id); } Log.outDebug(LogFilter.Condition, "SatisfyQuestConditions: conditions not met for quest {0}", qInfo.Id); return false; } return true; } public bool SatisfyQuestTimed(Quest qInfo, bool msg) { if (!m_timedquests.Empty() && qInfo.LimitTime != 0) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.OnlyOneTimed); Log.outDebug(LogFilter.Server, "SatisfyQuestTimed: Sent QuestFailedReasons.OnlyOneTimed (questId: {0}) because player is already on a timed quest.", qInfo.Id); } return false; } return true; } public bool SatisfyQuestExclusiveGroup(Quest qInfo, bool msg) { // non positive exclusive group, if > 0 then can be start if any other quest in exclusive group already started/completed if (qInfo.ExclusiveGroup <= 0) return true; var range = Global.ObjectMgr.GetExclusiveQuestGroupBounds(qInfo.ExclusiveGroup); // always must be found if qInfo.ExclusiveGroup != 0 foreach (var exclude_Id in range) { // skip checked quest id, only state of other quests in group is interesting if (exclude_Id == qInfo.Id) continue; // not allow have daily quest if daily quest from exclusive group already recently completed Quest Nquest = Global.ObjectMgr.GetQuestTemplate(exclude_Id); if (!SatisfyQuestDay(Nquest, false) || !SatisfyQuestWeek(Nquest, false) || !SatisfyQuestSeasonal(Nquest, false)) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestExclusiveGroup: Sent QuestFailedReason.None (questId: {0}) because player already did daily quests in exclusive group.", qInfo.Id); } return false; } // alternative quest already started or completed - but don't check rewarded states if both are repeatable if (GetQuestStatus(exclude_Id) != QuestStatus.None || (!(qInfo.IsRepeatable() && Nquest.IsRepeatable()) && GetQuestRewardStatus(exclude_Id))) { if (msg) { SendCanTakeQuestResponse(QuestFailedReasons.None); Log.outDebug(LogFilter.Server, "SatisfyQuestExclusiveGroup: Sent QuestFailedReason.None (questId: {0}) because player already did quest in exclusive group.", qInfo.Id); } return false; } } return true; } public bool SatisfyQuestDay(Quest qInfo, bool msg) { if (!qInfo.IsDaily() && !qInfo.IsDFQuest()) return true; if (qInfo.IsDFQuest()) { if (m_DFQuests.Contains(qInfo.Id)) return false; return true; } return m_activePlayerData.DailyQuestsCompleted.FindIndex(qInfo.Id) == -1; } public bool SatisfyQuestWeek(Quest qInfo, bool msg) { if (!qInfo.IsWeekly() || m_weeklyquests.Empty()) return true; // if not found in cooldown list return !m_weeklyquests.Contains(qInfo.Id); } public bool SatisfyQuestSeasonal(Quest qInfo, bool msg) { if (!qInfo.IsSeasonal() || m_seasonalquests.Empty()) return true; var list = m_seasonalquests.LookupByKey(qInfo.GetEventIdForQuest()); if (list == null || list.Empty()) return true; // if not found in cooldown list return !list.ContainsKey(qInfo.Id); } public bool SatisfyQuestExpansion(Quest qInfo, bool msg) { if ((int)GetSession().GetExpansion() < qInfo.Expansion) { if (msg) SendCanTakeQuestResponse(QuestFailedReasons.FailedExpansion); Log.outDebug(LogFilter.Misc, $"Player.SatisfyQuestExpansion: Sent QUEST_ERR_FAILED_EXPANSION (QuestID: {qInfo.Id}) because player '{GetName()}' ({GetGUID()}) does not have required expansion."); return false; } return true; } public bool SatisfyQuestMonth(Quest qInfo, bool msg) { if (!qInfo.IsMonthly() || m_monthlyquests.Empty()) return true; // if not found in cooldown list return !m_monthlyquests.Contains(qInfo.Id); } public bool GiveQuestSourceItem(Quest quest) { uint srcitem = quest.SourceItemId; if (srcitem > 0) { // Don't give source item if it is the same item used to start the quest ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(srcitem); if (quest.Id == itemTemplate.GetStartQuest()) return true; uint count = quest.SourceItemIdCount; if (count <= 0) count = 1; List dest = new(); InventoryResult msg = CanStoreNewItem(ItemConst.NullBag, ItemConst.NullSlot, dest, srcitem, count); if (msg == InventoryResult.Ok) { Item item = StoreNewItem(dest, srcitem, true); SendNewItem(item, count, true, false); return true; } // player already have max amount required item, just report success if (msg == InventoryResult.ItemMaxCount) return true; SendEquipError(msg, null, null, srcitem); return false; } return true; } public bool TakeQuestSourceItem(uint questId, bool msg) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) { uint srcItemId = quest.SourceItemId; ItemTemplate item = Global.ObjectMgr.GetItemTemplate(srcItemId); if (srcItemId > 0) { uint count = quest.SourceItemIdCount; if (count <= 0) count = 1; // There are two cases where the source item is not destroyed: // - Item cannot be unequipped (example: non-empty bags) // - The source item is the item that started the quest, so the player is supposed to keep it (otherwise it was already destroyed in AddQuestAndCheckCompletion()) InventoryResult res = CanUnequipItems(srcItemId, count); if (res != InventoryResult.Ok) { if (msg) SendEquipError(res, null, null, srcItemId); return false; } if (item.GetStartQuest() != questId) DestroyItemCount(srcItemId, count, true, true); } } return true; } public bool GetQuestRewardStatus(uint quest_id) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(quest_id); if (qInfo != null) { if (qInfo.IsSeasonal() && !qInfo.IsRepeatable()) return !SatisfyQuestSeasonal(qInfo, false); // for repeatable quests: rewarded field is set after first reward only to prevent getting XP more than once if (!qInfo.IsRepeatable()) return IsQuestRewarded(quest_id); return false; } return false; } public QuestStatus GetQuestStatus(uint questId) { if (questId != 0) { var questStatusData = m_QuestStatus.LookupByKey(questId); if (questStatusData != null) return questStatusData.Status; if (GetQuestRewardStatus(questId)) return QuestStatus.Rewarded; } return QuestStatus.None; } public bool CanShareQuest(uint quest_id) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(quest_id); if (qInfo != null && qInfo.HasFlag(QuestFlags.Sharable)) { var questStatusData = m_QuestStatus.LookupByKey(quest_id); return questStatusData != null; } return false; } public void SetQuestStatus(uint questId, QuestStatus status, bool update = true) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest != null) { if (!m_QuestStatus.ContainsKey(questId)) m_QuestStatus[questId] = new QuestStatusData(); QuestStatus oldStatus = m_QuestStatus[questId].Status; m_QuestStatus[questId].Status = status; if (!quest.IsTurnIn()) m_QuestStatusSave[questId] = QuestSaveType.Default; Global.ScriptMgr.OnQuestStatusChange(this, questId); Global.ScriptMgr.OnQuestStatusChange(this, quest, oldStatus, status); } if (update) SendQuestUpdate(questId); } public void RemoveActiveQuest(uint questId, bool update = true) { var questStatus = m_QuestStatus.LookupByKey(questId); if (questStatus != null) { foreach (var objective in m_questObjectiveStatus.KeyValueList) { if (objective.Value.QuestStatusPair.Status == questStatus) m_questObjectiveStatus.Remove(objective); } m_QuestStatus.Remove(questId); m_QuestStatusSave[questId] = QuestSaveType.Delete; } Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); bool updateVisibility = false; if (update) { SendQuestUpdate(questId); if (quest != null && quest.HasFlag(QuestFlags.UpdatePhaseshift)) updateVisibility = PhasingHandler.OnConditionChange(this, false); } if (updateVisibility) UpdateObjectVisibility(); } public void RemoveRewardedQuest(uint questId, bool update = true) { if (m_RewardedQuests.Contains(questId)) { m_RewardedQuests.Remove(questId); m_RewardedQuestsSave[questId] = QuestSaveType.ForceDelete; } uint questBit = Global.DB2Mgr.GetQuestUniqueBitFlag(questId); if (questBit != 0) SetQuestCompletedBit(questBit, false); // Remove seasonal quest also Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); bool updateVisibility = false; if (quest != null && quest.IsSeasonal()) { ushort eventId = quest.GetEventIdForQuest(); if (m_seasonalquests.ContainsKey(eventId)) { m_seasonalquests[eventId].Remove(questId); m_SeasonalQuestChanged = true; } } if (update) { SendQuestUpdate(questId); if (quest != null && quest.HasFlag(QuestFlags.UpdatePhaseshift)) updateVisibility = PhasingHandler.OnConditionChange(this, false); } if (updateVisibility) UpdateObjectVisibility(); } void SendQuestUpdate(uint questId, bool updateInteractions = true, bool updateGameObjectQuestGiverStatus = false) { var saBounds = Global.SpellMgr.GetSpellAreaForQuestMapBounds(questId); if (!saBounds.Empty()) { List aurasToRemove = new(); List aurasToCast = new(); GetZoneAndAreaId(out uint zone, out uint area); foreach (var spell in saBounds) { if (spell.flags.HasAnyFlag(SpellAreaFlag.AutoRemove) && !spell.IsFitToRequirements(this, zone, area)) aurasToRemove.Add(spell.spellId); else if (spell.flags.HasAnyFlag(SpellAreaFlag.AutoCast) && !spell.flags.HasAnyFlag(SpellAreaFlag.IgnoreAutocastOnQuestStatusChange)) aurasToCast.Add(spell.spellId); } // Auras matching the requirements will be inside the aurasToCast container. // Auras not matching the requirements may prevent using auras matching the requirements. // aurasToCast will erase conflicting auras in aurasToRemove container to handle spells used by multiple quests. for (var c = 0; c < aurasToRemove.Count;) { bool auraRemoved = false; foreach (var i in aurasToCast) { if (aurasToRemove[c] == i) { aurasToRemove.Remove(aurasToRemove[c]); auraRemoved = true; break; } } if (!auraRemoved) ++c; } foreach (var spellId in aurasToCast) if (!HasAura(spellId)) CastSpell(this, spellId, true); foreach (var spellId in aurasToRemove) RemoveAurasDueToSpell(spellId); } if (updateInteractions) UpdateVisibleObjectInteractions(true, false, updateGameObjectQuestGiverStatus, true); } public QuestGiverStatus GetQuestDialogStatus(WorldObject questgiver) { QuestRelationResult questRelations; QuestRelationResult questInvolvedRelations; switch (questgiver.GetTypeId()) { case TypeId.GameObject: { GameObjectAI ai = questgiver.ToGameObject().GetAI(); if (ai != null) { var questStatus = ai.GetDialogStatus(this); if (questStatus.HasValue) return questStatus.Value; } questRelations = Global.ObjectMgr.GetGOQuestRelations(questgiver.GetEntry()); questInvolvedRelations = Global.ObjectMgr.GetGOQuestInvolvedRelations(questgiver.GetEntry()); break; } case TypeId.Unit: { Creature questGiverCreature = questgiver.ToCreature(); if (!questGiverCreature.IsInteractionAllowedWhileHostile() && questGiverCreature.IsHostileTo(this)) return QuestGiverStatus.None; if (!questGiverCreature.IsInteractionAllowedInCombat() && questGiverCreature.IsInCombat()) return QuestGiverStatus.None; CreatureAI ai = questgiver.ToCreature().GetAI(); if (ai != null) { QuestGiverStatus? questStatus = ai.GetDialogStatus(this); if (questStatus.HasValue) return questStatus.Value; } questRelations = Global.ObjectMgr.GetCreatureQuestRelations(questgiver.GetEntry()); questInvolvedRelations = Global.ObjectMgr.GetCreatureQuestInvolvedRelations(questgiver.GetEntry()); break; } default: // it's impossible, but check Log.outError(LogFilter.Player, "GetQuestDialogStatus called for unexpected type {0}", questgiver.GetTypeId()); return QuestGiverStatus.None; } QuestGiverStatus result = QuestGiverStatus.None; foreach (var questId in questInvolvedRelations) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; switch (GetQuestStatus(questId)) { case QuestStatus.Complete: if (quest.IsImportant()) result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.ImportantQuestRewardCompleteNoPOI : QuestGiverStatus.ImportantQuestRewardCompletePOI; else if (quest.IsMeta()) result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.MetaQuestRewardCompleteNoPOI : QuestGiverStatus.MetaQuestRewardCompletePOI; else if (quest.GetQuestTag() == QuestTagType.CovenantCalling) result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.CovenantCallingRewardCompleteNoPOI : QuestGiverStatus.CovenantCallingRewardCompletePOI; else if (quest.HasFlagEx(QuestFlagsEx.Legendary)) result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.LegendaryRewardCompleteNoPOI : QuestGiverStatus.LegendaryRewardCompletePOI; else if (quest.IsDailyOrWeekly()) result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.RepeatableRewardCompleteNoPOI : QuestGiverStatus.RepeatableRewardCompletePOI; else result |= quest.HasFlag(QuestFlags.HideRewardPoi) ? QuestGiverStatus.RewardCompleteNoPOI : QuestGiverStatus.RewardCompletePOI; break; case QuestStatus.Incomplete: if (quest.IsImportant()) result |= QuestGiverStatus.ImportantReward; else if (quest.IsMeta()) result |= QuestGiverStatus.MetaReward; else if (quest.GetQuestTag() == QuestTagType.CovenantCalling) result |= QuestGiverStatus.CovenantCallingReward; else if (quest.HasFlagEx(QuestFlagsEx.Legendary)) result |= QuestGiverStatus.LegendaryReward; else if (quest.IsDailyOrWeekly()) result |= QuestGiverStatus.RepeatableReward; else result |= QuestGiverStatus.Reward; break; default: break; } if (quest.IsTurnIn() && CanTakeQuest(quest, false)) { bool isTrivial = GetLevel() > (GetQuestLevel(quest) + WorldConfig.GetIntValue(WorldCfg.QuestLowLevelHideDiff)); if (quest.IsRepeatable()) result |= isTrivial ? QuestGiverStatus.TrivialRepeatableTurnin : QuestGiverStatus.RepeatableTurnin; else result |= isTrivial ? QuestGiverStatus.Trivial : QuestGiverStatus.Quest; } } foreach (var questId in questRelations) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; if (!Global.ConditionMgr.IsObjectMeetingNotGroupedConditions(ConditionSourceType.QuestAvailable, quest.Id, this)) continue; if (GetQuestStatus(questId) == QuestStatus.None) { if (CanSeeStartQuest(quest)) { if (SatisfyQuestLevel(quest, false)) { bool isTrivial = GetLevel() > (GetQuestLevel(quest) + WorldConfig.GetIntValue(WorldCfg.QuestLowLevelHideDiff)); if (quest.IsImportant()) result |= isTrivial ? QuestGiverStatus.TrivialImportantQuest : QuestGiverStatus.ImportantQuest; else if (quest.IsMeta()) result |= isTrivial ? QuestGiverStatus.TrivialMetaQuest : QuestGiverStatus.MetaQuest; else if (quest.GetQuestTag() == QuestTagType.CovenantCalling) result |= QuestGiverStatus.CovenantCallingQuest; else if (quest.HasFlagEx(QuestFlagsEx.Legendary)) result |= isTrivial ? QuestGiverStatus.TrivialLegendaryQuest : QuestGiverStatus.LegendaryQuest; else if (quest.IsDailyOrWeekly()) result |= isTrivial ? QuestGiverStatus.TrivialRepeatableQuest : QuestGiverStatus.RepeatableQuest; else result |= isTrivial ? QuestGiverStatus.Trivial : QuestGiverStatus.Quest; } else if (quest.IsImportant()) result |= QuestGiverStatus.FutureImportantQuest; else if (quest.HasFlagEx(QuestFlagsEx.Legendary)) result |= QuestGiverStatus.FutureLegendaryQuest; else result |= QuestGiverStatus.Future; } } } return result; } /// /// Removes quest from log, flags rewarded, but does not give any rewards to player /// /// public void SkipQuests(List questIds) { bool updateVisibility = false; foreach (uint questId in questIds) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) return; ushort questSlot = FindQuestSlot(questId); QuestStatus oldStatus = GetQuestStatus(questSlot); if (questSlot != SharedConst.MaxQuestLogSize) { if (quest.LimitTime != 0) RemoveTimedQuest(questId); if (quest.HasFlag(QuestFlags.Pvp)) { pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); UpdatePvPState(); } SetQuestSlot(questSlot, 0); TakeQuestSourceItem(questId, true); // remove quest src item from player AbandonQuest(questId); // remove all quest items player received before abandoning quest. Note, this does not remove normal drop items that happen to be quest requirements. RemoveActiveQuest(questId); } SetRewardedQuest(questId); SendQuestUpdate(questId, false); if (!updateVisibility && quest.HasFlag(QuestFlags.UpdatePhaseshift)) updateVisibility = PhasingHandler.OnConditionChange(this, false); Global.ScriptMgr.OnQuestStatusChange(this, questId); Global.ScriptMgr.OnQuestStatusChange(this, quest, oldStatus, QuestStatus.Rewarded); } UpdateVisibleObjectInteractions(true, false, true, true); // make full db save SaveToDB(false); if (updateVisibility) UpdateObjectVisibility(); } public void DespawnPersonalSummonsForQuest(uint questId) { List creatureList = GetCreatureListWithOptionsInGrid(100.0f, new FindCreatureOptions() { IgnorePhases = true, PrivateObjectOwnerGuid = GetGUID() }); // we might want to replace this with SummonList in Player at some point foreach (Creature creature in creatureList) { CreatureSummonedData summonedData = Global.ObjectMgr.GetCreatureSummonedData(creature.GetEntry()); if (summonedData == null) continue; if (summonedData.DespawnOnQuestsRemoved != null) { if (summonedData.DespawnOnQuestsRemoved.Contains(questId)) creature.DespawnOrUnsummon(); } } } public ushort GetReqKillOrCastCurrentCount(uint quest_id, int entry) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(quest_id); if (qInfo == null) return 0; ushort slot = FindQuestSlot(quest_id); if (slot >= SharedConst.MaxQuestLogSize) return 0; foreach (QuestObjective obj in qInfo.Objectives) if (obj.ObjectID == entry) return (ushort)GetQuestSlotObjectiveData(slot, obj); return 0; } public void AdjustQuestObjectiveProgress(Quest quest) { // adjust progress of quest objectives that rely on external counters, like items foreach (QuestObjective obj in quest.Objectives) { switch (obj.Type) { case QuestObjectiveType.Item: if (!obj.Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem)) { uint reqItemCount = (uint)obj.Amount; uint curItemCount = GetItemCount((uint)obj.ObjectID, true); SetQuestObjectiveData(obj, (int)Math.Min(curItemCount, reqItemCount)); } break; case QuestObjectiveType.Currency: uint reqCurrencyCount = (uint)obj.Amount; uint curCurrencyCount = GetCurrencyQuantity((uint)obj.ObjectID); SetQuestObjectiveData(obj, (int)Math.Min(reqCurrencyCount, curCurrencyCount)); break; case QuestObjectiveType.CriteriaTree: if (m_questObjectiveCriteriaMgr.HasCompletedObjective(obj)) SetQuestObjectiveData(obj, 1); break; } } } public ushort FindQuestSlot(uint quest_id) { for (ushort i = 0; i < SharedConst.MaxQuestLogSize; ++i) if (GetQuestSlotQuestId(i) == quest_id) return i; return SharedConst.MaxQuestLogSize; } public uint GetQuestSlotQuestId(ushort slot) { return m_playerData.QuestLog[slot].QuestID; } public uint GetQuestSlotState(ushort slot, byte counter) { return m_playerData.QuestLog[slot].StateFlags; } public ushort GetQuestSlotCounter(ushort slot, byte counter) { if (counter < SharedConst.MaxQuestCounts) return m_playerData.QuestLog[slot].ObjectiveProgress[counter]; return 0; } public long GetQuestSlotEndTime(ushort slot) { return m_playerData.QuestLog[slot].EndTime; } bool GetQuestSlotObjectiveFlag(ushort slot, sbyte objectiveIndex) { if (objectiveIndex < SharedConst.MaxQuestCounts) return ((m_playerData.QuestLog[slot].ObjectiveFlags) & (1 << objectiveIndex)) != 0; return false; } public int GetQuestSlotObjectiveData(ushort slot, QuestObjective objective) { if (objective.StorageIndex < 0) { Log.outError(LogFilter.Player, $"Player.GetQuestObjectiveData: Called for quest {objective.QuestID} with invalid StorageIndex {objective.StorageIndex} (objective data is not tracked)"); return 0; } if (objective.StorageIndex >= SharedConst.MaxQuestCounts) { Log.outError(LogFilter.Player, $"Player.GetQuestObjectiveData: Player '{GetName()}' ({GetGUID()}) quest {objective.QuestID} out of range StorageIndex {objective.StorageIndex}"); return 0; } if (!objective.IsStoringFlag()) return GetQuestSlotCounter(slot, (byte)objective.StorageIndex); return GetQuestSlotObjectiveFlag(slot, objective.StorageIndex) ? 1 : 0; } public int GetQuestObjectiveData(uint questId, uint objectiveId) { ushort slot = FindQuestSlot(questId); if (slot >= SharedConst.MaxQuestLogSize) return 0; QuestObjective obj = Global.ObjectMgr.GetQuestObjective(objectiveId); if (obj == null) return 0; return GetQuestSlotObjectiveData(slot, obj); } public void SetQuestSlot(ushort slot, uint quest_id) { var questLogField = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); SetUpdateFieldValue(questLogField.ModifyValue(questLogField.QuestID), quest_id); SetUpdateFieldValue(questLogField.ModifyValue(questLogField.StateFlags), 0u); SetUpdateFieldValue(questLogField.ModifyValue(questLogField.EndTime), 0u); SetUpdateFieldValue(questLogField.ModifyValue(questLogField.ObjectiveFlags), 0u); for (int i = 0; i < SharedConst.MaxQuestCounts; ++i) SetUpdateFieldValue(ref questLogField.ModifyValue(questLogField.ObjectiveProgress, i), (ushort)0); } public void SetQuestSlotCounter(ushort slot, byte counter, ushort count) { if (counter >= SharedConst.MaxQuestCounts) return; QuestLog questLog = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); SetUpdateFieldValue(ref questLog.ModifyValue(questLog.ObjectiveProgress, counter), count); } public void SetQuestSlotState(ushort slot, QuestSlotStateMask state) { QuestLog questLogField = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); SetUpdateFieldFlagValue(questLogField.ModifyValue(questLogField.StateFlags), (uint)state); } public void RemoveQuestSlotState(ushort slot, QuestSlotStateMask state) { QuestLog questLogField = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); RemoveUpdateFieldFlagValue(questLogField.ModifyValue(questLogField.StateFlags), (uint)state); } public void SetQuestSlotEndTime(ushort slot, long endTime) { QuestLog questLog = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); SetUpdateFieldValue(questLog.ModifyValue(questLog.EndTime), (uint)endTime); } void SetQuestSlotObjectiveFlag(ushort slot, sbyte objectiveIndex) { QuestLog questLog = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); SetUpdateFieldFlagValue(questLog.ModifyValue(questLog.ObjectiveFlags), 1u << objectiveIndex); } void RemoveQuestSlotObjectiveFlag(ushort slot, sbyte objectiveIndex) { QuestLog questLog = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.QuestLog, slot); RemoveUpdateFieldFlagValue(questLog.ModifyValue(questLog.ObjectiveFlags), 1u << objectiveIndex); } void SetQuestCompletedBit(uint questBit, bool completed) { if (questBit == 0) return; uint fieldOffset = (uint)((questBit - 1) / ActivePlayerData.QuestCompletedBitsPerBlock); if (fieldOffset >= ActivePlayerData.QuestCompletedBitsSize) return; ulong flag = 1ul << (((int)questBit - 1) % ActivePlayerData.QuestCompletedBitsPerBlock); if (completed) SetUpdateFieldFlagValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.QuestCompleted, (int)fieldOffset), flag); else RemoveUpdateFieldFlagValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.QuestCompleted, (int)fieldOffset), flag); } public void AreaExploredOrEventHappens(uint questId) { if (questId != 0) { QuestStatusData status = m_QuestStatus.LookupByKey(questId); if (status != null) { // Dont complete failed quest if (!status.Explored && status.Status != QuestStatus.Failed) { status.Explored = true; m_QuestStatusSave[questId] = QuestSaveType.Default; SendQuestComplete(questId); } } if (CanCompleteQuest(questId)) CompleteQuest(questId); } } public void GroupEventHappens(uint questId, WorldObject pEventObject) { var group = GetGroup(); if (group != null) { for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) { Player player = refe.GetSource(); // for any leave or dead (with not released body) group member at appropriate distance if (player != null && player.IsAtGroupRewardDistance(pEventObject) && player.GetCorpse() == null) player.AreaExploredOrEventHappens(questId); } } else AreaExploredOrEventHappens(questId); } Func QuestBoundItemFunc = objective => { return objective.Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem); }; Func NotQuestBoundItemFunc = objective => { return !objective.Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem); }; public void ItemAddedQuestCheck(uint entry, uint count, bool? boundItemFlagRequirement = null) { ItemAddedQuestCheck(entry, count, boundItemFlagRequirement, out _); } public void ItemAddedQuestCheck(uint entry, uint count, bool? boundItemFlagRequirement, out bool hadBoundItemObjective) { hadBoundItemObjective = false; List updatedObjectives = new(); Func objectiveFilter = null; if (boundItemFlagRequirement.HasValue) objectiveFilter = boundItemFlagRequirement.Value ? QuestBoundItemFunc : NotQuestBoundItemFunc; ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(entry); UpdateQuestObjectiveProgress(QuestObjectiveType.Item, (int)itemTemplate.GetId(), count, ObjectGuid.Empty, updatedObjectives, objectiveFilter); if (itemTemplate.QuestLogItemId != 0 && (updatedObjectives.Count != 1 || !updatedObjectives[0].Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem))) UpdateQuestObjectiveProgress(QuestObjectiveType.Item, itemTemplate.QuestLogItemId, count, ObjectGuid.Empty, updatedObjectives, objectiveFilter); if (updatedObjectives.Count == 1 && updatedObjectives[0].Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem)) { // Quest source items should ignore QUEST_OBJECTIVE_FLAG_2_QUEST_BOUND_ITEM Quest quest = Global.ObjectMgr.GetQuestTemplate(updatedObjectives[0].QuestID); if (quest != null && quest.SourceItemId == entry) return; hadBoundItemObjective = updatedObjectives.Count == 1 && updatedObjectives[0].Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem); SendQuestUpdateAddItem(itemTemplate, updatedObjectives[0], (ushort)count); } } public void ItemRemovedQuestCheck(uint entry, uint count) { foreach (var objectiveStatusData in m_questObjectiveStatus.LookupByKey((QuestObjectiveType.Item, (int)entry))) { uint questId = objectiveStatusData.QuestStatusPair.QuestID; ushort logSlot = objectiveStatusData.QuestStatusPair.Status.Slot; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); QuestObjective objective = Global.ObjectMgr.GetQuestObjective(objectiveStatusData.ObjectiveId); if (quest == null || objective == null || !IsQuestObjectiveCompletable(logSlot, quest, objective)) continue; int newItemCount = (int)GetItemCount(entry, false); // we may have more than what the status shows, so we have to iterate inventory if (newItemCount < objective.Amount) { SetQuestObjectiveData(objective, newItemCount); IncompleteQuest(questId); } } UpdateVisibleObjectInteractions(true, false, false, true); } public void KilledMonster(CreatureTemplate cInfo, ObjectGuid guid) { Cypher.Assert(cInfo != null); if (cInfo.Entry != 0) KilledMonsterCredit(cInfo.Entry, guid); for (byte i = 0; i < 2; ++i) if (cInfo.KillCredit[i] != 0) KilledMonsterCredit(cInfo.KillCredit[i]); } public void KilledMonsterCredit(uint entry, ObjectGuid guid = default) { ushort addKillCount = 1; uint real_entry = entry; Creature killed = null; if (!guid.IsEmpty()) { killed = GetMap().GetCreature(guid); if (killed != null && killed.GetEntry() != 0) real_entry = killed.GetEntry(); } StartCriteria(CriteriaStartEvent.KillNPC, real_entry); // MUST BE CALLED FIRST UpdateCriteria(CriteriaType.KillCreature, real_entry, addKillCount, 0, killed); UpdateQuestObjectiveProgress(QuestObjectiveType.Monster, (int)entry, 1, guid); } public void KilledPlayerCredit(ObjectGuid victimGuid) { StartCriteria(CriteriaStartEvent.KillPlayer, 0); UpdateQuestObjectiveProgress(QuestObjectiveType.PlayerKills, 0, 1, victimGuid); } public void KillCreditGO(uint entry, ObjectGuid guid = default) { UpdateQuestObjectiveProgress(QuestObjectiveType.GameObject, (int)entry, 1, guid); } public void KillCreditCriteriaTreeObjective(QuestObjective questObjective) { UpdateQuestObjectiveProgress(QuestObjectiveType.CriteriaTree, questObjective.ObjectID, 1); } public void TalkedToCreature(uint entry, ObjectGuid guid) { UpdateQuestObjectiveProgress(QuestObjectiveType.TalkTo, (int)entry, 1, guid); } public void MoneyChanged(ulong value) { UpdateQuestObjectiveProgress(QuestObjectiveType.Money, 0, (long)value - (long)GetMoney()); } public void ReputationChanged(FactionRecord FactionRecord, int change) { UpdateQuestObjectiveProgress(QuestObjectiveType.MinReputation, (int)FactionRecord.Id, change); UpdateQuestObjectiveProgress(QuestObjectiveType.MaxReputation, (int)FactionRecord.Id, change); UpdateQuestObjectiveProgress(QuestObjectiveType.IncreaseReputation, (int)FactionRecord.Id, change); } void CurrencyChanged(uint currencyId, int change) { UpdateQuestObjectiveProgress(QuestObjectiveType.Currency, (int)currencyId, change); UpdateQuestObjectiveProgress(QuestObjectiveType.HaveCurrency, (int)currencyId, change); UpdateQuestObjectiveProgress(QuestObjectiveType.ObtainCurrency, (int)currencyId, change); } public void UpdateQuestObjectiveProgress(QuestObjectiveType objectiveType, int objectId, long addCount, ObjectGuid victimGuid = default, List updatedObjectives = null, Func objectiveFilter = null) { bool anyObjectiveChangedCompletionState = false; bool updatePhaseShift = false; bool updateZoneAuras = false; foreach (var objectiveStatusData in m_questObjectiveStatus.LookupByKey((objectiveType, objectId))) { uint questId = objectiveStatusData.QuestStatusPair.QuestID; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; if (!QuestObjective.CanAlwaysBeProgressedInRaid(objectiveType)) if (GetGroup() != null && GetGroup().IsRaidGroup() && !quest.IsAllowedInRaid(GetMap().GetDifficultyID())) continue; ushort logSlot = objectiveStatusData.QuestStatusPair.Status.Slot; QuestObjective objective = Global.ObjectMgr.GetQuestObjective(objectiveStatusData.ObjectiveId); if (objective == null || !IsQuestObjectiveCompletable(logSlot, quest, objective)) continue; if (quest.HasFlagEx(QuestFlagsEx.NoCreditForProxy)) if (objective.Type == QuestObjectiveType.Monster && victimGuid.IsEmpty()) continue; bool objectiveWasComplete = IsQuestObjectiveComplete(logSlot, quest, objective); if (objectiveWasComplete && addCount >= 0) continue; if (objectiveFilter != null && !objectiveFilter(objective)) continue; bool objectiveIsNowComplete = false; if (objective.IsStoringValue()) { if (objectiveType == QuestObjectiveType.PlayerKills && objective.Flags.HasAnyFlag(QuestObjectiveFlags.KillPlayersSameFaction)) { Player victim = Global.ObjAccessor.GetPlayer(GetMap(), victimGuid); if (victim?.GetEffectiveTeam() != GetEffectiveTeam()) continue; } int currentProgress = GetQuestSlotObjectiveData(logSlot, objective); if (addCount > 0 ? (currentProgress < objective.Amount) : (currentProgress > 0)) { int newProgress = (int)Math.Clamp(currentProgress + addCount, 0, objective.Amount); SetQuestObjectiveData(objective, newProgress); if (addCount > 0 && !objective.Flags.HasFlag(QuestObjectiveFlags.HideCreditMsg)) { switch (objectiveType) { case QuestObjectiveType.Item: break; // case handled by SMSG_ITEM_PUSH_RESULT case QuestObjectiveType.PlayerKills: SendQuestUpdateAddPlayer(quest, (uint)newProgress); break; default: SendQuestUpdateAddCredit(quest, victimGuid, objective, (uint)newProgress); break; } } objectiveIsNowComplete = IsQuestObjectiveComplete(logSlot, quest, objective); } } else if (objective.IsStoringFlag()) { SetQuestObjectiveData(objective, addCount > 0 ? 1 : 0); if (addCount > 0 && !objective.Flags.HasFlag(QuestObjectiveFlags.HideCreditMsg)) SendQuestUpdateAddCreditSimple(objective); objectiveIsNowComplete = IsQuestObjectiveComplete(logSlot, quest, objective); } else { switch (objectiveType) { case QuestObjectiveType.Currency: objectiveIsNowComplete = GetCurrencyQuantity((uint)objectId) + addCount >= objective.Amount; break; case QuestObjectiveType.LearnSpell: objectiveIsNowComplete = addCount != 0; break; case QuestObjectiveType.MinReputation: objectiveIsNowComplete = GetReputationMgr().GetReputation((uint)objectId) + addCount >= objective.Amount; break; case QuestObjectiveType.MaxReputation: objectiveIsNowComplete = GetReputationMgr().GetReputation((uint)objectId) + addCount <= objective.Amount; break; case QuestObjectiveType.Money: objectiveIsNowComplete = (long)GetMoney() + addCount >= objective.Amount; break; case QuestObjectiveType.ProgressBar: objectiveIsNowComplete = IsQuestObjectiveProgressBarComplete(logSlot, quest); break; default: Cypher.Assert(false, "Unhandled quest objective type {objectiveType}"); break; } } if (objective.Flags.HasAnyFlag(QuestObjectiveFlags.PartOfProgressBar)) { if (IsQuestObjectiveProgressBarComplete(logSlot, quest)) { var progressBarObjective = quest.Objectives.Find(otherObjective => otherObjective.Type == QuestObjectiveType.ProgressBar && !otherObjective.Flags.HasFlag(QuestObjectiveFlags.PartOfProgressBar)); if (progressBarObjective != null) SendQuestUpdateAddCreditSimple(progressBarObjective); objectiveIsNowComplete = true; } } if (objectiveWasComplete != objectiveIsNowComplete) anyObjectiveChangedCompletionState = true; if (objectiveIsNowComplete && objective.CompletionEffect != null) { if (objective.CompletionEffect.GameEventId.HasValue) GameEvents.Trigger(objective.CompletionEffect.GameEventId.Value, this, null); if (objective.CompletionEffect.SpellId.HasValue) CastSpell(this, objective.CompletionEffect.SpellId.Value, true); if (objective.CompletionEffect.ConversationId.HasValue) Conversation.CreateConversation(objective.CompletionEffect.ConversationId.Value, this, GetPosition(), GetGUID()); if (objective.CompletionEffect.UpdatePhaseShift) updatePhaseShift = true; if (objective.CompletionEffect.UpdateZoneAuras) updateZoneAuras = true; } if (objectiveIsNowComplete) { if (CanCompleteQuest(questId, objective.Id)) CompleteQuest(questId); } else if (!objective.Flags.HasAnyFlag(QuestObjectiveFlags.Optional) && objectiveStatusData.QuestStatusPair.Status.Status == QuestStatus.Complete) IncompleteQuest(questId); if (updatedObjectives != null) updatedObjectives.Add(objective); if (objective.Type == QuestObjectiveType.Item && addCount >= 0 && objective.Flags2.HasFlag(QuestObjectiveFlags2.QuestBoundItem)) break; } if (anyObjectiveChangedCompletionState) UpdateVisibleObjectInteractions(true, false, false, true); if (updatePhaseShift) PhasingHandler.OnConditionChange(this); if (updateZoneAuras) { UpdateZoneDependentAuras(GetZoneId()); UpdateAreaDependentAuras(GetAreaId()); } } public bool HasQuestForItem(uint itemid) { // Search incomplete objective first if (GetQuestObjectiveForItem(itemid, true) != null) return true; // This part - for ItemDrop foreach (var questStatus in m_QuestStatus) { if (questStatus.Value.Status != QuestStatus.Incomplete) continue; Quest qInfo = Global.ObjectMgr.GetQuestTemplate(questStatus.Key); // hide quest if player is in raid-group and quest is no raid quest if (GetGroup() != null && GetGroup().IsRaidGroup() && !qInfo.IsAllowedInRaid(GetMap().GetDifficultyID())) if (!InBattleground()) continue; for (byte j = 0; j < SharedConst.QuestItemDropCount; ++j) { // examined item is a source item if (qInfo.ItemDrop[j] != itemid) continue; ItemTemplate pProto = Global.ObjectMgr.GetItemTemplate(itemid); // allows custom amount drop when not 0 uint maxAllowedCount = qInfo.ItemDropQuantity[j] != 0 ? qInfo.ItemDropQuantity[j] : pProto.GetMaxStackSize(); // 'unique' item if (pProto.GetMaxCount() != 0 && pProto.GetMaxCount() < maxAllowedCount) maxAllowedCount = pProto.GetMaxCount(); if (GetItemCount(itemid, true) < maxAllowedCount) return true; } } return false; } public bool CanSeeGossipOn(Creature creature) { if (creature.HasNpcFlag(NPCFlags.Gossip)) { if (GetGossipMenuForSource(creature) != 0) return true; } // for cases with questgiver/ender without gossip menus if (creature.HasNpcFlag(NPCFlags.QuestGiver)) { QuestRelationResult objectQIR = Global.ObjectMgr.GetCreatureQuestInvolvedRelations(creature.GetEntry()); foreach (uint quest_id in objectQIR) { QuestStatus status = GetQuestStatus(quest_id); if (status == QuestStatus.Complete || status == QuestStatus.Incomplete) return true; } QuestRelationResult objectQR = Global.ObjectMgr.GetCreatureQuestRelations(creature.GetEntry()); foreach (uint quest_id in objectQR) { Quest quest = Global.ObjectMgr.GetQuestTemplate(quest_id); if (quest == null) continue; if (CanTakeQuest(quest, false)) return true; } } return false; } QuestObjective GetQuestObjectiveForItem(uint itemId, bool onlyIncomplete) { QuestObjective findObjectiveForItem(int tempItemId) { foreach (var objectiveItr in m_questObjectiveStatus.LookupByKey((QuestObjectiveType.Item, tempItemId))) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(objectiveItr.QuestStatusPair.QuestID); QuestObjective objective = Global.ObjectMgr.GetQuestObjective(objectiveItr.ObjectiveId); if (qInfo == null || objective == null || !IsQuestObjectiveCompletable(objectiveItr.QuestStatusPair.Status.Slot, qInfo, objective)) continue; // hide quest if player is in raid-group and quest is no raid quest if (GetGroup() != null && GetGroup().IsRaidGroup() && !qInfo.IsAllowedInRaid(GetMap().GetDifficultyID())) if (!InBattleground()) //there are two ways.. we can make every bg-quest a raidquest, or add this code here.. i don't know if this can be exploited by other quests, but i think all other quests depend on a specific area.. but keep this in mind, if something strange happens later continue; if (!onlyIncomplete || !IsQuestObjectiveComplete(objectiveItr.QuestStatusPair.Status.Slot, qInfo, objective)) return objective; } return null; }; QuestObjective objective = findObjectiveForItem((int)itemId); if (objective != null) return objective; ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(itemId); if (itemTemplate != null && itemTemplate.QuestLogItemId != 0) { objective = findObjectiveForItem(itemTemplate.QuestLogItemId); if (objective != null) return objective; } return null; } public bool HasQuestForCurrency(uint currencyId) { bool isCompletableObjective(QuestObjectiveStatusData objectiveStatus) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(objectiveStatus.QuestStatusPair.QuestID); QuestObjective objective = Global.ObjectMgr.GetQuestObjective(objectiveStatus.ObjectiveId); if (qInfo == null || objective == null || !IsQuestObjectiveCompletable(objectiveStatus.QuestStatusPair.Status.Slot, qInfo, objective)) return false; // hide quest if player is in raid-group and quest is no raid quest if (GetGroup() != null && GetGroup().IsRaidGroup() && !qInfo.IsAllowedInRaid(GetMap().GetDifficultyID())) if (!InBattleground()) //there are two ways.. we can make every bg-quest a raidquest, or add this code here.. i don't know if this can be exploited by other quests, but i think all other quests depend on a specific area.. but keep this in mind, if something strange happens later return false; if (!IsQuestObjectiveComplete(objectiveStatus.QuestStatusPair.Status.Slot, qInfo, objective)) return true; return false; }; bool hasObjectiveTypeForCurrency(QuestObjectiveType type) => m_questObjectiveStatus.LookupByKey((type, currencyId)).Any(isCompletableObjective); if (hasObjectiveTypeForCurrency(QuestObjectiveType.Currency)) return true; if (hasObjectiveTypeForCurrency(QuestObjectiveType.HaveCurrency)) return true; if (hasObjectiveTypeForCurrency(QuestObjectiveType.ObtainCurrency)) return true; return false; } public int GetQuestObjectiveData(QuestObjective objective) { ushort slot = FindQuestSlot(objective.QuestID); if (slot >= SharedConst.MaxQuestLogSize) return 0; return GetQuestSlotObjectiveData(slot, objective); } public void SetQuestObjectiveData(QuestObjective objective, int data) { if (objective.StorageIndex < 0) { Log.outError(LogFilter.Player, $"Player.SetQuestObjectiveData: called for quest {objective.QuestID} with invalid StorageIndex {objective.StorageIndex} (objective data is not tracked)"); return; } var status = m_QuestStatus.LookupByKey(objective.QuestID); if (status == null) { Log.outError(LogFilter.Player, $"Player.SetQuestObjectiveData: player '{GetName()}' ({GetGUID()}) doesn't have quest status data (QuestID: {objective.QuestID})"); return; } if (objective.StorageIndex >= SharedConst.MaxQuestCounts) { Log.outError(LogFilter.Player, $"Player.SetQuestObjectiveData: player '{GetName()}' ({GetGUID()}) quest {objective.QuestID} out of range StorageIndex {objective.StorageIndex}"); return; } if (status.Slot >= SharedConst.MaxQuestLogSize) return; // No change int oldData = GetQuestSlotObjectiveData(status.Slot, objective); if (oldData == data) return; Quest quest = Global.ObjectMgr.GetQuestTemplate(objective.QuestID); if (quest != null) Global.ScriptMgr.OnQuestObjectiveChange(this, quest, objective, oldData, data); // Add to save m_QuestStatusSave[objective.QuestID] = QuestSaveType.Default; // Update quest fields if (!objective.IsStoringFlag()) SetQuestSlotCounter(status.Slot, (byte)objective.StorageIndex, (ushort)data); else if (data != 0) SetQuestSlotObjectiveFlag(status.Slot, objective.StorageIndex); else RemoveQuestSlotObjectiveFlag(status.Slot, objective.StorageIndex); } public bool IsQuestObjectiveCompletable(ushort slot, Quest quest, QuestObjective objective) { Cypher.Assert(objective.QuestID == quest.Id); if (objective.Flags.HasAnyFlag(QuestObjectiveFlags.PartOfProgressBar)) { // delegate check to actual progress bar objective var progressBarObjective = quest.Objectives.Find(otherObjective => otherObjective.Type == QuestObjectiveType.ProgressBar && !otherObjective.Flags.HasAnyFlag(QuestObjectiveFlags.PartOfProgressBar)); if (progressBarObjective == null) return false; return IsQuestObjectiveCompletable(slot, quest, progressBarObjective) && !IsQuestObjectiveComplete(slot, quest, progressBarObjective); } int objectiveIndex = quest.Objectives.IndexOf(objective); if (objectiveIndex == 0) return true; // check sequenced objectives int previousIndex = objectiveIndex - 1; bool objectiveSequenceSatisfied = true; bool previousSequencedObjectiveComplete = false; int previousSequencedObjectiveIndex = -1; do { QuestObjective previousObjective = quest.Objectives[previousIndex]; if (previousObjective.Flags.HasAnyFlag(QuestObjectiveFlags.Sequenced)) { previousSequencedObjectiveIndex = previousIndex; previousSequencedObjectiveComplete = IsQuestObjectiveComplete(slot, quest, previousObjective); break; } if (objectiveSequenceSatisfied) objectiveSequenceSatisfied = IsQuestObjectiveComplete(slot, quest, previousObjective) || previousObjective.Flags.HasAnyFlag(QuestObjectiveFlags.Optional | QuestObjectiveFlags.PartOfProgressBar); --previousIndex; } while (previousIndex >= 0); if (objective.Flags.HasAnyFlag(QuestObjectiveFlags.Sequenced)) { if (previousSequencedObjectiveIndex == -1) return objectiveSequenceSatisfied; if (!previousSequencedObjectiveComplete || !objectiveSequenceSatisfied) return false; } else if (!previousSequencedObjectiveComplete && previousSequencedObjectiveIndex != -1) { if (!IsQuestObjectiveCompletable(slot, quest, quest.Objectives[previousSequencedObjectiveIndex])) return false; } return true; } public bool IsQuestObjectiveComplete(ushort slot, Quest quest, QuestObjective objective) { switch (objective.Type) { case QuestObjectiveType.Monster: case QuestObjectiveType.Item: case QuestObjectiveType.GameObject: case QuestObjectiveType.TalkTo: case QuestObjectiveType.PlayerKills: case QuestObjectiveType.WinPvpPetBattles: case QuestObjectiveType.HaveCurrency: case QuestObjectiveType.ObtainCurrency: case QuestObjectiveType.IncreaseReputation: if (GetQuestSlotObjectiveData(slot, objective) < objective.Amount) return false; break; case QuestObjectiveType.MinReputation: if (GetReputationMgr().GetReputation((uint)objective.ObjectID) < objective.Amount) return false; break; case QuestObjectiveType.MaxReputation: if (GetReputationMgr().GetReputation((uint)objective.ObjectID) > objective.Amount) return false; break; case QuestObjectiveType.Money: if (!HasEnoughMoney(objective.Amount)) return false; break; case QuestObjectiveType.AreaTrigger: case QuestObjectiveType.WinPetBattleAgainstNpc: case QuestObjectiveType.DefeatBattlePet: case QuestObjectiveType.CriteriaTree: case QuestObjectiveType.AreaTriggerEnter: case QuestObjectiveType.AreaTriggerExit: if (GetQuestSlotObjectiveData(slot, objective) == 0) return false; break; case QuestObjectiveType.LearnSpell: if (!HasSpell((uint)objective.ObjectID)) return false; break; case QuestObjectiveType.Currency: if (!HasCurrency((uint)objective.ObjectID, (uint)objective.Amount)) return false; break; case QuestObjectiveType.ProgressBar: if (!IsQuestObjectiveProgressBarComplete(slot, quest)) return false; break; default: Log.outError(LogFilter.Player, "Player.CanCompleteQuest: Player '{0}' ({1}) tried to complete a quest (ID: {2}) with an unknown objective type {3}", GetName(), GetGUID().ToString(), objective.QuestID, objective.Type); return false; } return true; } bool IsQuestObjectiveComplete(uint questId, uint objectiveId) { Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) return false; ushort slot = FindQuestSlot(questId); if (slot >= SharedConst.MaxQuestLogSize) return false; QuestObjective obj = Global.ObjectMgr.GetQuestObjective(objectiveId); if (obj == null) return false; return IsQuestObjectiveComplete(slot, quest, obj); } public bool IsQuestObjectiveProgressBarComplete(ushort slot, Quest quest) { float progress = 0.0f; foreach (QuestObjective obj in quest.Objectives) { if (obj.Flags.HasAnyFlag(QuestObjectiveFlags.PartOfProgressBar)) { progress += GetQuestSlotObjectiveData(slot, obj) * obj.ProgressBarWeight; if (progress >= 100.0f) return true; } } return false; } public void SendQuestComplete(uint questId) { if (questId != 0) { QuestUpdateComplete data = new(); data.QuestID = questId; SendPacket(data); } } public void SendQuestReward(Quest quest, Creature questGiver, uint xp, bool hideChatMessage) { uint questId = quest.Id; Global.GameEventMgr.HandleQuestComplete(questId); uint moneyReward; if (!IsMaxLevel()) { moneyReward = GetQuestMoneyReward(quest); } else // At max level, increase gold reward { xp = 0; moneyReward = (uint)(GetQuestMoneyReward(quest) + (int)(quest.GetRewMoneyMaxLevel() * WorldConfig.GetFloatValue(WorldCfg.RateDropMoney))); } if (quest.HasFlag(QuestFlags.TrackingEvent)) return; QuestGiverQuestComplete packet = new(); packet.QuestID = questId; packet.MoneyReward = moneyReward; packet.XPReward = xp; packet.SkillLineIDReward = quest.RewardSkillId; packet.NumSkillUpsReward = quest.RewardSkillPoints; if (questGiver != null) { if (questGiver.IsGossip()) packet.LaunchGossip = quest.HasFlag(QuestFlags.LaunchGossipComplete); if (questGiver.IsQuestGiver()) packet.LaunchQuest = (GetQuestDialogStatus(questGiver) & ~QuestGiverStatus.Future) != QuestGiverStatus.None; if (!quest.HasFlag(QuestFlags.AutoComplete)) { Quest rewardQuest = GetNextQuest(questGiver, quest); if (rewardQuest != null) packet.UseQuestReward = CanTakeQuest(rewardQuest, false); } } packet.HideChatMessage = hideChatMessage; SendPacket(packet); } public void SendQuestFailed(uint questId, InventoryResult reason = InventoryResult.Ok) { if (questId != 0) { QuestGiverQuestFailed questGiverQuestFailed = new(); questGiverQuestFailed.QuestID = questId; questGiverQuestFailed.Reason = reason; // failed reason (valid reasons: 4, 16, 50, 17, other values show default message) SendPacket(questGiverQuestFailed); } } public void SendQuestTimerFailed(uint questId) { if (questId != 0) { QuestUpdateFailedTimer questUpdateFailedTimer = new(); questUpdateFailedTimer.QuestID = questId; SendPacket(questUpdateFailedTimer); } } public void SendCanTakeQuestResponse(QuestFailedReasons reason, bool sendErrorMessage = true, string reasonText = "") { QuestGiverInvalidQuest questGiverInvalidQuest = new(); questGiverInvalidQuest.Reason = reason; questGiverInvalidQuest.SendErrorMessage = sendErrorMessage; questGiverInvalidQuest.ReasonText = reasonText; SendPacket(questGiverInvalidQuest); } public void SendQuestConfirmAccept(Quest quest, Player receiver) { if (receiver == null) return; QuestConfirmAcceptResponse packet = new(); packet.QuestTitle = quest.LogTitle; Locale loc_idx = receiver.GetSession().GetSessionDbLocaleIndex(); if (loc_idx != Locale.enUS) { QuestTemplateLocale questLocale = Global.ObjectMgr.GetQuestLocale(quest.Id); if (questLocale != null) ObjectManager.GetLocaleString(questLocale.LogTitle, loc_idx, ref packet.QuestTitle); } packet.QuestID = quest.Id; packet.InitiatedBy = GetGUID(); receiver.SendPacket(packet); } public void SendPushToPartyResponse(Player player, QuestPushReason reason, Quest quest = null) { if (player != null) { QuestPushResultResponse response = new(); response.SenderGUID = player.GetGUID(); response.Result = reason; if (quest != null) { response.QuestTitle = quest.LogTitle; Locale localeConstant = GetSession().GetSessionDbLocaleIndex(); if (localeConstant != Locale.enUS) { QuestTemplateLocale questTemplateLocale = Global.ObjectMgr.GetQuestLocale(quest.Id); if (questTemplateLocale != null) ObjectManager.GetLocaleString(questTemplateLocale.LogTitle, localeConstant, ref response.QuestTitle); } } SendPacket(response); } } void SendQuestUpdateAddCredit(Quest quest, ObjectGuid guid, QuestObjective obj, uint count) { QuestUpdateAddCredit packet = new(); packet.VictimGUID = guid; packet.QuestID = quest.Id; packet.ObjectID = obj.ObjectID; packet.Count = (ushort)count; packet.Required = (ushort)obj.Amount; packet.ObjectiveType = (byte)obj.Type; SendPacket(packet); } public void SendQuestUpdateAddCreditSimple(QuestObjective obj) { QuestUpdateAddCreditSimple packet = new(); packet.QuestID = obj.QuestID; packet.ObjectID = obj.ObjectID; packet.ObjectiveType = obj.Type; SendPacket(packet); } void SendQuestUpdateAddItem(ItemTemplate itemTemplate, QuestObjective obj, ushort count) { ItemPushResult packet = new(); packet.PlayerGUID = GetGUID(); packet.Slot = InventorySlots.Bag0; packet.SlotInBag = 0; packet.Item.ItemID = itemTemplate.GetId(); packet.ProxyItemID = itemTemplate.QuestLogItemId; packet.Quantity = count; packet.QuantityInInventory = (uint)GetQuestObjectiveData(obj); packet.ChatNotifyType = ItemPushResult.DisplayType.EncounterLoot; packet.FakeQuestItem = true; if (GetGroup() != null && !itemTemplate.HasFlag(ItemFlags3.DontReportLootLogToParty)) GetGroup().BroadcastPacket(packet, true); else SendPacket(packet); } public void SendQuestUpdateAddPlayer(Quest quest, uint newCount) { QuestUpdateAddPvPCredit packet = new(); packet.QuestID = quest.Id; packet.Count = (ushort)newCount; SendPacket(packet); } public void SendQuestGiverStatusMultiple() { QuestGiverStatusMultiple response = new(); foreach (var itr in m_clientGUIDs) { if (itr.IsAnyTypeCreature()) { // need also pet quests case support Creature questgiver = ObjectAccessor.GetCreatureOrPetOrVehicle(this, itr); if (questgiver == null) continue; if (!questgiver.HasNpcFlag(NPCFlags.QuestGiver)) continue; response.QuestGiver.Add(new QuestGiverInfo(questgiver.GetGUID(), GetQuestDialogStatus(questgiver))); } else if (itr.IsGameObject()) { GameObject questgiver = GetMap().GetGameObject(itr); if (questgiver == null || questgiver.GetGoType() != GameObjectTypes.QuestGiver) continue; response.QuestGiver.Add(new QuestGiverInfo(questgiver.GetGUID(), GetQuestDialogStatus(questgiver))); } } SendPacket(response); } public bool HasPvPForcingQuest() { for (byte i = 0; i < SharedConst.MaxQuestLogSize; ++i) { uint questId = GetQuestSlotQuestId(i); if (questId == 0) continue; Quest quest = Global.ObjectMgr.GetQuestTemplate(questId); if (quest == null) continue; if (quest.HasFlag(QuestFlags.Pvp)) return true; } return false; } public bool HasQuestForGO(int GOId) { foreach (var objectiveStatusData in m_questObjectiveStatus.LookupByKey((QuestObjectiveType.GameObject, GOId))) { Quest qInfo = Global.ObjectMgr.GetQuestTemplate(objectiveStatusData.QuestStatusPair.QuestID); QuestObjective objective = Global.ObjectMgr.GetQuestObjective(objectiveStatusData.ObjectiveId); if (qInfo == null || objective == null || !IsQuestObjectiveCompletable(objectiveStatusData.QuestStatusPair.Status.Slot, qInfo, objective)) continue; // hide quest if player is in raid-group and quest is no raid quest if (GetGroup() != null && GetGroup().IsRaidGroup() && !qInfo.IsAllowedInRaid(GetMap().GetDifficultyID())) if (!InBattleground()) //there are two ways.. we can make every bg-quest a raidquest, or add this code here.. i don't know if this can be exploited by other quests, but i think all other quests depend on a specific area.. but keep this in mind, if something strange happens later continue; if (!IsQuestObjectiveComplete(objectiveStatusData.QuestStatusPair.Status.Slot, qInfo, objective)) return true; } return false; } public void UpdateVisibleObjectInteractions(bool allUnits, bool onlySpellClicks, bool gameObjectQuestGiverStatus, bool questObjectiveGameObjects) { QuestGiverStatusMultiple giverStatusMultiple = new(); UpdateData udata = new(GetMapId()); foreach (var visibleObjectGuid in m_clientGUIDs) { if (visibleObjectGuid.IsGameObject() && (gameObjectQuestGiverStatus || questObjectiveGameObjects)) { GameObject gameObject = ObjectAccessor.GetGameObject(this, visibleObjectGuid); if (gameObject == null) continue; if (gameObjectQuestGiverStatus && gameObject.GetGoType() == GameObjectTypes.QuestGiver) giverStatusMultiple.QuestGiver.Add(new QuestGiverInfo(visibleObjectGuid, GetQuestDialogStatus(gameObject))); if (questObjectiveGameObjects) { ObjectFieldData objMask = new(); GameObjectFieldData goMask = new(); if (m_questObjectiveStatus.ContainsKey((QuestObjectiveType.GameObject, (int)gameObject.GetEntry())) || gameObject.GetGoInfo().GetConditionID1() != 0) objMask.MarkChanged(gameObject.m_objectData.DynamicFlags); else { switch (gameObject.GetGoType()) { case GameObjectTypes.QuestGiver: case GameObjectTypes.Chest: case GameObjectTypes.Generic: case GameObjectTypes.SpellFocus: case GameObjectTypes.Goober: case GameObjectTypes.GatheringNode: if (Global.ObjectMgr.IsGameObjectForQuests(gameObject.GetEntry())) objMask.MarkChanged(gameObject.m_objectData.DynamicFlags); break; default: break; } } if (objMask.GetUpdateMask().IsAnySet() || goMask.GetUpdateMask().IsAnySet()) gameObject.BuildValuesUpdateForPlayerWithMask(udata, objMask.GetUpdateMask(), goMask.GetUpdateMask(), this); } } else if (visibleObjectGuid.IsCreatureOrVehicle() && (allUnits || onlySpellClicks)) { Creature creature = ObjectAccessor.GetCreatureOrPetOrVehicle(this, visibleObjectGuid); if (creature == null) continue; if (allUnits) { ObjectFieldData objMask = new(); UnitData unitMask = new(); if (creature.m_unitData.NpcFlags != 0) unitMask.MarkChanged(m_unitData.NpcFlags); if (creature.m_unitData.NpcFlags2 != 0) unitMask.MarkChanged(m_unitData.NpcFlags2); if (objMask.GetUpdateMask().IsAnySet() || unitMask.GetUpdateMask().IsAnySet()) creature.BuildValuesUpdateForPlayerWithMask(udata, objMask.GetUpdateMask(), unitMask.GetUpdateMask(), this); if (creature.IsQuestGiver()) giverStatusMultiple.QuestGiver.Add(new QuestGiverInfo(visibleObjectGuid, GetQuestDialogStatus(creature))); } else if (onlySpellClicks) { // check if this unit requires quest specific flags if (!creature.HasNpcFlag(NPCFlags.SpellClick)) continue; var clickBounds = Global.ObjectMgr.GetSpellClickInfoMapBounds(creature.GetEntry()); foreach (var spellClickInfo in clickBounds) { if (Global.ConditionMgr.HasConditionsForSpellClickEvent(creature.GetEntry(), spellClickInfo.spellId)) { ObjectFieldData objMask = new(); UnitData unitMask = new(); unitMask.MarkChanged(m_unitData.NpcFlags); // NpcFlags has UNIT_NPC_FLAG_SPELLCLICK creature.BuildValuesUpdateForPlayerWithMask(udata, objMask.GetUpdateMask(), unitMask.GetUpdateMask(), this); break; } } } } } // If as a result of npcflag updates we stop seeing UNIT_NPC_FLAG_QUESTGIVER then // we must also send SMSG_QUEST_GIVER_STATUS_MULTIPLE because client will not request it automatically if (!giverStatusMultiple.QuestGiver.Empty()) SendPacket(giverStatusMultiple); if (udata.HasData()) { udata.BuildPacket(out var packet); SendPacket(packet); } } void SetDailyQuestStatus(uint quest_id) { Quest qQuest = Global.ObjectMgr.GetQuestTemplate(quest_id); if (qQuest != null) { if (!qQuest.IsDFQuest()) { AddDynamicUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.DailyQuestsCompleted), quest_id); m_lastDailyQuestTime = GameTime.GetGameTime(); // last daily quest time m_DailyQuestChanged = true; } else { m_DFQuests.Add(quest_id); m_lastDailyQuestTime = GameTime.GetGameTime(); m_DailyQuestChanged = true; } } } public bool IsDailyQuestDone(uint quest_id) { return m_activePlayerData.DailyQuestsCompleted.FindIndex(quest_id) >= 0; } void SetWeeklyQuestStatus(uint quest_id) { m_weeklyquests.Add(quest_id); m_WeeklyQuestChanged = true; } void SetSeasonalQuestStatus(uint quest_id) { Quest quest = Global.ObjectMgr.GetQuestTemplate(quest_id); if (quest == null) return; if (!m_seasonalquests.ContainsKey(quest.GetEventIdForQuest())) m_seasonalquests[quest.GetEventIdForQuest()] = new(); m_seasonalquests[quest.GetEventIdForQuest()][quest_id] = GameTime.GetGameTime(); m_SeasonalQuestChanged = true; } void SetMonthlyQuestStatus(uint quest_id) { m_monthlyquests.Add(quest_id); m_MonthlyQuestChanged = true; } void PushQuests() { foreach (Quest quest in Global.ObjectMgr.GetQuestTemplatesAutoPush()) { if (quest.GetQuestTag() != 0 && quest.GetQuestTag() != QuestTagType.Tag) continue; if (!quest.IsUnavailable() && CanTakeQuest(quest, false)) AddQuestAndCheckCompletion(quest, null); } } void SendDisplayToast(uint entry, DisplayToastType type, bool isBonusRoll, uint quantity, DisplayToastMethod method, uint questId, Item item = null) { DisplayToast displayToast = new(); displayToast.Quantity = quantity; displayToast.DisplayToastMethod = method; displayToast.QuestID = questId; displayToast.Type = type; switch (type) { case DisplayToastType.NewItem: { if (item == null) return; displayToast.BonusRoll = isBonusRoll; displayToast.Item = new(item); displayToast.LootSpec = 0; // loot spec that was selected when loot was generated (not at loot time) displayToast.Gender = GetNativeGender(); break; } case DisplayToastType.NewCurrency: displayToast.CurrencyID = entry; break; default: break; } SendPacket(displayToast); } } }