aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShauren <shauren.trinity@gmail.com>2025-06-22 21:56:58 +0200
committerShauren <shauren.trinity@gmail.com>2025-06-22 21:56:58 +0200
commite59059e1bd2f67691e2da0105771b0cb55b123a4 (patch)
tree4f4864f8aae63eeafac29f440e0ce64a027c4108
parent7ca6b226a7420ff38e3a4f17a3758393d68629e3 (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.sql11
-rw-r--r--src/server/game/Conditions/ConditionMgr.cpp1
-rw-r--r--src/server/game/Entities/Creature/GossipDef.cpp11
-rw-r--r--src/server/game/Entities/Creature/GossipDef.h40
-rw-r--r--src/server/game/Entities/Player/Player.cpp68
-rw-r--r--src/server/game/Globals/ObjectMgr.cpp185
-rw-r--r--src/server/game/Globals/ObjectMgr.h93
-rw-r--r--src/server/game/Globals/PlayerChoice.h133
-rw-r--r--src/server/game/Handlers/QuestHandler.cpp82
-rw-r--r--src/server/game/Scripting/ScriptMgr.cpp30
-rw-r--r--src/server/game/Scripting/ScriptMgr.h27
-rw-r--r--src/server/game/Server/Packets/QuestPackets.cpp8
-rw-r--r--src/server/game/Server/Packets/QuestPackets.h8
-rw-r--r--src/server/game/Server/WorldSession.h2
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);