diff --git a/sql/updates/hotfixes/master/2026_03_08_00_hotfixes.sql b/sql/updates/hotfixes/master/2026_03_08_00_hotfixes.sql new file mode 100644 index 0000000000..051623ccf8 --- /dev/null +++ b/sql/updates/hotfixes/master/2026_03_08_00_hotfixes.sql @@ -0,0 +1,59 @@ +-- +-- Table structure for table `campaign` +-- +DROP TABLE IF EXISTS `campaign`; +CREATE TABLE `campaign` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `Title` text, + `Description` text, + `UiTextureKitID` int NOT NULL DEFAULT '0', + `RewardQuestID` int NOT NULL DEFAULT '0', + `Prerequisite` int NOT NULL DEFAULT '0', + `Stalled` int NOT NULL DEFAULT '0', + `Completed` int NOT NULL DEFAULT '0', + `OnlyStallIf` int NOT NULL DEFAULT '0', + `UiQuestDetailsThemeID` int NOT NULL DEFAULT '0', + `Flags` int NOT NULL DEFAULT '0', + `DisplayPriority` int NOT NULL DEFAULT '0', + `SortAsNormalQuest` int NOT NULL DEFAULT '0', + `UseMinimalHeader` int NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `campaign_locale` +-- +DROP TABLE IF EXISTS `campaign_locale`; +CREATE TABLE `campaign_locale` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `locale` varchar(4) NOT NULL, + `Title_lang` text, + `Description_lang` text, + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST COLUMNS(locale) +(PARTITION deDE VALUES IN ('deDE') ENGINE = InnoDB, + PARTITION esES VALUES IN ('esES') ENGINE = InnoDB, + PARTITION esMX VALUES IN ('esMX') ENGINE = InnoDB, + PARTITION frFR VALUES IN ('frFR') ENGINE = InnoDB, + PARTITION itIT VALUES IN ('itIT') ENGINE = InnoDB, + PARTITION koKR VALUES IN ('koKR') ENGINE = InnoDB, + PARTITION ptBR VALUES IN ('ptBR') ENGINE = InnoDB, + PARTITION ruRU VALUES IN ('ruRU') ENGINE = InnoDB, + PARTITION zhCN VALUES IN ('zhCN') ENGINE = InnoDB, + PARTITION zhTW VALUES IN ('zhTW') ENGINE = InnoDB); + +-- +-- Table structure for table `campaign_x_quest_line` +-- +DROP TABLE IF EXISTS `campaign_x_quest_line`; +CREATE TABLE `campaign_x_quest_line` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `CampaignID` int unsigned NOT NULL DEFAULT '0', + `QuestLineID` int unsigned NOT NULL DEFAULT '0', + `OrderIndex` int unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp index 366a171796..c6b7c5e9fc 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.cpp +++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp @@ -298,6 +298,18 @@ void HotfixDatabaseConnection::DoPrepareStatements() " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_BROADCAST_TEXT_DURATION, "SELECT MAX(ID) + 1 FROM broadcast_text_duration", CONNECTION_SYNCH); + // Campaign.db2 + PrepareStatement(HOTFIX_SEL_CAMPAIGN, "SELECT ID, Title, Description, UiTextureKitID, RewardQuestID, Prerequisite, Stalled, Completed, " + "OnlyStallIf, UiQuestDetailsThemeID, Flags, DisplayPriority, SortAsNormalQuest, UseMinimalHeader FROM campaign WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_CAMPAIGN, "SELECT MAX(ID) + 1 FROM campaign", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_CAMPAIGN, "SELECT ID, Title_lang, Description_lang FROM campaign_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + + // CampaignXQuestLine.db2 + PrepareStatement(HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE, "SELECT ID, CampaignID, QuestLineID, OrderIndex FROM campaign_x_quest_line" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE, "SELECT MAX(ID) + 1 FROM campaign_x_quest_line", CONNECTION_SYNCH); + // CfgCategories.db2 PrepareStatement(HOTFIX_SEL_CFG_CATEGORIES, "SELECT ID, Name, LocaleMask, CreateCharsetMask, ExistingCharsetMask, Flags, `Order`" " FROM cfg_categories WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h index c8010c82c7..078c45c64d 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.h +++ b/src/server/database/Database/Implementation/HotfixDatabase.h @@ -188,6 +188,13 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_BROADCAST_TEXT_DURATION, HOTFIX_SEL_BROADCAST_TEXT_DURATION_MAX_ID, + HOTFIX_SEL_CAMPAIGN, + HOTFIX_SEL_CAMPAIGN_MAX_ID, + HOTFIX_SEL_CAMPAIGN_LOCALE, + + HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE, + HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE_MAX_ID, + HOTFIX_SEL_CFG_CATEGORIES, HOTFIX_SEL_CFG_CATEGORIES_MAX_ID, HOTFIX_SEL_CFG_CATEGORIES_LOCALE, diff --git a/src/server/game/Achievements/CriteriaHandler.cpp b/src/server/game/Achievements/CriteriaHandler.cpp index e467ee3dba..0a49db74fd 100644 --- a/src/server/game/Achievements/CriteriaHandler.cpp +++ b/src/server/game/Achievements/CriteriaHandler.cpp @@ -3998,6 +3998,10 @@ bool CriteriaHandler::ModifierSatisfied(ModifierTreeEntry const* modifier, uint6 if (referencePlayer->m_activePlayerData->TimerunningSeasonID != int32(reqValue)) return false; break; + case ModifierTreeType::PlayerHasCompletedCampaign: // 388 + if (!QuestMgr::IsCampaignCompletedByPlayer(reqValue, referencePlayer)) + return false; + break; case ModifierTreeType::TargetCreatureClassificationEqual: // 389 { Creature const* targetCreature = Object::ToCreature(ref); diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h index f2b0feda89..6d78bfb4ca 100644 --- a/src/server/game/DataStores/DB2LoadInfo.h +++ b/src/server/game/DataStores/DB2LoadInfo.h @@ -810,6 +810,42 @@ struct BroadcastTextDurationLoadInfo static constexpr DB2LoadInfo Instance{ Fields, 4, &BroadcastTextDurationMeta::Instance, HOTFIX_SEL_BROADCAST_TEXT_DURATION }; }; +struct CampaignLoadInfo +{ + static constexpr DB2FieldMeta Fields[14] = + { + { .IsSigned = false, .Type = FT_INT, .Name = "ID" }, + { .IsSigned = false, .Type = FT_STRING, .Name = "Title" }, + { .IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + { .IsSigned = true, .Type = FT_INT, .Name = "UiTextureKitID" }, + { .IsSigned = true, .Type = FT_INT, .Name = "RewardQuestID" }, + { .IsSigned = true, .Type = FT_INT, .Name = "Prerequisite" }, + { .IsSigned = true, .Type = FT_INT, .Name = "Stalled" }, + { .IsSigned = true, .Type = FT_INT, .Name = "Completed" }, + { .IsSigned = true, .Type = FT_INT, .Name = "OnlyStallIf" }, + { .IsSigned = true, .Type = FT_INT, .Name = "UiQuestDetailsThemeID" }, + { .IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + { .IsSigned = true, .Type = FT_INT, .Name = "DisplayPriority" }, + { .IsSigned = true, .Type = FT_INT, .Name = "SortAsNormalQuest" }, + { .IsSigned = true, .Type = FT_INT, .Name = "UseMinimalHeader" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 14, &CampaignMeta::Instance, HOTFIX_SEL_CAMPAIGN }; +}; + +struct CampaignXQuestLineLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + { .IsSigned = false, .Type = FT_INT, .Name = "ID" }, + { .IsSigned = false, .Type = FT_INT, .Name = "CampaignID" }, + { .IsSigned = false, .Type = FT_INT, .Name = "QuestLineID" }, + { .IsSigned = false, .Type = FT_INT, .Name = "OrderIndex" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &CampaignXQuestLineMeta::Instance, HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE }; +}; + struct CfgCategoriesLoadInfo { static constexpr DB2FieldMeta Fields[7] = diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp index a960ceb847..15145e7827 100644 --- a/src/server/game/DataStores/DB2Stores.cpp +++ b/src/server/game/DataStores/DB2Stores.cpp @@ -85,6 +85,8 @@ DB2Storage sBattlemasterListStore("Battlema DB2Storage sBattlemasterListXMapStore("BattlemasterListXMap.db2", &BattlemasterListXMapLoadInfo::Instance); DB2Storage sBroadcastTextStore("BroadcastText.db2", &BroadcastTextLoadInfo::Instance); DB2Storage sBroadcastTextDurationStore("BroadcastTextDuration.db2", &BroadcastTextDurationLoadInfo::Instance); +DB2Storage sCampaignStore("Campaign.db2", &CampaignLoadInfo::Instance); +DB2Storage sCampaignXQuestLineStore("CampaignXQuestLine.db2", &CampaignXQuestLineLoadInfo::Instance); DB2Storage sCfgCategoriesStore("Cfg_Categories.db2", &CfgCategoriesLoadInfo::Instance); DB2Storage sCfgRegionsStore("Cfg_Regions.db2", &CfgRegionsLoadInfo::Instance); DB2Storage sChallengeModeItemBonusOverrideStore("ChallengeModeItemBonusOverride.db2", &ChallengeModeItemBonusOverrideLoadInfo::Instance); @@ -714,6 +716,8 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul LOAD_DB2(sBattlemasterListXMapStore); LOAD_DB2(sBroadcastTextStore); LOAD_DB2(sBroadcastTextDurationStore); + LOAD_DB2(sCampaignStore); + LOAD_DB2(sCampaignXQuestLineStore); LOAD_DB2(sCfgCategoriesStore); LOAD_DB2(sCfgRegionsStore); LOAD_DB2(sChallengeModeItemBonusOverrideStore); diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h index be9bdc4a21..0c43a982b5 100644 --- a/src/server/game/DataStores/DB2Stores.h +++ b/src/server/game/DataStores/DB2Stores.h @@ -67,6 +67,8 @@ TC_GAME_API extern DB2Storage sBattlePetSp TC_GAME_API extern DB2Storage sBattlemasterListStore; TC_GAME_API extern DB2Storage sBattlemasterListXMapStore; TC_GAME_API extern DB2Storage sBroadcastTextStore; +TC_GAME_API extern DB2Storage sCampaignStore; +TC_GAME_API extern DB2Storage sCampaignXQuestLineStore; TC_GAME_API extern DB2Storage sCfgCategoriesStore; TC_GAME_API extern DB2Storage sCfgRegionsStore; TC_GAME_API extern DB2Storage sChallengeModeItemBonusOverrideStore; diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h index 54d78c5832..71791ec2d0 100644 --- a/src/server/game/DataStores/DB2Structure.h +++ b/src/server/game/DataStores/DB2Structure.h @@ -571,6 +571,34 @@ struct BroadcastTextDurationEntry uint32 BroadcastTextID; }; +struct CampaignEntry +{ + uint32 ID; + LocalizedString Title; + LocalizedString Description; + int32 UiTextureKitID; + int32 RewardQuestID; + int32 Prerequisite; + int32 Stalled; + int32 Completed; + int32 OnlyStallIf; + int32 UiQuestDetailsThemeID; + int32 Flags; + int32 DisplayPriority; + int32 SortAsNormalQuest; + int32 UseMinimalHeader; + + bool HasFlag(CampaignFlags flag) const { return EnumFlag(static_cast(Flags)).HasFlag(flag); } +}; + +struct CampaignXQuestLineEntry +{ + uint32 ID; + uint32 CampaignID; + uint32 QuestLineID; + uint32 OrderIndex; +}; + struct Cfg_CategoriesEntry { uint32 ID; diff --git a/src/server/game/DataStores/DBCEnums.h b/src/server/game/DataStores/DBCEnums.h index d2346cc62e..d494c170ca 100644 --- a/src/server/game/DataStores/DBCEnums.h +++ b/src/server/game/DataStores/DBCEnums.h @@ -284,6 +284,14 @@ enum class BattlemasterListFlags : uint32 DEFINE_ENUM_FLAG(BattlemasterListFlags); +enum class CampaignFlags : int32 +{ + DontUseJourneyQuestBang = 0x01, + IsContainer = 0x02 +}; + +DEFINE_ENUM_FLAG(CampaignFlags); + enum class CfgCategoriesCharsets : uint8 { Any = 0x00, @@ -2035,7 +2043,7 @@ enum class ModifierTreeType : int32 PlayerHasActiveTraitSubTree = 385, // Player has active trait config with {TraitSubTree} PlayerIsInTimerunningSeason = 386, // Player is timerunning {TimerunningSeason} PlayerIsInSoloRBG = 387, /*NYI*/ // Player is in solo RBG (BG Blitz) - PlayerHasCompletedCampaign = 388, /*NYI*/ // Player has completed campaign "{Campaign}" + PlayerHasCompletedCampaign = 388, // Player has completed campaign "{Campaign}" TargetCreatureClassificationEqual = 389, // Creature classification is {CreatureClassification} PlayerDataElementCharacterBetween = 390, // Player {PlayerDataElementCharacter} is between {#Amount} and {#Amount2} PlayerDataElementAccountBetween = 391, // Player {PlayerDataElementAccount} is between {#Amount} and {#Amount2} diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 30320a0dc3..47f6913988 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -108,6 +108,7 @@ #include "QueryHolder.h" #include "QueryResultStructured.h" #include "QuestDef.h" +#include "QuestMgr.h" #include "QuestObjectiveCriteriaMgr.h" #include "QuestPackets.h" #include "RealmList.h" @@ -16073,6 +16074,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::CovenantCallingRewardCompleteNoPOI : QuestGiverStatus::CovenantCallingRewardCompletePOI; else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY)) result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::LegendaryRewardCompleteNoPOI : QuestGiverStatus::LegendaryRewardCompletePOI; + else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this)) + result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::JourneyRewardCompleteNoPOI : QuestGiverStatus::JourneyRewardCompletePOI; else if (quest->IsDailyOrWeekly()) result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::RepeatableRewardCompleteNoPOI : QuestGiverStatus::RepeatableRewardCompletePOI; else @@ -16087,6 +16090,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const result |= QuestGiverStatus::CovenantCallingReward; else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY)) result |= QuestGiverStatus::LegendaryReward; + else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this)) + result |= QuestGiverStatus::JourneyReward; else if (quest->IsDailyOrWeekly()) result |= QuestGiverStatus::RepeatableReward; else @@ -16130,6 +16135,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const result |= QuestGiverStatus::CovenantCallingQuest; else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY)) result |= isTrivial ? QuestGiverStatus::TrivialLegendaryQuest : QuestGiverStatus::LegendaryQuest; + else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this)) + result |= isTrivial ? QuestGiverStatus::TrivialJourneyQuest : QuestGiverStatus::JourneyQuest; else if (quest->IsDailyOrWeekly()) result |= isTrivial ? QuestGiverStatus::TrivialRepeatableQuest : QuestGiverStatus::RepeatableQuest; else @@ -16139,6 +16146,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const result |= QuestGiverStatus::FutureImportantQuest; else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY)) result |= QuestGiverStatus::FutureLegendaryQuest; + else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this)) + result |= QuestGiverStatus::FutureJourneyQuest; else result |= QuestGiverStatus::Future; } diff --git a/src/server/game/Quests/QuestMgr.cpp b/src/server/game/Quests/QuestMgr.cpp index bff1020c2b..ad0b388cff 100644 --- a/src/server/game/Quests/QuestMgr.cpp +++ b/src/server/game/Quests/QuestMgr.cpp @@ -17,6 +17,7 @@ #include "QuestMgr.h" #include "DB2Stores.h" +#include "MapUtils.h" #include "ObjectMgr.h" #include "Player.h" #include @@ -24,13 +25,65 @@ namespace { std::unordered_map> QuestsByQuestLine; + +struct QuestLineData +{ + QuestLineXQuestEntry const* QuestLineQuest = nullptr; + std::vector* Campaigns = nullptr; +}; +std::map> CampaignsByQuestLine; +std::unordered_map> QuestLineDataByQuest; + +struct CampaignQuestLine +{ + uint32 CampaignId = 0; + uint32 QuestLineId = 0; + + friend std::strong_ordering operator<=>(CampaignQuestLine const& left, CampaignQuestLine const& right) = default; +}; +std::vector CampaignQuestLines; + +struct CampaignQuestLinesSentinel +{ + std::vector::const_iterator End; + uint32 CampaignId; + + friend bool operator==(std::vector::const_iterator const& itr, CampaignQuestLinesSentinel const& end) + { + return itr == end.End || itr->CampaignId != end.CampaignId; + } +}; + +Trinity::IteratorPair::iterator, CampaignQuestLinesSentinel> GetQuestLinesForCampaign(uint32 campaignId) +{ + return Trinity::Containers::MakeIteratorPair( + std::ranges::lower_bound(CampaignQuestLines, campaignId, std::ranges::less(), &CampaignQuestLine::CampaignId), + CampaignQuestLinesSentinel{ .End = CampaignQuestLines.end(), .CampaignId = campaignId }); +} } void QuestMgr::Load() { + for (CampaignXQuestLineEntry const* campaignQuestLine : sCampaignXQuestLineStore) + { + if (CampaignEntry const* campaign = sCampaignStore.LookupEntry(campaignQuestLine->CampaignID)) + { + CampaignsByQuestLine[campaignQuestLine->QuestLineID].push_back(campaign); + CampaignQuestLines.push_back({ .CampaignId = campaignQuestLine->CampaignID, .QuestLineId = campaignQuestLine->QuestLineID }); + } + } + for (QuestLineXQuestEntry const* questLineQuest : sQuestLineXQuestStore) + { QuestsByQuestLine[questLineQuest->QuestLineID].push_back(questLineQuest); + QuestLineData& questLineData = QuestLineDataByQuest[questLineQuest->QuestID].emplace_back(); + questLineData.QuestLineQuest = questLineQuest; + questLineData.Campaigns = Trinity::Containers::MapGetValuePtr(CampaignsByQuestLine, questLineQuest->QuestLineID); + } + + std::ranges::sort(CampaignQuestLines); + for (auto& [_, questLineQuests] : QuestsByQuestLine) std::ranges::sort(questLineQuests, std::ranges::less(), &QuestLineXQuestEntry::OrderIndex); } @@ -99,3 +152,67 @@ void QuestMgr::SkipQuestLineForPlayer(uint32 questLineId, Player* player) std::ranges::transform(questLineQuests, questIds.begin(), &QuestLineXQuestEntry::QuestID); player->SkipQuests(questIds); } + +bool QuestMgr::IsCampaignCompletedByPlayer(uint32 campaignId, Player const* player) +{ + auto questLines = GetQuestLinesForCampaign(campaignId); + if (questLines.begin() == questLines.end()) + return false; + + for (CampaignQuestLine const& campaignQuestLine : questLines) + if (!IsQuestLineCompletedByPlayer(campaignQuestLine.QuestLineId, player)) + return false; + + // all questlines completed + return true; +} + +bool QuestMgr::IsCampaignQuestStatusVisibleForPlayer(uint32 questId, Player const* player) +{ + auto itr = QuestLineDataByQuest.find(questId); + if (itr == QuestLineDataByQuest.end()) + return false; + + for (QuestLineData const& questLineData : itr->second) + { + if (!questLineData.Campaigns) + continue; + + for (CampaignEntry const* campaign : *questLineData.Campaigns) + { + if (campaign->HasFlag(CampaignFlags::DontUseJourneyQuestBang)) + continue; + + if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->Prerequisite)) + continue; + + if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->Stalled)) + continue; + + if (campaign->Completed && ConditionMgr::IsPlayerMeetingCondition(player, campaign->Completed)) + continue; + + if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->OnlyStallIf)) + continue; + + return true; + } + } + + return false; +} + +void QuestMgr::SkipCampaignForPlayer(uint32 campaignId, Player* player) +{ + std::vector questIds; + + for (CampaignQuestLine const& campaignQuestLine : GetQuestLinesForCampaign(campaignId)) + { + std::ptrdiff_t oldSize = std::ssize(questIds); + std::span questLineQuests = GetQuestsForQuestLine(campaignQuestLine.QuestLineId); + questIds.resize(oldSize + questLineQuests.size()); + std::ranges::transform(questLineQuests, questIds.begin() + oldSize, &QuestLineXQuestEntry::QuestID); + } + + player->SkipQuests(questIds); +} diff --git a/src/server/game/Quests/QuestMgr.h b/src/server/game/Quests/QuestMgr.h index f2c9559a57..e99d740cbf 100644 --- a/src/server/game/Quests/QuestMgr.h +++ b/src/server/game/Quests/QuestMgr.h @@ -41,6 +41,13 @@ struct QuestLineStats { uint32 Completed = 0; uint32 Total = 0; }; TC_GAME_API QuestLineStats GetQuestLineStatsForPlayer(uint32 questLineId, Player const* player); TC_GAME_API void SkipQuestLineForPlayer(uint32 questLineId, Player* player); + +// Campaign +TC_GAME_API bool IsCampaignCompletedByPlayer(uint32 campaignId, Player const* player); + +TC_GAME_API bool IsCampaignQuestStatusVisibleForPlayer(uint32 questId, Player const* player); + +TC_GAME_API void SkipCampaignForPlayer(uint32 campaignId, Player* player); } #endif // TRINITYCORE_CAMPAIGN_MGR_H diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h index 605ffbd46d..c161995b33 100644 --- a/src/server/game/Spells/Spell.h +++ b/src/server/game/Spells/Spell.h @@ -445,6 +445,7 @@ class TC_GAME_API Spell void EffectLearnAzeriteEssencePower(); void EffectCreatePrivateConversation(); void EffectApplyMountEquipment(); + void EffectSkipCampaign(); void EffectSendChatMessage(); void EffectGrantBattlePetExperience(); void EffectLearnTransmogIllusion(); diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 0ac893b691..70bf5f8bcc 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -372,7 +372,7 @@ NonDefaultConstructible SpellEffectHandlers[TOTAL_SPELL_EF &Spell::EffectUnused, //280 SPELL_EFFECT_280 &Spell::EffectNULL, //281 SPELL_EFFECT_LEARN_SOULBIND_CONDUIT &Spell::EffectNULL, //282 SPELL_EFFECT_CONVERT_ITEMS_TO_CURRENCY - &Spell::EffectNULL, //283 SPELL_EFFECT_COMPLETE_CAMPAIGN + &Spell::EffectSkipCampaign, //283 SPELL_EFFECT_COMPLETE_CAMPAIGN &Spell::EffectSendChatMessage, //284 SPELL_EFFECT_SEND_CHAT_MESSAGE &Spell::EffectNULL, //285 SPELL_EFFECT_MODIFY_KEYSTONE_2 &Spell::EffectGrantBattlePetExperience, //286 SPELL_EFFECT_GRANT_BATTLEPET_EXPERIENCE @@ -6053,6 +6053,18 @@ void Spell::EffectApplyMountEquipment() playerTarget->SendDirectMessage(applyMountEquipmentResult.Write()); } +void Spell::EffectSkipCampaign() +{ + if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET) + return; + + Player* target = Object::ToPlayer(unitTarget); + if (!target) + return; + + QuestMgr::SkipCampaignForPlayer(effectInfo->MiscValue, target); +} + void Spell::EffectSendChatMessage() { if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)