From d6e7cc52a82cb7e5e97ee73f795d88fc57180b32 Mon Sep 17 00:00:00 2001 From: xinef1 Date: Thu, 20 Feb 2020 01:51:14 +0100 Subject: [PATCH] Various quest system fixes (seasonal quests, timed quests and more) (#18940) - Unify quest status checking function, use dedicated function instead of direct map checks - Fixed seasonal quest chains and ability to complete the same quests rewarded in past - Update area dependent auras on quest status change (they often requires specific quest status) - Send all not stored quest rewards by mail - When casting quest reward spell, check if it is not self casted, if so - use player to cast this spell - Perform full db save on quest reward to prevent data desynchronization - Don't allow to fail completed timed quests, except for quests which are completed right from the start - Don't allow to share pooled quests, if they are not available in the current pool (eg sharing easy dalaran weeklies, stored at alt character) - Remove seasonal quest if rewarded quest is removed - Don't complete whole quest on AreaExplore event, check if there are no more requirements that should be fulfilled - Quests with flag QUEST_SPECIAL_FLAGS_PLAYER_KILL can be only credited in quest zone Closes #18913 Closes #11187 Closes #15279 --- src/server/game/Entities/Player/Player.cpp | 102 +++++++++++++-------- src/server/game/Globals/ObjectMgr.cpp | 20 ++++ src/server/game/Quests/QuestDef.h | 3 +- src/server/game/Spells/SpellInfo.cpp | 8 ++ src/server/game/Spells/SpellInfo.h | 1 + 5 files changed, 97 insertions(+), 37 deletions(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 3d3bd21ff48..eaba98a5b92 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -76,6 +76,7 @@ #include "OutdoorPvPMgr.h" #include "Pet.h" #include "PhasingHandler.h" +#include "PoolMgr.h" #include "QueryCallback.h" #include "QueryHolder.h" #include "QuestDef.h" @@ -14721,7 +14722,7 @@ bool Player::CanCompleteQuest(uint32 quest_id) if (!qInfo) return false; - if (!qInfo->IsRepeatable() && m_RewardedQuests.find(quest_id) != m_RewardedQuests.end()) + if (!qInfo->IsRepeatable() && GetQuestRewardStatus(quest_id)) return false; // not allow re-complete quest // auto complete quest @@ -15122,12 +15123,13 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, SendNewItem(item, quest->RewardItemIdCount[i], true, false, false, false); } else if (quest->IsDFQuest()) - SendItemRetrievalMail(quest->RewardItemId[i], quest->RewardItemIdCount[i]); + SendItemRetrievalMail(itemId, quest->RewardItemIdCount[i]); } } } for (uint8 i = 0; i < QUEST_REWARD_CURRENCY_COUNT; ++i) + { if (quest->RewardCurrencyId[i]) { uint32 rewardCurrencyCount = quest->RewardCurrencyCount[i]; @@ -15138,6 +15140,7 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, ModifyCurrency(quest->RewardCurrencyId[i], rewardCurrencyCount, !quest->IsDFQuest()); } + } if (uint32 skill = quest->GetRewardSkillId()) UpdateSkillPro(skill, 1000, quest->GetRewardSkillPoints()); @@ -15159,10 +15162,10 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, if (log_slot < MAX_QUEST_LOG_SIZE) SetQuestSlot(log_slot, 0); - bool rewarded = (m_RewardedQuests.find(quest_id) != m_RewardedQuests.end()); + bool rewarded = IsQuestRewarded(quest_id) && !quest->IsDFQuest(); // Not give XP in case already completed once repeatable quest - uint32 XP = rewarded && !quest->IsDFQuest() ? 0 : uint32(quest->GetXPReward(this) * sWorld->getRate(RATE_XP_QUEST)); + uint32 XP = rewarded ? 0 : uint32(quest->GetXPReward(this) * sWorld->getRate(RATE_XP_QUEST)); // handle SPELL_AURA_MOD_XP_QUEST_PCT auras Unit::AuraEffectList const& ModXPPctAuras = GetAuraEffectsByType(SPELL_AURA_MOD_XP_QUEST_PCT); @@ -15245,12 +15248,6 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, if (quest->CanIncreaseRewardedQuestCounters()) SetRewardedQuest(quest_id); - // StoreNewItem, mail reward, etc. save data directly to the database - // to prevent exploitable data desynchronisation we save the quest status to the database too - // (to prevent rewarding this quest another time while rewards were already given out) - SQLTransaction trans = SQLTransaction(nullptr); - _SaveQuestStatus(trans); - if (announce) SendQuestReward(quest, questGiver ? questGiver->ToCreature() : nullptr, XP); @@ -15260,7 +15257,7 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, if (quest->GetRewSpellCast() > 0) { SpellInfo const* spellInfo = ASSERT_NOTNULL(sSpellMgr->GetSpellInfo(quest->GetRewSpellCast())); - if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_ON_COMPLETE)) + if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !spellInfo->IsSelfCast() && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_ON_COMPLETE)) { if (Creature* creature = GetMap()->GetCreature(questGiver->GetGUID())) creature->CastSpell(this, quest->GetRewSpellCast(), true); @@ -15271,7 +15268,7 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, else if (quest->GetRewSpell() > 0) { SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(quest->GetRewSpell()); - if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_ON_COMPLETE)) + if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !spellInfo->IsSelfCast() && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_ON_COMPLETE)) { if (Creature* creature = GetMap()->GetCreature(questGiver->GetGUID())) creature->CastSpell(this, quest->GetRewSpell(), true); @@ -15285,6 +15282,9 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST_COUNT); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST, quest->GetQuestId()); + // make full db save + SaveToDB(false); + if (quest->HasFlag(QUEST_FLAGS_FLAGS_PVP)) { pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); @@ -15312,13 +15312,15 @@ void Player::FailQuest(uint32 questId) { if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) { - // Already complete quests shouldn't turn failed. - if (GetQuestStatus(questId) == QUEST_STATUS_COMPLETE && !quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED)) - return; + QuestStatus qStatus = GetQuestStatus(questId); - // You can't fail a quest if you don't have it, or if it's already rewarded. - if (GetQuestStatus(questId) == QUEST_STATUS_NONE || GetQuestStatus(questId) == QUEST_STATUS_REWARDED) - return; + // we can only fail incomplete quest or... + if (qStatus != QUEST_STATUS_INCOMPLETE) + { + // completed timed quest with no requirements + if (qStatus != QUEST_STATUS_COMPLETE || !quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED) || !quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_COMPLETED_AT_START)) + return; + } SetQuestStatus(questId, QUEST_STATUS_FAILED); @@ -15698,7 +15700,7 @@ bool Player::SatisfyQuestExclusiveGroup(Quest const* qInfo, bool msg) const } // alternative quest already started or completed - but don't check rewarded states if both are repeatable - if (GetQuestStatus(exclude_Id) != QUEST_STATUS_NONE || (!(qInfo->IsRepeatable() && Nquest->IsRepeatable()) && (m_RewardedQuests.find(exclude_Id) != m_RewardedQuests.end()))) + if (GetQuestStatus(exclude_Id) != QUEST_STATUS_NONE || (!(qInfo->IsRepeatable() && Nquest->IsRepeatable()) && GetQuestRewardStatus(exclude_Id))) { if (msg) { @@ -15873,7 +15875,7 @@ bool Player::GetQuestRewardStatus(uint32 quest_id) const // for repeatable quests: rewarded field is set after first reward only to prevent getting XP more than once if (!qInfo->IsRepeatable()) - return m_RewardedQuests.find(quest_id) != m_RewardedQuests.end(); + return IsQuestRewarded(quest_id); return false; } @@ -15888,14 +15890,8 @@ QuestStatus Player::GetQuestStatus(uint32 quest_id) const if (itr != m_QuestStatus.end()) return itr->second.Status; - if (Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id)) - { - if (qInfo->IsSeasonal() && !qInfo->IsRepeatable()) - return SatisfyQuestSeasonal(qInfo, false) ? QUEST_STATUS_NONE : QUEST_STATUS_REWARDED; - - if (!qInfo->IsRepeatable() && IsQuestRewarded(quest_id)) - return QUEST_STATUS_REWARDED; - } + if (GetQuestRewardStatus(quest_id)) + return QUEST_STATUS_REWARDED; } return QUEST_STATUS_NONE; } @@ -15903,7 +15899,22 @@ QuestStatus Player::GetQuestStatus(uint32 quest_id) const bool Player::CanShareQuest(uint32 quest_id) const { Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id); - return qInfo && qInfo->HasFlag(QUEST_FLAGS_SHARABLE) && IsActiveQuest(quest_id); + if (qInfo && qInfo->HasFlag(QUEST_FLAGS_SHARABLE)) + { + QuestStatusMap::const_iterator itr = m_QuestStatus.find(quest_id); + if (itr != m_QuestStatus.end()) + { + if (itr->second.Status != QUEST_STATUS_INCOMPLETE) + return false; + + // in pool and not currently available (wintergrasp weekly, dalaran weekly) - can't share + if (sPoolMgr->IsPartOfAPool(quest_id) && !sPoolMgr->IsSpawnedObject(quest_id)) + return false; + + return true; + } + } + return false; } void Player::SetQuestStatus(uint32 questId, QuestStatus status, bool update /*= true*/) @@ -15944,6 +15955,18 @@ void Player::RemoveRewardedQuest(uint32 questId, bool update /*= true*/) m_RewardedQuestsSave[questId] = QUEST_FORCE_DELETE_SAVE_TYPE; } + // Remove seasonal quest also + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questId); + if (qInfo->IsSeasonal()) + { + uint16 eventId = qInfo->GetEventIdForQuest(); + if (m_seasonalquests.find(eventId) != m_seasonalquests.end()) + { + m_seasonalquests[eventId].erase(questId); + m_SeasonalQuestChanged = true; + } + } + if (update) SendQuestUpdate(questId); } @@ -16228,12 +16251,15 @@ void Player::AreaExploredOrEventHappens(uint32 questId) { QuestStatusData& q_status = m_QuestStatus[questId]; - if (!q_status.Explored) + // Dont complete failed quest + if (!q_status.Explored && q_status.Status != QUEST_STATUS_FAILED) { q_status.Explored = true; m_QuestStatusSave[questId] = QUEST_DEFAULT_SAVE_TYPE; - SetQuestSlotState(log_slot, QUEST_STATE_COMPLETE); - SendQuestComplete(qInfo); + + // if we cannot complete quest send exploration succeded (to mark exploration on client) + if (!CanCompleteQuest(questId)) + SendQuestComplete(qInfo); } } if (CanCompleteQuest(questId)) @@ -16433,7 +16459,8 @@ void Player::KilledPlayerCredit(uint16 count) QuestStatusData& q_status = m_QuestStatus[questid]; if (q_status.Status == QUEST_STATUS_INCOMPLETE && (!GetGroup() || !GetGroup()->isRaidGroup() || qInfo->IsAllowedInRaid(GetMap()->GetDifficulty()))) { - if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL)) + // PvP Killing quest require player to be in same zone as quest zone (only 2 quests so no doubt) + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL) && GetZoneId() == static_cast(qInfo->GetZoneOrSort())) { KilledPlayerCreditForQuest(count, qInfo); break; // there is only one quest per zone @@ -20478,7 +20505,7 @@ void Player::_SaveWeeklyQuestStatus(SQLTransaction& trans) void Player::_SaveSeasonalQuestStatus(SQLTransaction& trans) { - if (!m_SeasonalQuestChanged || m_seasonalquests.empty()) + if (!m_SeasonalQuestChanged) return; // we don't need transactions here. @@ -20486,6 +20513,11 @@ void Player::_SaveSeasonalQuestStatus(SQLTransaction& trans) stmt->setUInt32(0, GetGUID().GetCounter()); trans->Append(stmt); + m_SeasonalQuestChanged = false; + + if (m_seasonalquests.empty()) + return; + for (SeasonalEventQuestMap::const_iterator iter = m_seasonalquests.begin(); iter != m_seasonalquests.end(); ++iter) { uint16 eventId = iter->first; @@ -20501,8 +20533,6 @@ void Player::_SaveSeasonalQuestStatus(SQLTransaction& trans) trans->Append(stmt); } } - - m_SeasonalQuestChanged = false; } void Player::_SaveMonthlyQuestStatus(SQLTransaction& trans) diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 2c3aa5b23aa..82d02b7e381 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -4991,6 +4991,26 @@ void ObjectMgr::LoadQuests() qinfo->SetSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED); if (qinfo->_requiredPlayerKills) qinfo->SetSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL); + + // Special flag to determine if quest is completed from the start, used to determine if we can fail timed quest if it is completed + if (!qinfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_KILL | QUEST_SPECIAL_FLAGS_CAST | QUEST_SPECIAL_FLAGS_SPEAKTO | QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT)) + { + bool addFlag = true; + if (qinfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + { + for (uint8 j = 0; j < QUEST_ITEM_OBJECTIVES_COUNT; ++j) + { + if (qinfo->RequiredItemId[j] != 0 && (qinfo->RequiredItemId[j] != qinfo->GetSrcItemId() || qinfo->RequiredItemCount[j] > qinfo->GetSrcItemCount())) + { + addFlag = false; + break; + } + } + } + + if (addFlag) + qinfo->SetSpecialFlag(QUEST_SPECIAL_FLAGS_COMPLETED_AT_START); + } } // check QUEST_SPECIAL_FLAGS_EXPLORATION_OR_EVENT for spell with SPELL_EFFECT_QUEST_COMPLETE diff --git a/src/server/game/Quests/QuestDef.h b/src/server/game/Quests/QuestDef.h index f1565e3902d..95d301fa2bb 100644 --- a/src/server/game/Quests/QuestDef.h +++ b/src/server/game/Quests/QuestDef.h @@ -186,7 +186,8 @@ enum QuestSpecialFlags QUEST_SPECIAL_FLAGS_SPEAKTO = 0x100, // Internal flag computed only QUEST_SPECIAL_FLAGS_KILL = 0x200, // Internal flag computed only QUEST_SPECIAL_FLAGS_TIMED = 0x400, // Internal flag computed only - QUEST_SPECIAL_FLAGS_PLAYER_KILL = 0x800 // Internal flag computed only + QUEST_SPECIAL_FLAGS_PLAYER_KILL = 0x800, // Internal flag computed only + QUEST_SPECIAL_FLAGS_COMPLETED_AT_START = 0x1000 // Internal flag computed only }; enum QuestStatusFlags diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index 854019ec3c7..9746e276b68 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -1243,6 +1243,14 @@ bool SpellInfo::NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) con return false; } +bool SpellInfo::IsSelfCast() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Effect && Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER) + return false; + return true; +} + bool SpellInfo::IsPassive() const { return HasAttribute(SPELL_ATTR0_PASSIVE); diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index 98404ba7919..d279f62aa00 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -487,6 +487,7 @@ class TC_GAME_API SpellInfo bool IsTargetingArea() const; bool NeedsExplicitUnitTarget() const; bool NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) const; + bool IsSelfCast() const; bool IsPassive() const; bool IsRaidMarker() const;