Core/Quests Implement quest campaigns

This commit is contained in:
luis
2026-03-11 08:46:19 -03:00
parent 8fdb729d0b
commit 456d69d25a
7 changed files with 226 additions and 11 deletions
@@ -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;
@@ -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);
@@ -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,
+118 -1
View File
@@ -17,20 +17,73 @@
#include "QuestMgr.h"
#include "DB2Stores.h"
#include "MapUtils.h"
#include "ObjectMgr.h"
#include "Player.h"
#include <algorithm>
namespace
{
std::unordered_map<uint32, std::vector<QuestLineXQuestEntry const*>> QuestsByQuestLine;
std::unordered_map<uint32, std::vector<QuestLineXQuestEntry const*>> QuestsByQuestLine;
struct QuestLineData
{
QuestLineXQuestEntry const* QuestLineQuest = nullptr;
std::vector<CampaignEntry const*>* Campaigns = nullptr;
};
std::map<uint32, std::vector<CampaignEntry const*>> CampaignsByQuestLine;
std::unordered_map<uint32, std::vector<QuestLineData>> QuestLineDataByQuest;
struct CampaignQuestLine
{
uint32 CampaignId = 0;
uint32 QuestLineId = 0;
friend std::strong_ordering operator<=>(CampaignQuestLine const& left, CampaignQuestLine const& right) = default;
};
std::vector<CampaignQuestLine> CampaignQuestLines;
struct CampaignQuestLinesSentinel
{
std::vector<CampaignQuestLine>::const_iterator End;
uint32 CampaignId;
friend bool operator==(std::vector<CampaignQuestLine>::const_iterator const& itr, CampaignQuestLinesSentinel const& end)
{
return itr == end.End || itr->CampaignId != end.CampaignId;
}
};
Trinity::IteratorPair<std::vector<CampaignQuestLine>::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<uint32> questIds;
for (CampaignQuestLine const& campaignQuestLine : GetQuestLinesForCampaign(campaignId))
{
std::ptrdiff_t oldSize = std::ssize(questIds);
std::span<QuestLineXQuestEntry const* const> questLineQuests = GetQuestsForQuestLine(campaignQuestLine.QuestLineId);
questIds.resize(oldSize + questLineQuests.size());
std::ranges::transform(questLineQuests, questIds.begin() + oldSize, &QuestLineXQuestEntry::QuestID);
}
player->SkipQuests(questIds);
}
+16 -9
View File
@@ -26,21 +26,28 @@ class Player;
namespace QuestMgr
{
void Load();
void Load();
// QuestLine
TC_GAME_API std::span<QuestLineXQuestEntry const* const> GetQuestsForQuestLine(uint32 questLineId);
// QuestLine
TC_GAME_API std::span<QuestLineXQuestEntry const* const> GetQuestsForQuestLine(uint32 questLineId);
TC_GAME_API bool IsQuestLineQuestAvailableForPlayer(uint32 questLineId, Player const* player);
TC_GAME_API bool IsQuestLineQuestAvailableForPlayer(uint32 questLineId, Player const* player);
TC_GAME_API bool IsQuestLineQuestActiveForPlayer(uint32 questLineId, Player const* player);
TC_GAME_API bool IsQuestLineQuestActiveForPlayer(uint32 questLineId, Player const* player);
TC_GAME_API bool IsQuestLineCompletedByPlayer(uint32 questLineId, Player const* player);
TC_GAME_API bool IsQuestLineCompletedByPlayer(uint32 questLineId, Player const* player);
struct QuestLineStats { uint32 Completed = 0; uint32 Total = 0; };
TC_GAME_API QuestLineStats GetQuestLineStatsForPlayer(uint32 questLineId, Player const* player);
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);
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
+1
View File
@@ -445,6 +445,7 @@ class TC_GAME_API Spell
void EffectLearnAzeriteEssencePower();
void EffectCreatePrivateConversation();
void EffectApplyMountEquipment();
void EffectSkipCampaign();
void EffectSendChatMessage();
void EffectGrantBattlePetExperience();
void EffectLearnTransmogIllusion();
+13 -1
View File
@@ -386,7 +386,7 @@ NonDefaultConstructible<SpellEffectHandlerFn> SpellEffectHandlers[TOTAL_SPELL_EF
&Spell::EffectUnused, //280 SPELL_EFFECT_280
&Spell::EffectNULL, //281 SPELL_EFFECT_LEARN_SOULBIND_CONDUIT
&Spell::EffectConvertItemsToCurrency, //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
@@ -6080,6 +6080,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)