diff options
author | Shauren <shauren.trinity@gmail.com> | 2025-06-22 21:56:58 +0200 |
---|---|---|
committer | Shauren <shauren.trinity@gmail.com> | 2025-06-22 21:56:58 +0200 |
commit | e59059e1bd2f67691e2da0105771b0cb55b123a4 (patch) | |
tree | 4f4864f8aae63eeafac29f440e0ce64a027c4108 | |
parent | 7ca6b226a7420ff38e3a4f17a3758393d68629e3 (diff) |
Core/Players: PlayerChoice improvements
* Add missing choice properties to database (InfiniteRange, ShowChoicesAsList)
* Allow limiiting the number of responses sent at the same time
* Fixed duration sent in SMSG_DISPLAY_PLAYER_CHOICE
* Remove dynamically generated response identifiers from database
* Remove auto rewarding choice responses
* Change response scripts to be bound to scriptname
-rw-r--r-- | sql/updates/world/master/2025_06_22_00_world.sql | 11 | ||||
-rw-r--r-- | src/server/game/Conditions/ConditionMgr.cpp | 1 | ||||
-rw-r--r-- | src/server/game/Entities/Creature/GossipDef.cpp | 11 | ||||
-rw-r--r-- | src/server/game/Entities/Creature/GossipDef.h | 40 | ||||
-rw-r--r-- | src/server/game/Entities/Player/Player.cpp | 68 | ||||
-rw-r--r-- | src/server/game/Globals/ObjectMgr.cpp | 185 | ||||
-rw-r--r-- | src/server/game/Globals/ObjectMgr.h | 93 | ||||
-rw-r--r-- | src/server/game/Globals/PlayerChoice.h | 133 | ||||
-rw-r--r-- | src/server/game/Handlers/QuestHandler.cpp | 82 | ||||
-rw-r--r-- | src/server/game/Scripting/ScriptMgr.cpp | 30 | ||||
-rw-r--r-- | src/server/game/Scripting/ScriptMgr.h | 27 | ||||
-rw-r--r-- | src/server/game/Server/Packets/QuestPackets.cpp | 8 | ||||
-rw-r--r-- | src/server/game/Server/Packets/QuestPackets.h | 8 | ||||
-rw-r--r-- | src/server/game/Server/WorldSession.h | 2 |
14 files changed, 421 insertions, 278 deletions
diff --git a/sql/updates/world/master/2025_06_22_00_world.sql b/sql/updates/world/master/2025_06_22_00_world.sql new file mode 100644 index 00000000000..659c7c7773b --- /dev/null +++ b/sql/updates/world/master/2025_06_22_00_world.sql @@ -0,0 +1,11 @@ +ALTER TABLE `playerchoice` + MODIFY `Duration` bigint NULL DEFAULT 0 AFTER `CloseSoundKitId` + ADD `InfiniteRange` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `Question`, + MODIFY `HideWarboardHeader` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `Question`, + MODIFY `KeepOpenAfterChoice` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `HideWarboardHeader`, + ADD `ShowChoicesAsList` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `KeepOpenAfterChoice`, + ADD `ForceDontShowChoicesAsList` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `ShowChoicesAsList`, + ADD `MaxResponses` int unsigned DEFAULT NULL AFTER `ForceDontShowChoicesAsList`, + ADD `ScriptName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL AFTER `MaxResponses`; + +ALTER TABLE `playerchoice_response` DROP `ResponseIdentifier`; diff --git a/src/server/game/Conditions/ConditionMgr.cpp b/src/server/game/Conditions/ConditionMgr.cpp index 758586c1876..213d8c02418 100644 --- a/src/server/game/Conditions/ConditionMgr.cpp +++ b/src/server/game/Conditions/ConditionMgr.cpp @@ -43,6 +43,7 @@ #include "Pet.h" #include "PhasingHandler.h" #include "Player.h" +#include "PlayerChoice.h" #include "RaceMask.h" #include "RealmList.h" #include "ReputationMgr.h" diff --git a/src/server/game/Entities/Creature/GossipDef.cpp b/src/server/game/Entities/Creature/GossipDef.cpp index 20390ce2833..75fc7f8ce87 100644 --- a/src/server/game/Entities/Creature/GossipDef.cpp +++ b/src/server/game/Entities/Creature/GossipDef.cpp @@ -353,6 +353,17 @@ bool QuestMenu::HasItem(uint32 questId) const return advstd::ranges::contains(_questMenuItems, questId, &QuestMenuItem::QuestId); } +Optional<uint32> PlayerChoiceData::FindIdByClientIdentifier(uint16 clientIdentifier) const +{ + auto itr = std::ranges::find(_responses, clientIdentifier, &Response::ClientIdentifier); + return itr != _responses.end() ? itr->Id : Optional<uint32>(); +} + +void PlayerChoiceData::AddResponse(uint32 id, uint16 clientIdentifier) +{ + _responses.push_back({ .Id = id, .ClientIdentifier = clientIdentifier }); +} + void QuestMenu::ClearMenu() { _questMenuItems.clear(); diff --git a/src/server/game/Entities/Creature/GossipDef.h b/src/server/game/Entities/Creature/GossipDef.h index b596a3ec5ed..da83b064300 100644 --- a/src/server/game/Entities/Creature/GossipDef.h +++ b/src/server/game/Entities/Creature/GossipDef.h @@ -19,6 +19,7 @@ #define TRINITYCORE_GOSSIP_H #include "Common.h" +#include "Duration.h" #include "ObjectGuid.h" #include "Optional.h" #include <variant> @@ -232,6 +233,33 @@ class TC_GAME_API QuestMenu QuestMenuItemList _questMenuItems; }; +class PlayerChoiceData +{ +public: + PlayerChoiceData() = default; + explicit PlayerChoiceData(uint32 choiceId) : _choiceId(choiceId) { } + + uint32 GetChoiceId() const { return _choiceId; } + void SetChoiceId(uint32 choiceId) { _choiceId = choiceId; } + + Optional<uint32> FindIdByClientIdentifier(uint16 clientIdentifier) const; + void AddResponse(uint32 id, uint16 clientIdentifier); + + Optional<SystemTimePoint> GetExpireTime() const { return _expireTime; } + void SetExpireTime(Optional<SystemTimePoint> expireTime) { _expireTime = expireTime; } + +private: + struct Response + { + uint32 Id = 0; + uint16 ClientIdentifier = 0; + }; + + uint32 _choiceId = 0; + std::vector<Response> _responses; + Optional<SystemTimePoint> _expireTime; +}; + class InteractionData { template <typename> @@ -246,9 +274,6 @@ class InteractionData struct TrainerTag; using TrainerData = TaggedId<TrainerTag>; - struct PlayerChoiceTag; - using PlayerChoiceData = TaggedId<PlayerChoiceTag>; - public: void Reset() { @@ -262,12 +287,19 @@ public: Optional<uint32> GetTrainerId() const { return std::holds_alternative<TrainerData>(_data) ? std::get<TrainerData>(_data).Id : Optional<uint32>(); } void SetTrainerId(uint32 trainerId) { _data.emplace<TrainerData>(trainerId); } - Optional<uint32> GetPlayerChoiceId() const { return std::holds_alternative<TrainerData>(_data) ? std::get<PlayerChoiceData>(_data).Id : Optional<uint32>(); } + PlayerChoiceData* GetPlayerChoice() { return std::holds_alternative<PlayerChoiceData>(_data) ? &std::get<PlayerChoiceData>(_data) : nullptr; } void SetPlayerChoice(uint32 choiceId) { _data.emplace<PlayerChoiceData>(choiceId); } + uint16 AddPlayerChoiceResponse(uint32 responseId) + { + std::get<PlayerChoiceData>(_data).AddResponse(responseId, ++_playerChoiceResponseIdentifierGenerator); + return _playerChoiceResponseIdentifierGenerator; + } + bool IsLaunchedByQuest = false; private: + uint16 _playerChoiceResponseIdentifierGenerator = 0; // not reset between interactions std::variant<std::monostate, TrainerData, PlayerChoiceData> _data; }; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 6e37b039fe1..e1fc38f9ab4 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -102,6 +102,7 @@ #include "PoolMgr.h" #include "PetitionMgr.h" #include "PhasingHandler.h" +#include "PlayerChoice.h" #include "QueryCallback.h" #include "QueryHolder.h" #include "QuestDef.h" @@ -29793,16 +29794,22 @@ void Player::SendPlayerChoice(ObjectGuid sender, int32 choiceId) displayPlayerChoice.ChoiceID = choiceId; displayPlayerChoice.UiTextureKitID = playerChoice->UiTextureKitId; displayPlayerChoice.SoundKitID = playerChoice->SoundKitId; + displayPlayerChoice.CloseUISoundKitID = playerChoice->CloseSoundKitId; + if (playerChoice->Duration > 0s) + displayPlayerChoice.ExpireTime = GameTime::GetSystemTime() + playerChoice->Duration; + displayPlayerChoice.Question = playerChoice->Question; if (playerChoiceLocale) ObjectMgr::GetLocaleString(playerChoiceLocale->Question, locale, displayPlayerChoice.Question); displayPlayerChoice.Responses.reserve(playerChoice->Responses.size()); - displayPlayerChoice.InfiniteRange = false; + displayPlayerChoice.InfiniteRange = playerChoice->InfiniteRange; displayPlayerChoice.HideWarboardHeader = playerChoice->HideWarboardHeader; displayPlayerChoice.KeepOpenAfterChoice = playerChoice->KeepOpenAfterChoice; + displayPlayerChoice.ShowChoicesAsList = playerChoice->ShowChoicesAsList; + displayPlayerChoice.ForceDontShowChoicesAsList = playerChoice->ForceDontShowChoicesAsList; - for (std::size_t i = 0; i < playerChoice->Responses.size(); ++i) + for (std::size_t i = 0; i < playerChoice->Responses.size() && (!playerChoice->MaxResponses || displayPlayerChoice.Responses.size() < *playerChoice->MaxResponses); ++i) { PlayerChoiceResponse const& playerChoiceResponseTemplate = playerChoice->Responses[i]; if (!sConditionMgr->IsObjectMeetingPlayerChoiceResponseConditions(choiceId, playerChoiceResponseTemplate.ResponseId, this)) @@ -29810,9 +29817,9 @@ void Player::SendPlayerChoice(ObjectGuid sender, int32 choiceId) WorldPackets::Quest::PlayerChoiceResponse& playerChoiceResponse = displayPlayerChoice.Responses.emplace_back(); playerChoiceResponse.ResponseID = playerChoiceResponseTemplate.ResponseId; - playerChoiceResponse.ResponseIdentifier = playerChoiceResponseTemplate.ResponseIdentifier; + playerChoiceResponse.ResponseIdentifier = PlayerTalkClass->GetInteractionData().AddPlayerChoiceResponse(playerChoiceResponseTemplate.ResponseId); playerChoiceResponse.ChoiceArtFileID = playerChoiceResponseTemplate.ChoiceArtFileId; - playerChoiceResponse.Flags = playerChoiceResponseTemplate.Flags; + playerChoiceResponse.Flags = playerChoiceResponseTemplate.Flags.AsUnderlyingType(); playerChoiceResponse.WidgetSetID = playerChoiceResponseTemplate.WidgetSetID; playerChoiceResponse.UiTextureAtlasElementID = playerChoiceResponseTemplate.UiTextureAtlasElementID; playerChoiceResponse.SoundKitID = playerChoiceResponseTemplate.SoundKitID; @@ -29848,40 +29855,31 @@ void Player::SendPlayerChoice(ObjectGuid sender, int32 choiceId) playerChoiceResponse.Reward->HonorPointCount = playerChoiceResponseTemplate.Reward->HonorPointCount; playerChoiceResponse.Reward->Money = playerChoiceResponseTemplate.Reward->Money; playerChoiceResponse.Reward->Xp = playerChoiceResponseTemplate.Reward->Xp; - for (PlayerChoiceResponseRewardItem const& item : playerChoiceResponseTemplate.Reward->Items) - { - WorldPackets::Quest::PlayerChoiceResponseRewardEntry& rewardEntry = playerChoiceResponse.Reward->Items.emplace_back(); - rewardEntry.Item.ItemID = item.Id; - rewardEntry.Quantity = item.Quantity; - if (!item.BonusListIDs.empty()) - { - rewardEntry.Item.ItemBonus.emplace(); - rewardEntry.Item.ItemBonus->BonusListIDs = item.BonusListIDs; - } - } - for (PlayerChoiceResponseRewardEntry const& currency : playerChoiceResponseTemplate.Reward->Currency) - { - WorldPackets::Quest::PlayerChoiceResponseRewardEntry& rewardEntry = playerChoiceResponse.Reward->Items.emplace_back(); - rewardEntry.Item.ItemID = currency.Id; - rewardEntry.Quantity = currency.Quantity; - } - for (PlayerChoiceResponseRewardEntry const& faction : playerChoiceResponseTemplate.Reward->Faction) - { - WorldPackets::Quest::PlayerChoiceResponseRewardEntry& rewardEntry = playerChoiceResponse.Reward->Items.emplace_back(); - rewardEntry.Item.ItemID = faction.Id; - rewardEntry.Quantity = faction.Quantity; - } - for (PlayerChoiceResponseRewardItem const& item : playerChoiceResponseTemplate.Reward->ItemChoices) + + auto fillRewardItems = []<typename Src>(std::vector<Src> const& src, std::vector<WorldPackets::Quest::PlayerChoiceResponseRewardEntry>& dest) { - WorldPackets::Quest::PlayerChoiceResponseRewardEntry& rewardEntry = playerChoiceResponse.Reward->ItemChoices.emplace_back(); - rewardEntry.Item.ItemID = item.Id; - rewardEntry.Quantity = item.Quantity; - if (!item.BonusListIDs.empty()) + dest.resize(src.size()); + for (std::size_t j = 0; j < src.size(); ++j) { - rewardEntry.Item.ItemBonus.emplace(); - rewardEntry.Item.ItemBonus->BonusListIDs = item.BonusListIDs; + Src const& rewardEntryTemplate = src[j]; + WorldPackets::Quest::PlayerChoiceResponseRewardEntry& rewardEntry = dest[j]; + rewardEntry.Item.ItemID = rewardEntryTemplate.Id; + rewardEntry.Quantity = rewardEntryTemplate.Quantity; + if constexpr (std::is_same_v<Src, PlayerChoiceResponseRewardItem>) + { + if (!rewardEntryTemplate.BonusListIDs.empty()) + { + rewardEntry.Item.ItemBonus.emplace(); + rewardEntry.Item.ItemBonus->BonusListIDs = rewardEntryTemplate.BonusListIDs; + } + } } - } + }; + + fillRewardItems(playerChoiceResponseTemplate.Reward->Items, playerChoiceResponse.Reward->Items); + fillRewardItems(playerChoiceResponseTemplate.Reward->Currency, playerChoiceResponse.Reward->Currencies); + fillRewardItems(playerChoiceResponseTemplate.Reward->Faction, playerChoiceResponse.Reward->Factions); + fillRewardItems(playerChoiceResponseTemplate.Reward->ItemChoices, playerChoiceResponse.Reward->ItemChoices); } playerChoiceResponse.RewardQuestID = playerChoiceResponseTemplate.RewardQuestID; diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 90e05a9ec13..0687ec39a51 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -52,7 +52,9 @@ #include "ObjectDefines.h" #include "PhasingHandler.h" #include "Player.h" +#include "PlayerChoice.h" #include "QueryPackets.h" +#include "QueryResultStructured.h" #include "QuestDef.h" #include "Random.h" #include "RealmList.h" @@ -11151,9 +11153,8 @@ void ObjectMgr::LoadPlayerChoices() uint32 oldMSTime = getMSTime(); _playerChoices.clear(); - // 0 1 2 3 4 5 6 7 8 - QueryResult choices = WorldDatabase.Query("SELECT ChoiceId, UiTextureKitId, SoundKitId, CloseSoundKitId, Duration, Question, PendingChoiceText, HideWarboardHeader, KeepOpenAfterChoice FROM playerchoice"); - + QueryResult choices = WorldDatabase.Query("SELECT ChoiceId, UiTextureKitId, SoundKitId, CloseSoundKitId, Duration, Question, PendingChoiceText, " + "InfiniteRange, HideWarboardHeader, KeepOpenAfterChoice, ShowChoicesAsList, ForceDontShowChoicesAsList, MaxResponses, ScriptName FROM playerchoice"); if (!choices) { TC_LOG_INFO("server.loading", ">> Loaded 0 player choices. DB table `playerchoice` is empty."); @@ -11168,37 +11169,48 @@ void ObjectMgr::LoadPlayerChoices() uint32 itemChoiceRewardCount = 0; uint32 mawPowersCount = 0; - do { - Field* fields = choices->Fetch(); + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(UiTextureKitId)(SoundKitId)(CloseSoundKitId)(Duration)(Question)(PendingChoiceText) + (InfiniteRange)(HideWarboardHeader)(KeepOpenAfterChoice)(ShowChoicesAsList)(ForceDontShowChoicesAsList)(MaxResponses)(ScriptName)) fields { .Result = *choices }; + + do + { + int32 choiceId = fields.ChoiceId().GetInt32(); - int32 choiceId = fields[0].GetInt32(); + PlayerChoice& choice = _playerChoices[choiceId]; + choice.ChoiceId = choiceId; + choice.UiTextureKitId = fields.UiTextureKitId().GetInt32(); + choice.SoundKitId = fields.SoundKitId().GetUInt32(); + choice.CloseSoundKitId = fields.CloseSoundKitId().GetUInt32(); + if (!fields.Duration().IsNull()) + choice.Duration = Seconds(fields.Duration().GetInt64()); + choice.Question = fields.Question().GetStringView(); + choice.PendingChoiceText = fields.PendingChoiceText().GetStringView(); + choice.InfiniteRange = fields.InfiniteRange().GetBool(); + choice.HideWarboardHeader = fields.HideWarboardHeader().GetBool(); + choice.KeepOpenAfterChoice = fields.KeepOpenAfterChoice().GetBool(); + choice.ShowChoicesAsList = fields.ShowChoicesAsList().GetBool(); + choice.ForceDontShowChoicesAsList = fields.ForceDontShowChoicesAsList().GetBool(); - PlayerChoice& choice = _playerChoices[choiceId]; - choice.ChoiceId = choiceId; - choice.UiTextureKitId = fields[1].GetInt32(); - choice.SoundKitId = fields[2].GetUInt32(); - choice.CloseSoundKitId = fields[3].GetUInt32(); - choice.Duration = fields[4].GetInt64(); - choice.Question = fields[5].GetString(); - choice.PendingChoiceText = fields[6].GetString(); - choice.HideWarboardHeader = fields[7].GetBool(); - choice.KeepOpenAfterChoice = fields[8].GetBool(); + if (!fields.MaxResponses().IsNull()) + choice.MaxResponses = fields.MaxResponses().GetUInt32(); - } while (choices->NextRow()); + choice.ScriptId = GetScriptId(fields.ScriptName().GetString()); - // 0 1 2 3 4 5 - if (QueryResult responses = WorldDatabase.Query("SELECT ChoiceId, ResponseId, ResponseIdentifier, ChoiceArtFileId, Flags, WidgetSetID, " - // 6 7 8 9 10 11 12 13 14 15 16 + } while (choices->NextRow()); + } + + if (QueryResult responses = WorldDatabase.Query("SELECT ChoiceId, ResponseId, NULL, ChoiceArtFileId, Flags, WidgetSetID, " "UiTextureAtlasElementID, SoundKitID, GroupID, UiTextureKitID, Answer, Header, SubHeader, ButtonTooltip, Description, Confirmation, RewardQuestID " "FROM playerchoice_response ORDER BY `Index` ASC")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(ChoiceArtFileId)(Flags)(WidgetSetID)(UiTextureAtlasElementID)(SoundKitID) + (GroupID)(UiTextureKitID)(Answer)(Header)(SubHeader)(ButtonTooltip)(Description)(Confirmation)(RewardQuestID)) fields{ .Result = *responses }; + do { - Field* fields = responses->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11211,22 +11223,21 @@ void ObjectMgr::LoadPlayerChoices() PlayerChoiceResponse& response = choice->Responses.back(); response.ResponseId = responseId; - response.ResponseIdentifier = fields[2].GetUInt16(); - response.ChoiceArtFileId = fields[3].GetInt32(); - response.Flags = fields[4].GetInt32(); - response.WidgetSetID = fields[5].GetUInt32(); - response.UiTextureAtlasElementID = fields[6].GetUInt32(); - response.SoundKitID = fields[7].GetUInt32(); - response.GroupID = fields[8].GetUInt8(); - response.UiTextureKitID = fields[9].GetInt32(); - response.Answer = fields[10].GetString(); - response.Header = fields[11].GetString(); - response.SubHeader = fields[12].GetString(); - response.ButtonTooltip = fields[13].GetString(); - response.Description = fields[14].GetString(); - response.Confirmation = fields[15].GetString(); - if (!fields[16].IsNull()) - response.RewardQuestID = fields[16].GetUInt32(); + response.ChoiceArtFileId = fields.ChoiceArtFileId().GetInt32(); + response.Flags = static_cast<PlayerChoiceResponseFlags>(fields.Flags().GetInt32()); + response.WidgetSetID = fields.WidgetSetID().GetUInt32(); + response.UiTextureAtlasElementID = fields.UiTextureAtlasElementID().GetUInt32(); + response.SoundKitID = fields.SoundKitID().GetUInt32(); + response.GroupID = fields.GroupID().GetUInt8(); + response.UiTextureKitID = fields.UiTextureKitID().GetInt32(); + response.Answer = fields.Answer().GetStringView(); + response.Header = fields.Header().GetStringView(); + response.SubHeader = fields.SubHeader().GetStringView(); + response.ButtonTooltip = fields.ButtonTooltip().GetStringView(); + response.Description = fields.Description().GetStringView(); + response.Confirmation = fields.Confirmation().GetStringView(); + if (!fields.RewardQuestID().IsNull()) + response.RewardQuestID = fields.RewardQuestID().GetUInt32(); ++responseCount; @@ -11235,12 +11246,13 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult rewards = WorldDatabase.Query("SELECT ChoiceId, ResponseId, TitleId, PackageId, SkillLineId, SkillPointCount, ArenaPointCount, HonorPointCount, Money, Xp FROM playerchoice_response_reward")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(TitleId)(PackageId)(SkillLineId) + (SkillPointCount)(ArenaPointCount)(HonorPointCount)(Money)(Xp)) fields{ .Result = *rewards }; + do { - Field* fields = rewards->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11257,14 +11269,14 @@ void ObjectMgr::LoadPlayerChoices() } PlayerChoiceResponseReward* reward = &responseItr->Reward.emplace(); - reward->TitleId = fields[2].GetInt32(); - reward->PackageId = fields[3].GetInt32(); - reward->SkillLineId = fields[4].GetInt32(); - reward->SkillPointCount = fields[5].GetUInt32(); - reward->ArenaPointCount = fields[6].GetUInt32(); - reward->HonorPointCount = fields[7].GetUInt32(); - reward->Money = fields[8].GetUInt64(); - reward->Xp = fields[9].GetUInt32(); + reward->TitleId = fields.TitleId().GetInt32(); + reward->PackageId = fields.PackageId().GetInt32(); + reward->SkillLineId = fields.SkillLineId().GetInt32(); + reward->SkillPointCount = fields.SkillPointCount().GetUInt32(); + reward->ArenaPointCount = fields.ArenaPointCount().GetUInt32(); + reward->HonorPointCount = fields.HonorPointCount().GetUInt32(); + reward->Money = fields.Money().GetUInt64(); + reward->Xp = fields.Xp().GetUInt32(); ++rewardCount; if (reward->TitleId && !sCharTitlesStore.LookupEntry(reward->TitleId)) @@ -11294,18 +11306,18 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult rewards = WorldDatabase.Query("SELECT ChoiceId, ResponseId, ItemId, BonusListIDs, Quantity FROM playerchoice_response_reward_item ORDER BY `Index` ASC")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(ItemId)(BonusListIDs)(Quantity)) fields { .Result = *rewards }; + do { - Field* fields = rewards->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); - uint32 itemId = fields[2].GetUInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); + uint32 itemId = fields.ItemId().GetUInt32(); std::vector<int32> bonusListIds; - for (std::string_view token : Trinity::Tokenize(fields[3].GetStringView(), ' ', false)) + for (std::string_view token : Trinity::Tokenize(fields.BonusListIDs().GetStringView(), ' ', false)) if (Optional<int32> bonusListID = Trinity::StringTo<int32>(token)) bonusListIds.push_back(*bonusListID); - int32 quantity = fields[4].GetInt32(); + int32 quantity = fields.Quantity().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11343,14 +11355,14 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult rewards = WorldDatabase.Query("SELECT ChoiceId, ResponseId, CurrencyId, Quantity FROM playerchoice_response_reward_currency ORDER BY `Index` ASC")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(CurrencyId)(Quantity)) fields { .Result = *rewards }; + do { - Field* fields = rewards->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); - uint32 currencyId = fields[2].GetUInt32(); - int32 quantity = fields[3].GetInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); + uint32 currencyId = fields.CurrencyId().GetUInt32(); + int32 quantity = fields.Quantity().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11388,14 +11400,14 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult rewards = WorldDatabase.Query("SELECT ChoiceId, ResponseId, FactionId, Quantity FROM playerchoice_response_reward_faction ORDER BY `Index` ASC")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(FactionId)(Quantity)) fields { .Result = *rewards }; + do { - Field* fields = rewards->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); - uint32 factionId = fields[2].GetUInt32(); - int32 quantity = fields[3].GetInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); + uint32 factionId = fields.FactionId().GetUInt32(); + int32 quantity = fields.Quantity().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11433,18 +11445,18 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult rewards = WorldDatabase.Query("SELECT ChoiceId, ResponseId, ItemId, BonusListIDs, Quantity FROM playerchoice_response_reward_item_choice ORDER BY `Index` ASC")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(ItemId)(BonusListIDs)(Quantity)) fields { .Result = *rewards }; + do { - Field* fields = rewards->Fetch(); - - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); - uint32 itemId = fields[2].GetUInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); + uint32 itemId = fields.ItemId().GetUInt32(); std::vector<int32> bonusListIds; - for (std::string_view token : Trinity::Tokenize(fields[3].GetStringView(), ' ', false)) + for (std::string_view token : Trinity::Tokenize(fields.BonusListIDs().GetStringView(), ' ', false)) if (Optional<int32> bonusListID = Trinity::StringTo<int32>(token)) bonusListIds.push_back(*bonusListID); - int32 quantity = fields[4].GetInt32(); + int32 quantity = fields.Quantity().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11482,11 +11494,12 @@ void ObjectMgr::LoadPlayerChoices() if (QueryResult mawPowersResult = WorldDatabase.Query("SELECT ChoiceId, ResponseId, TypeArtFileID, Rarity, SpellID, MaxStacks FROM playerchoice_response_maw_power")) { + DEFINE_FIELD_ACCESSOR_CACHE_ANONYMOUS(ResultSet, (ChoiceId)(ResponseId)(TypeArtFileID)(Rarity)(SpellID)(MaxStacks)) fields { .Result = *mawPowersResult }; + do { - Field* fields = mawPowersResult->Fetch(); - int32 choiceId = fields[0].GetInt32(); - int32 responseId = fields[1].GetInt32(); + int32 choiceId = fields.ChoiceId().GetInt32(); + int32 responseId = fields.ResponseId().GetInt32(); PlayerChoice* choice = Trinity::Containers::MapGetValuePtr(_playerChoices, choiceId); if (!choice) @@ -11503,11 +11516,11 @@ void ObjectMgr::LoadPlayerChoices() } PlayerChoiceResponseMawPower& mawPower = responseItr->MawPower.emplace(); - mawPower.TypeArtFileID = fields[2].GetInt32(); - if (!fields[3].IsNull()) - mawPower.Rarity = fields[3].GetInt32(); - mawPower.SpellID = fields[4].GetInt32(); - mawPower.MaxStacks = fields[5].GetInt32(); + mawPower.TypeArtFileID = fields.TypeArtFileID().GetInt32(); + if (!fields.Rarity().IsNull()) + mawPower.Rarity = fields.Rarity().GetInt32(); + mawPower.SpellID = fields.SpellID().GetInt32(); + mawPower.MaxStacks = fields.MaxStacks().GetInt32(); ++mawPowersCount; diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 491e1b92679..d989bf18df3 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -30,6 +30,7 @@ #include "ObjectDefines.h" #include "ObjectGuid.h" #include "ObjectGuidSequenceGenerator.h" +#include "PlayerChoice.h" #include "Position.h" #include "QuestDef.h" #include "RaceMask.h" @@ -52,6 +53,7 @@ enum class GossipOptionNpc : uint8; struct AccessRequirement; struct DeclinedName; struct FactionEntry; +struct PlayerChoice; struct PlayerInfo; struct PlayerLevelInfo; struct SkillRaceClassInfoEntry; @@ -869,97 +871,6 @@ typedef std::unordered_map<uint32, SceneTemplate> SceneTemplateContainer; typedef std::unordered_map<uint32, std::string> PhaseNameContainer; -struct PlayerChoiceResponseRewardItem -{ - PlayerChoiceResponseRewardItem() : Id(0), Quantity(0) { } - PlayerChoiceResponseRewardItem(uint32 id, std::vector<int32> bonusListIDs, int32 quantity) : Id(id), BonusListIDs(std::move(bonusListIDs)), Quantity(quantity) { } - - uint32 Id; - std::vector<int32> BonusListIDs; - int32 Quantity; -}; - -struct PlayerChoiceResponseRewardEntry -{ - PlayerChoiceResponseRewardEntry() : Id(0), Quantity(0) { } - PlayerChoiceResponseRewardEntry(uint32 id, int32 quantity) : Id(id), Quantity(quantity) { } - - uint32 Id; - int32 Quantity; -}; - -struct PlayerChoiceResponseReward -{ - int32 TitleId; - int32 PackageId; - int32 SkillLineId; - uint32 SkillPointCount; - uint32 ArenaPointCount; - uint32 HonorPointCount; - uint64 Money; - uint32 Xp; - std::vector<PlayerChoiceResponseRewardItem> Items; - std::vector<PlayerChoiceResponseRewardEntry> Currency; - std::vector<PlayerChoiceResponseRewardEntry> Faction; - std::vector<PlayerChoiceResponseRewardItem> ItemChoices; -}; - -struct PlayerChoiceResponseMawPower -{ - int32 TypeArtFileID = 0; - Optional<int32> Rarity; - int32 SpellID = 0; - int32 MaxStacks = 0; -}; - -struct PlayerChoiceResponse -{ - int32 ResponseId = 0; - uint16 ResponseIdentifier = 0; - int32 ChoiceArtFileId = 0; - int32 Flags = 0; - uint32 WidgetSetID = 0; - uint32 UiTextureAtlasElementID = 0; - uint32 SoundKitID = 0; - uint8 GroupID = 0; - int32 UiTextureKitID = 0; - std::string Answer; - std::string Header; - std::string SubHeader; - std::string ButtonTooltip; - std::string Description; - std::string Confirmation; - Optional<PlayerChoiceResponseReward> Reward; - Optional<uint32> RewardQuestID; - Optional<PlayerChoiceResponseMawPower> MawPower; -}; - -struct PlayerChoice -{ - int32 ChoiceId = 0; - int32 UiTextureKitId = 0; - uint32 SoundKitId = 0; - uint32 CloseSoundKitId = 0; - int64 Duration = 0; - std::string Question; - std::string PendingChoiceText; - std::vector<PlayerChoiceResponse> Responses; - bool HideWarboardHeader = false; - bool KeepOpenAfterChoice = false; - - PlayerChoiceResponse const* GetResponse(int32 responseId) const - { - auto itr = std::ranges::find(Responses, responseId, &PlayerChoiceResponse::ResponseId); - return itr != Responses.end() ? &(*itr) : nullptr; - } - - PlayerChoiceResponse const* GetResponseByIdentifier(int32 responseIdentifier) const - { - auto itr = std::ranges::find(Responses, responseIdentifier, &PlayerChoiceResponse::ResponseIdentifier); - return itr != Responses.end() ? &(*itr) : nullptr; - } -}; - enum SkillRangeType { SKILL_RANGE_LANGUAGE, // 300..300 diff --git a/src/server/game/Globals/PlayerChoice.h b/src/server/game/Globals/PlayerChoice.h new file mode 100644 index 00000000000..3a8e7948d8e --- /dev/null +++ b/src/server/game/Globals/PlayerChoice.h @@ -0,0 +1,133 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_PLAYER_CHOICE_H +#define TRINITYCORE_PLAYER_CHOICE_H + +#include "Define.h" +#include "EnumFlag.h" +#include "Optional.h" +#include <algorithm> +#include <string> +#include <vector> + +struct PlayerChoiceResponseRewardItem +{ + PlayerChoiceResponseRewardItem() : Id(0), Quantity(0) { } + PlayerChoiceResponseRewardItem(uint32 id, std::vector<int32>&& bonusListIDs, int32 quantity) : Id(id), BonusListIDs(std::move(bonusListIDs)), Quantity(quantity) { } + + uint32 Id; + std::vector<int32> BonusListIDs; + int32 Quantity; +}; + +struct PlayerChoiceResponseRewardEntry +{ + PlayerChoiceResponseRewardEntry() : Id(0), Quantity(0) { } + PlayerChoiceResponseRewardEntry(uint32 id, int32 quantity) : Id(id), Quantity(quantity) { } + + uint32 Id; + int32 Quantity; +}; + +struct PlayerChoiceResponseReward +{ + int32 TitleId; + int32 PackageId; + int32 SkillLineId; + uint32 SkillPointCount; + uint32 ArenaPointCount; + uint32 HonorPointCount; + uint64 Money; + uint32 Xp; + std::vector<PlayerChoiceResponseRewardItem> Items; + std::vector<PlayerChoiceResponseRewardEntry> Currency; + std::vector<PlayerChoiceResponseRewardEntry> Faction; + std::vector<PlayerChoiceResponseRewardItem> ItemChoices; +}; + +struct PlayerChoiceResponseMawPower +{ + int32 TypeArtFileID = 0; + Optional<int32> Rarity; + int32 SpellID = 0; + int32 MaxStacks = 0; +}; + +enum class PlayerChoiceResponseFlags : int32 +{ + None = 0x000, + DisabledButton = 0x001, // Disables single button + DesaturateArt = 0x002, + DisabledOption = 0x004, // Disables the entire group of options + ConsolidateWidgets = 0x020, + ShowCheckmark = 0x040, + HideButtonShowText = 0x080, + Selected = 0x100, +}; + +DEFINE_ENUM_FLAG(PlayerChoiceResponseFlags); + +struct PlayerChoiceResponse +{ + int32 ResponseId = 0; + int32 ChoiceArtFileId = 0; + EnumFlag<PlayerChoiceResponseFlags> Flags = PlayerChoiceResponseFlags::None; + uint32 WidgetSetID = 0; + uint32 UiTextureAtlasElementID = 0; + uint32 SoundKitID = 0; + uint8 GroupID = 0; + int32 UiTextureKitID = 0; + std::string Answer; + std::string Header; + std::string SubHeader; + std::string ButtonTooltip; + std::string Description; + std::string Confirmation; + Optional<PlayerChoiceResponseReward> Reward; + Optional<uint32> RewardQuestID; + Optional<PlayerChoiceResponseMawPower> MawPower; +}; + +struct PlayerChoice +{ + int32 ChoiceId = 0; + int32 UiTextureKitId = 0; + uint32 SoundKitId = 0; + uint32 CloseSoundKitId = 0; + Seconds Duration = 0s; + std::string Question; + std::string PendingChoiceText; + std::vector<PlayerChoiceResponse> Responses; + bool InfiniteRange = false; + bool HideWarboardHeader = false; + bool KeepOpenAfterChoice = false; + bool ShowChoicesAsList = false; + bool ForceDontShowChoicesAsList = false; + + Optional<uint32> MaxResponses; + + uint32 ScriptId = 0; + + PlayerChoiceResponse const* GetResponse(int32 responseId) const + { + auto itr = std::ranges::find(Responses, responseId, &PlayerChoiceResponse::ResponseId); + return itr != Responses.end() ? &(*itr) : nullptr; + } +}; + +#endif // TRINITYCORE_PLAYER_CHOICE_H diff --git a/src/server/game/Handlers/QuestHandler.cpp b/src/server/game/Handlers/QuestHandler.cpp index 1ad44984893..d69cd4829a9 100644 --- a/src/server/game/Handlers/QuestHandler.cpp +++ b/src/server/game/Handlers/QuestHandler.cpp @@ -24,12 +24,14 @@ #include "DB2Stores.h" #include "GameObject.h" #include "GameObjectAI.h" +#include "GameTime.h" #include "GossipDef.h" #include "Group.h" #include "Log.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" +#include "PlayerChoice.h" #include "PoolMgr.h" #include "QuestDef.h" #include "QuestPackets.h" @@ -768,65 +770,59 @@ void WorldSession::HandleRequestWorldQuestUpdate(WorldPackets::Quest::RequestWor SendPacket(response.Write()); } -void WorldSession::HandlePlayerChoiceResponse(WorldPackets::Quest::ChoiceResponse& choiceResponse) +void WorldSession::HandlePlayerChoiceResponse(WorldPackets::Quest::ChoiceResponse const& choiceResponse) { - if (_player->PlayerTalkClass->GetInteractionData().GetPlayerChoiceId() != uint32(choiceResponse.ChoiceID)) + PlayerChoiceData const* playerChoiceData = _player->PlayerTalkClass->GetInteractionData().GetPlayerChoice(); + if (!playerChoiceData) { - TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to respond to invalid player choice {} (allowed {}) (possible packet-hacking detected)", - GetPlayerInfo(), choiceResponse.ChoiceID, _player->PlayerTalkClass->GetInteractionData().GetPlayerChoiceId()); + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to respond to invalid player choice {} (none allowed)", + GetPlayerInfo(), choiceResponse.ChoiceID); return; } - PlayerChoice const* playerChoice = sObjectMgr->GetPlayerChoice(choiceResponse.ChoiceID); - if (!playerChoice) + if (playerChoiceData->GetChoiceId() != uint32(choiceResponse.ChoiceID)) + { + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to respond to invalid player choice {} ({} allowed)", + GetPlayerInfo(), choiceResponse.ChoiceID, playerChoiceData->GetChoiceId()); return; + } - PlayerChoiceResponse const* playerChoiceResponse = playerChoice->GetResponseByIdentifier(choiceResponse.ResponseIdentifier); - if (!playerChoiceResponse) + if (playerChoiceData->GetExpireTime() && playerChoiceData->GetExpireTime() < GameTime::GetSystemTime()) { - TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to select invalid player choice response {} (possible packet-hacking detected)", - GetPlayerInfo(), choiceResponse.ResponseIdentifier); + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to respond to expired player choice {})", + GetPlayerInfo(), choiceResponse.ChoiceID); return; } - sScriptMgr->OnPlayerChoiceResponse(GetPlayer(), choiceResponse.ChoiceID, choiceResponse.ResponseIdentifier); - - if (playerChoiceResponse->Reward) + Optional<uint32> responseId = playerChoiceData->FindIdByClientIdentifier(choiceResponse.ResponseIdentifier); + if (!responseId) { - if (playerChoiceResponse->Reward->TitleId) - _player->SetTitle(sCharTitlesStore.AssertEntry(playerChoiceResponse->Reward->TitleId), false); - - if (playerChoiceResponse->Reward->PackageId) - _player->RewardQuestPackage(playerChoiceResponse->Reward->PackageId, ItemContext::NONE); - - if (playerChoiceResponse->Reward->SkillLineId && _player->HasSkill(playerChoiceResponse->Reward->SkillLineId)) - _player->UpdateSkillPro(playerChoiceResponse->Reward->SkillLineId, 1000, playerChoiceResponse->Reward->SkillPointCount); - - if (playerChoiceResponse->Reward->HonorPointCount) - _player->AddHonorXP(playerChoiceResponse->Reward->HonorPointCount); - - if (playerChoiceResponse->Reward->Money) - _player->ModifyMoney(playerChoiceResponse->Reward->Money, false); - - if (playerChoiceResponse->Reward->Xp) - _player->GiveXP(playerChoiceResponse->Reward->Xp, nullptr, 0.0f); + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to select invalid player choice response identifier {}", + GetPlayerInfo(), choiceResponse.ResponseIdentifier); + return; + } - for (PlayerChoiceResponseRewardItem const& item : playerChoiceResponse->Reward->Items) - { - ItemPosCountVec dest; - if (_player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, item.Id, item.Quantity) == EQUIP_ERR_OK) - { - Item* newItem = _player->StoreNewItem(dest, item.Id, true, GenerateItemRandomBonusListId(item.Id), {}, ItemContext::Quest_Reward, &item.BonusListIDs); - _player->SendNewItem(newItem, item.Quantity, true, false); - } - } + PlayerChoice const* playerChoice = sObjectMgr->GetPlayerChoice(choiceResponse.ChoiceID); + if (!playerChoice) + return; - for (PlayerChoiceResponseRewardEntry const& currency : playerChoiceResponse->Reward->Currency) - _player->ModifyCurrency(currency.Id, currency.Quantity); + PlayerChoiceResponse const* playerChoiceResponse = playerChoice->GetResponse(*responseId); + if (!playerChoiceResponse) + { + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to select invalid player choice response {}", + GetPlayerInfo(), *responseId); + return; + } - for (PlayerChoiceResponseRewardEntry const& faction : playerChoiceResponse->Reward->Faction) - _player->GetReputationMgr().ModifyReputation(sFactionStore.AssertEntry(faction.Id), faction.Quantity); + if (playerChoiceResponse->Flags.HasFlag(PlayerChoiceResponseFlags::DisabledButton | PlayerChoiceResponseFlags::DisabledOption | PlayerChoiceResponseFlags::HideButtonShowText)) + { + TC_LOG_ERROR("entities.player.cheat", "Error in CMSG_CHOICE_RESPONSE: {} tried to select disabled player choice response {}", + GetPlayerInfo(), *responseId); + return; } + + sScriptMgr->OnPlayerChoiceResponse(ObjectAccessor::GetWorldObject(*_player, _player->PlayerTalkClass->GetInteractionData().SourceGuid), _player, + playerChoice, playerChoiceResponse, choiceResponse.ResponseIdentifier); } void WorldSession::HandleUiMapQuestLinesRequest(WorldPackets::Quest::UiMapQuestLinesRequest& uiMapQuestLinesRequest) diff --git a/src/server/game/Scripting/ScriptMgr.cpp b/src/server/game/Scripting/ScriptMgr.cpp index 3135dce1a22..09e3f4eca11 100644 --- a/src/server/game/Scripting/ScriptMgr.cpp +++ b/src/server/game/Scripting/ScriptMgr.cpp @@ -39,6 +39,7 @@ #include "ObjectMgr.h" #include "OutdoorPvPMgr.h" #include "Player.h" +#include "PlayerChoice.h" #include "ScriptReloadMgr.h" #include "ScriptSystem.h" #include "SmartAI.h" @@ -142,6 +143,10 @@ template<> struct is_script_database_bound<EventScript> : std::true_type { }; +template<> +struct is_script_database_bound<PlayerChoiceScript> + : std::true_type { }; + enum Spells { SPELL_HOTSWAP_VISUAL_SPELL_EFFECT = 40162 // 59084 @@ -2150,9 +2155,13 @@ void ScriptMgr::OnMovieComplete(Player* player, uint32 movieId) FOREACH_SCRIPT(PlayerScript)->OnMovieComplete(player, movieId); } -void ScriptMgr::OnPlayerChoiceResponse(Player* player, uint32 choiceId, uint32 responseId) +void ScriptMgr::OnPlayerChoiceResponse(WorldObject* object, Player* player, PlayerChoice const* choice, PlayerChoiceResponse const* response, uint16 clientIdentifier) { - FOREACH_SCRIPT(PlayerScript)->OnPlayerChoiceResponse(player, choiceId, responseId); + ASSERT(choice); + ASSERT(response); + + GET_SCRIPT(PlayerChoiceScript, choice->ScriptId, tmpscript); + tmpscript->OnResponse(object, player, choice, response, clientIdentifier); } // Account @@ -3020,10 +3029,6 @@ void PlayerScript::OnMovieComplete(Player* /*player*/, uint32 /*movieId*/) { } -void PlayerScript::OnPlayerChoiceResponse(Player* /*player*/, uint32 /*choiceId*/, uint32 /*responseId*/) -{ -} - AccountScript::AccountScript(char const* name) noexcept : ScriptObject(name) { @@ -3232,6 +3237,18 @@ void EventScript::OnTrigger(WorldObject* /*object*/, WorldObject* /*invoker*/, u { } +PlayerChoiceScript::PlayerChoiceScript(char const* name) noexcept + : ScriptObject(name) +{ + ScriptRegistry<PlayerChoiceScript>::Instance()->AddScript(this); +} + +PlayerChoiceScript::~PlayerChoiceScript() = default; + +void PlayerChoiceScript::OnResponse(WorldObject* /*object*/, Player* /*player*/, PlayerChoice const* /*choice*/, PlayerChoiceResponse const* /*response*/, uint16 /*clientIdentifier*/) +{ +} + // Specialize for each script type class like so: template class TC_GAME_API ScriptRegistry<SpellScriptLoader>; template class TC_GAME_API ScriptRegistry<ServerScript>; @@ -3266,3 +3283,4 @@ template class TC_GAME_API ScriptRegistry<SceneScript>; template class TC_GAME_API ScriptRegistry<QuestScript>; template class TC_GAME_API ScriptRegistry<WorldStateScript>; template class TC_GAME_API ScriptRegistry<EventScript>; +template class TC_GAME_API ScriptRegistry<PlayerChoiceScript>; diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index 482d687f659..2e711c9ffae 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -76,6 +76,8 @@ struct CreatureTemplate; struct CreatureData; struct ItemTemplate; struct MapEntry; +struct PlayerChoice; +struct PlayerChoiceResponse; struct Position; struct QuestObjective; struct SceneTemplate; @@ -789,9 +791,6 @@ class TC_GAME_API PlayerScript : public ScriptObject // Called when a player completes a movie virtual void OnMovieComplete(Player* player, uint32 movieId); - - // Called when a player choose a response from a PlayerChoice - virtual void OnPlayerChoiceResponse(Player* player, uint32 choiceId, uint32 responseId); }; class TC_GAME_API AccountScript : public ScriptObject @@ -991,6 +990,26 @@ class TC_GAME_API EventScript : public ScriptObject virtual void OnTrigger(WorldObject* object, WorldObject* invoker, uint32 eventId); }; +class TC_GAME_API PlayerChoiceScript : public ScriptObject +{ + protected: + + explicit PlayerChoiceScript(char const* name) noexcept; + + public: + + ~PlayerChoiceScript(); + + /** + * @param object Source object of the PlayerChoice (can be nullptr) + * @param player Player making the choice + * @param choice Database template of PlayerChoice + * @param response Database template of selected PlayerChoice response + * @param clientIdentifier Dynamically generated identifier of the response, changes every time PlayerChoice is sent to player + */ + virtual void OnResponse(WorldObject* object, Player* player, PlayerChoice const* choice, PlayerChoiceResponse const* response, uint16 clientIdentifier); +}; + // Manages registration, loading, and execution of scripts. class TC_GAME_API ScriptMgr { @@ -1226,7 +1245,7 @@ class TC_GAME_API ScriptMgr void OnQuestStatusChange(Player* player, uint32 questId); void OnPlayerRepop(Player* player); void OnMovieComplete(Player* player, uint32 movieId); - void OnPlayerChoiceResponse(Player* player, uint32 choiceId, uint32 responseId); + void OnPlayerChoiceResponse(WorldObject* object, Player* player, PlayerChoice const* choice, PlayerChoiceResponse const* response, uint16 clientIdentifier); public: /* AccountScript */ diff --git a/src/server/game/Server/Packets/QuestPackets.cpp b/src/server/game/Server/Packets/QuestPackets.cpp index 016efd60f97..bef3ef35892 100644 --- a/src/server/game/Server/Packets/QuestPackets.cpp +++ b/src/server/game/Server/Packets/QuestPackets.cpp @@ -765,7 +765,7 @@ ByteBuffer& operator<<(ByteBuffer& data, PlayerChoiceResponseMawPower const& pla { data << int32(playerChoiceResponseMawPower.Unused901_1); data << int32(playerChoiceResponseMawPower.TypeArtFileID); - data << int32(playerChoiceResponseMawPower.Unused901_2); + data << int32(playerChoiceResponseMawPower.BorderUiTextureAtlasMemberID); data << int32(playerChoiceResponseMawPower.SpellID); data << int32(playerChoiceResponseMawPower.MaxStacks); data << OptionalInit(playerChoiceResponseMawPower.Rarity); @@ -828,14 +828,14 @@ WorldPacket const* DisplayPlayerChoice::Write() _worldPacket << uint32(SoundKitID); _worldPacket << uint32(CloseUISoundKitID); _worldPacket << uint8(NumRerolls); - _worldPacket << Duration; + _worldPacket << ExpireTime; _worldPacket << SizedString::BitsSize<8>(Question); _worldPacket << SizedString::BitsSize<8>(PendingChoiceText); _worldPacket << Bits<1>(InfiniteRange); _worldPacket << Bits<1>(HideWarboardHeader); _worldPacket << Bits<1>(KeepOpenAfterChoice); - _worldPacket << Bits<1>(Unknown_1115_1); - _worldPacket << Bits<1>(Unknown_1115_2); + _worldPacket << Bits<1>(ShowChoicesAsList); + _worldPacket << Bits<1>(ForceDontShowChoicesAsList); _worldPacket.FlushBits(); for (PlayerChoiceResponse const& response : Responses) diff --git a/src/server/game/Server/Packets/QuestPackets.h b/src/server/game/Server/Packets/QuestPackets.h index 8f3b1b76438..7c14b336127 100644 --- a/src/server/game/Server/Packets/QuestPackets.h +++ b/src/server/game/Server/Packets/QuestPackets.h @@ -730,7 +730,7 @@ namespace WorldPackets int32 Unused901_1 = 0; int32 TypeArtFileID = 0; Optional<int32> Rarity; - int32 Unused901_2 = 0; + int32 BorderUiTextureAtlasMemberID = 0; int32 SpellID = 0; int32 MaxStacks = 0; }; @@ -770,15 +770,15 @@ namespace WorldPackets uint32 SoundKitID = 0; uint32 CloseUISoundKitID = 0; uint8 NumRerolls = 0; - WorldPackets::Duration<Seconds> Duration; + WorldPackets::Timestamp<> ExpireTime; std::string_view Question; std::string_view PendingChoiceText; std::vector<PlayerChoiceResponse> Responses; bool InfiniteRange = false; bool HideWarboardHeader = false; bool KeepOpenAfterChoice = false; - bool Unknown_1115_1 = false; - bool Unknown_1115_2 = false; + bool ShowChoicesAsList = false; + bool ForceDontShowChoicesAsList = false; }; class ChoiceResponse final : public ClientPacket diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index 29ccc49f3f1..a6c96948101 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -1578,7 +1578,7 @@ class TC_GAME_API WorldSession void HandlePushQuestToParty(WorldPackets::Quest::PushQuestToParty& packet); void HandleQuestPushResult(WorldPackets::Quest::QuestPushResult& packet); void HandleRequestWorldQuestUpdate(WorldPackets::Quest::RequestWorldQuestUpdate& packet); - void HandlePlayerChoiceResponse(WorldPackets::Quest::ChoiceResponse& choiceResponse); + void HandlePlayerChoiceResponse(WorldPackets::Quest::ChoiceResponse const& choiceResponse); void HandleUiMapQuestLinesRequest(WorldPackets::Quest::UiMapQuestLinesRequest& uiMapQuestLinesRequest); void HandleQueryTreasurePicker(WorldPackets::Query::QueryTreasurePicker const& queryTreasurePicker); void HandleSpawnTrackingUpdate(WorldPackets::Quest::SpawnTrackingUpdate& spawnTrackingUpdate); |