/*
* 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 .
*/
#include "TraitMgr.h"
#include "DB2Stores.h"
#include "FlatSet.h"
#include "IteratorPair.h"
#include "MapUtils.h"
#include "TraitPacketsCommon.h"
#include "UpdateFields.h"
namespace TraitMgr
{
namespace
{
struct NodeEntry;
struct Node;
struct NodeGroup;
struct SubTree;
struct Tree;
struct NodeEntry
{
TraitNodeEntryEntry const* Data = nullptr;
std::vector Conditions;
std::vector Costs;
};
struct Node
{
TraitNodeEntry const* Data = nullptr;
std::vector Entries;
std::vector Groups;
std::vector> ParentNodes; // TraitEdge::LeftTraitNodeID
std::vector Conditions;
std::vector Costs;
};
struct NodeGroup
{
TraitNodeGroupEntry const* Data = nullptr;
std::vector Conditions;
std::vector Costs;
std::vector Nodes;
};
struct SubTree
{
TraitSubTreeEntry const* Data = nullptr;
std::vector Nodes;
Trinity::Containers::FlatSet Currencies;
};
struct Tree
{
TraitTreeEntry const* Data = nullptr;
std::vector Nodes;
std::vector Costs;
std::vector Currencies;
std::vector SubTrees;
TraitConfigType ConfigType = TraitConfigType::Invalid;
};
std::unordered_map _traitGroups;
std::unordered_map _traitNodes;
std::unordered_map _traitSubTrees;
std::unordered_map _traitTrees;
std::array _skillLinesByClass;
std::unordered_map> _traitTreesBySkillLine;
std::unordered_map> _traitTreesByTraitSystem;
int32 _configIdGenerator = 0;
std::unordered_map> _traitCurrencySourcesByCurrency;
std::unordered_map> _traitDefinitionEffectPointModifiers;
std::unordered_map> _traitTreeLoadoutsByChrSpecialization;
}
void Load()
{
_configIdGenerator = time(nullptr);
std::unordered_map> nodeEntryConditions;
for (TraitNodeEntryXTraitCondEntry const* traitNodeEntryXTraitCondEntry : sTraitNodeEntryXTraitCondStore)
if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeEntryXTraitCondEntry->TraitCondID))
nodeEntryConditions[traitNodeEntryXTraitCondEntry->TraitNodeEntryID].push_back(traitCondEntry);
std::unordered_map> nodeEntryCosts;
for (TraitNodeEntryXTraitCostEntry const* traitNodeEntryXTraitCostEntry : sTraitNodeEntryXTraitCostStore)
if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitNodeEntryXTraitCostEntry->TraitCostID))
nodeEntryCosts[traitNodeEntryXTraitCostEntry->TraitNodeEntryID].push_back(traitCostEntry);
std::unordered_map> nodeGroupConditions;
for (TraitNodeGroupXTraitCondEntry const* traitNodeGroupXTraitCondEntry : sTraitNodeGroupXTraitCondStore)
if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeGroupXTraitCondEntry->TraitCondID))
nodeGroupConditions[traitNodeGroupXTraitCondEntry->TraitNodeGroupID].push_back(traitCondEntry);
std::unordered_map> nodeGroupCosts;
for (TraitNodeGroupXTraitCostEntry const* traitNodeGroupXTraitCostEntry : sTraitNodeGroupXTraitCostStore)
if (TraitCostEntry const* traitCondEntry = sTraitCostStore.LookupEntry(traitNodeGroupXTraitCostEntry->TraitCostID))
nodeGroupCosts[traitNodeGroupXTraitCostEntry->TraitNodeGroupID].push_back(traitCondEntry);
std::unordered_multimap nodeGroups;
for (TraitNodeGroupXTraitNodeEntry const* traitNodeGroupXTraitNodeEntry : sTraitNodeGroupXTraitNodeStore)
nodeGroups.emplace(traitNodeGroupXTraitNodeEntry->TraitNodeID, traitNodeGroupXTraitNodeEntry->TraitNodeGroupID);
std::unordered_map> nodeConditions;
for (TraitNodeXTraitCondEntry const* traitNodeXTraitCondEntry : sTraitNodeXTraitCondStore)
if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeXTraitCondEntry->TraitCondID))
nodeConditions[traitNodeXTraitCondEntry->TraitNodeID].push_back(traitCondEntry);
std::unordered_map> nodeCosts;
for (TraitNodeXTraitCostEntry const* traitNodeXTraitCostEntry : sTraitNodeXTraitCostStore)
if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitNodeXTraitCostEntry->TraitCostID))
nodeCosts[traitNodeXTraitCostEntry->TraitNodeID].push_back(traitCostEntry);
std::unordered_multimap nodeEntries;
for (TraitNodeXTraitNodeEntryEntry const* traitNodeXTraitNodeEntryEntry : sTraitNodeXTraitNodeEntryStore)
if (TraitNodeEntryEntry const* traitNodeEntryEntry = sTraitNodeEntryStore.LookupEntry(traitNodeXTraitNodeEntryEntry->TraitNodeEntryID))
nodeEntries.emplace(traitNodeXTraitNodeEntryEntry->TraitNodeID, traitNodeEntryEntry);
std::unordered_map> treeCosts;
for (TraitTreeXTraitCostEntry const* traitTreeXTraitCostEntry : sTraitTreeXTraitCostStore)
if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitTreeXTraitCostEntry->TraitCostID))
treeCosts[traitTreeXTraitCostEntry->TraitTreeID].push_back(traitCostEntry);
std::unordered_map> treeCurrencies;
for (TraitTreeXTraitCurrencyEntry const* traitTreeXTraitCurrencyEntry : sTraitTreeXTraitCurrencyStore)
if (sTraitCurrencyStore.HasRecord(traitTreeXTraitCurrencyEntry->TraitCurrencyID))
treeCurrencies[traitTreeXTraitCurrencyEntry->TraitTreeID].push_back(traitTreeXTraitCurrencyEntry);
std::unordered_map> traitTreesIdsByTraitSystem;
for (TraitTreeEntry const* traitTree : sTraitTreeStore)
{
Tree& tree = _traitTrees[traitTree->ID];
tree.Data = traitTree;
if (std::vector* costs = Trinity::Containers::MapGetValuePtr(treeCosts, traitTree->ID))
tree.Costs = std::move(*costs);
if (std::vector* currencies = Trinity::Containers::MapGetValuePtr(treeCurrencies, traitTree->ID))
{
tree.Currencies.resize(currencies->size());
std::ranges::sort(*currencies, {}, &TraitTreeXTraitCurrencyEntry::Index);
std::ranges::transform(*currencies, tree.Currencies.begin(),
[](uint32 traitCurrencyId) { return sTraitCurrencyStore.AssertEntry(traitCurrencyId); },
&TraitTreeXTraitCurrencyEntry::TraitCurrencyID);
}
if (traitTree->TraitSystemID)
{
traitTreesIdsByTraitSystem[traitTree->TraitSystemID].push_back(traitTree->ID);
tree.ConfigType = TraitConfigType::Generic;
}
}
for (TraitSubTreeEntry const* traitSubTree : sTraitSubTreeStore)
{
SubTree& subTree = _traitSubTrees[traitSubTree->ID];
subTree.Data = traitSubTree;
if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitSubTree->TraitTreeID))
tree->SubTrees.push_back(&subTree);
}
for (TraitNodeGroupEntry const* traitNodeGroup : sTraitNodeGroupStore)
{
NodeGroup& nodeGroup = _traitGroups[traitNodeGroup->ID];
nodeGroup.Data = traitNodeGroup;
if (std::vector* conditions = Trinity::Containers::MapGetValuePtr(nodeGroupConditions, traitNodeGroup->ID))
nodeGroup.Conditions = std::move(*conditions);
if (std::vector* costs = Trinity::Containers::MapGetValuePtr(nodeGroupCosts, traitNodeGroup->ID))
nodeGroup.Costs = std::move(*costs);
}
for (TraitNodeEntry const* traitNode : sTraitNodeStore)
{
Node& node = _traitNodes[traitNode->ID];
node.Data = traitNode;
if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitNode->TraitTreeID))
tree->Nodes.push_back(&node);
for (auto&& [_, traitNodeEntry] : Trinity::Containers::MapEqualRange(nodeEntries, traitNode->ID))
{
NodeEntry& entry = node.Entries.emplace_back();
entry.Data = traitNodeEntry;
if (std::vector* conditions = Trinity::Containers::MapGetValuePtr(nodeEntryConditions, traitNodeEntry->ID))
entry.Conditions = std::move(*conditions);
if (std::vector* costs = Trinity::Containers::MapGetValuePtr(nodeEntryCosts, traitNodeEntry->ID))
entry.Costs = std::move(*costs);
}
for (auto&& [_, nodeGroupId] : Trinity::Containers::MapEqualRange(nodeGroups, traitNode->ID))
{
NodeGroup* nodeGroup = Trinity::Containers::MapGetValuePtr(_traitGroups, nodeGroupId);
if (!nodeGroup)
continue;
nodeGroup->Nodes.push_back(&node);
node.Groups.push_back(nodeGroup);
}
if (std::vector* conditions = Trinity::Containers::MapGetValuePtr(nodeConditions, traitNode->ID))
node.Conditions = std::move(*conditions);
if (std::vector* costs = Trinity::Containers::MapGetValuePtr(nodeCosts, traitNode->ID))
node.Costs = std::move(*costs);
if (SubTree* subTree = Trinity::Containers::MapGetValuePtr(_traitSubTrees, traitNode->TraitSubTreeID))
{
subTree->Nodes.push_back(&node);
for (NodeEntry const& nodeEntry : node.Entries)
for (TraitCostEntry const* cost : nodeEntry.Costs)
if (TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(cost->TraitCurrencyID))
subTree->Currencies.insert(traitCurrency);
for (NodeGroup const* nodeGroup : node.Groups)
for (TraitCostEntry const* cost : nodeGroup->Costs)
if (TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(cost->TraitCurrencyID))
subTree->Currencies.insert(traitCurrency);
for (TraitCostEntry const* cost : node.Costs)
if (TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(cost->TraitCurrencyID))
subTree->Currencies.insert(traitCurrency);
if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitNode->TraitTreeID))
for (TraitCostEntry const* cost : tree->Costs)
if (TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(cost->TraitCurrencyID))
subTree->Currencies.insert(traitCurrency);
}
}
for (TraitEdgeEntry const* traitEdgeEntry : sTraitEdgeStore)
{
Node* left = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEdgeEntry->LeftTraitNodeID);
Node* right = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEdgeEntry->RightTraitNodeID);
if (!left || !right)
continue;
right->ParentNodes.emplace_back(left, static_cast(traitEdgeEntry->Type));
}
for (SkillLineXTraitTreeEntry const* skillLineXTraitTreeEntry : sSkillLineXTraitTreeStore)
{
Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, skillLineXTraitTreeEntry->TraitTreeID);
if (!tree)
continue;
SkillLineEntry const* skillLineEntry = sSkillLineStore.LookupEntry(skillLineXTraitTreeEntry->SkillLineID);
if (!skillLineEntry)
continue;
_traitTreesBySkillLine[skillLineXTraitTreeEntry->SkillLineID].push_back(tree);
if (skillLineEntry->CategoryID == SKILL_CATEGORY_CLASS)
{
for (SkillRaceClassInfoEntry const* skillRaceClassInfo : sDB2Manager.GetSkillRaceClassInfo(skillLineEntry->ID))
for (int32 i = 1; i < MAX_CLASSES; ++i)
if (skillRaceClassInfo->ClassMask & (1 << (i - 1)))
_skillLinesByClass[i] = skillLineXTraitTreeEntry->SkillLineID;
tree->ConfigType = TraitConfigType::Combat;
}
else
tree->ConfigType = TraitConfigType::Profession;
}
for (auto&& [traitSystemId, traitTreeIds] : traitTreesIdsByTraitSystem)
for (int32 traitTreeId : traitTreeIds)
if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitTreeId))
_traitTreesByTraitSystem[traitSystemId].push_back(tree);
for (TraitCurrencySourceEntry const* traitCurrencySource : sTraitCurrencySourceStore)
_traitCurrencySourcesByCurrency[traitCurrencySource->TraitCurrencyID].push_back(traitCurrencySource);
for (TraitDefinitionEffectPointsEntry const* traitDefinitionEffectPoints : sTraitDefinitionEffectPointsStore)
_traitDefinitionEffectPointModifiers[traitDefinitionEffectPoints->TraitDefinitionID].push_back(traitDefinitionEffectPoints);
std::unordered_map> traitTreeLoadoutEntries;
for (TraitTreeLoadoutEntryEntry const* traitTreeLoadoutEntry : sTraitTreeLoadoutEntryStore)
traitTreeLoadoutEntries[traitTreeLoadoutEntry->TraitTreeLoadoutID].push_back(traitTreeLoadoutEntry);
for (TraitTreeLoadoutEntry const* traitTreeLoadout : sTraitTreeLoadoutStore)
{
if (std::vector* entries = Trinity::Containers::MapGetValuePtr(traitTreeLoadoutEntries, traitTreeLoadout->ID))
{
std::ranges::sort(*entries, {}, & TraitTreeLoadoutEntryEntry::OrderIndex);
// there should be only one loadout per spec, we take last one encountered
_traitTreeLoadoutsByChrSpecialization[traitTreeLoadout->ChrSpecializationID] = std::move(*entries);
}
}
}
/**
* Generates new TraitConfig identifier.
* Because this only needs to be unique for each character we let it overflow
*/
int32 GenerateNewTraitConfigId()
{
if (_configIdGenerator == std::numeric_limits::max())
_configIdGenerator = 0;
return ++_configIdGenerator;
}
TraitConfigType GetConfigTypeForTree(int32 traitTreeId)
{
Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitTreeId);
if (!tree)
return TraitConfigType::Invalid;
return tree->ConfigType;
}
/**
* @brief Finds relevant TraitTree identifiers
* @param traitConfig config data
* @return Trait tree data
*/
std::vector const* GetTreesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig)
{
switch (traitConfig.Type)
{
case TraitConfigType::Combat:
if (ChrSpecializationEntry const* chrSpecializationEntry = sChrSpecializationStore.LookupEntry(traitConfig.ChrSpecializationID))
return Trinity::Containers::MapGetValuePtr(_traitTreesBySkillLine, _skillLinesByClass[chrSpecializationEntry->ClassID]);
break;
case TraitConfigType::Profession:
return Trinity::Containers::MapGetValuePtr(_traitTreesBySkillLine, traitConfig.SkillLineID);
case TraitConfigType::Generic:
return Trinity::Containers::MapGetValuePtr(_traitTreesByTraitSystem, traitConfig.TraitSystemID);
default:
break;
}
return nullptr;
}
bool HasEnoughCurrency(WorldPackets::Traits::TraitEntry const& entry, std::map const& currencies)
{
auto getCurrencyCount = [&](int32 currencyId)
{
int32 const* count = Trinity::Containers::MapGetValuePtr(currencies, currencyId);
return count ? *count : 0;
};
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
for (NodeGroup const* group : node->Groups)
for (TraitCostEntry const* cost : group->Costs)
if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
return false;
auto nodeEntryItr = std::ranges::find_if(node->Entries, [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
if (nodeEntryItr != node->Entries.end())
for (TraitCostEntry const* cost : nodeEntryItr->Costs)
if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
return false;
for (TraitCostEntry const* cost : node->Costs)
if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
return false;
if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
for (TraitCostEntry const* cost : tree->Costs)
if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
return false;
return true;
}
void TakeCurrencyCost(WorldPackets::Traits::TraitEntry const& entry, std::map& currencies)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
for (NodeGroup const* group : node->Groups)
for (TraitCostEntry const* cost : group->Costs)
currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
auto nodeEntryItr = std::ranges::find_if(node->Entries, [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
if (nodeEntryItr != node->Entries.end())
for (TraitCostEntry const* cost : nodeEntryItr->Costs)
currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
for (TraitCostEntry const* cost : node->Costs)
currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
for (TraitCostEntry const* cost : tree->Costs)
currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
}
void FillOwnedCurrenciesMap(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player, std::map& currencies)
{
std::vector const* trees = GetTreesForConfig(traitConfig);
if (!trees)
return;
auto hasTraitNodeEntry = [&traitConfig](int32 traitNodeEntryId)
{
return std::ranges::find_if(traitConfig.Entries, [traitNodeEntryId](WorldPackets::Traits::TraitEntry const& traitEntry)
{
return traitEntry.TraitNodeEntryID == traitNodeEntryId && (traitEntry.Rank > 0 || traitEntry.GrantedRanks > 0);
}) != traitConfig.Entries.end();
};
for (Tree const* tree : *trees)
{
for (TraitCurrencyEntry const* currency : tree->Currencies)
{
switch (currency->GetType())
{
case TraitCurrencyType::Gold:
{
int32& amount = currencies[currency->ID];
if (player.GetMoney() > uint64(std::numeric_limits::max() - amount))
amount = std::numeric_limits::max();
else
amount += player.GetMoney();
break;
}
case TraitCurrencyType::CurrencyTypesBased:
currencies[currency->ID] += player.GetCurrencyQuantity(currency->CurrencyTypesID);
break;
case TraitCurrencyType::TraitSourced:
if (std::vector const* currencySources = Trinity::Containers::MapGetValuePtr(_traitCurrencySourcesByCurrency, currency->ID))
{
for (TraitCurrencySourceEntry const* currencySource : *currencySources)
{
if (currencySource->QuestID && !player.IsQuestRewarded(currencySource->QuestID))
continue;
if (currencySource->AchievementID && !player.HasAchieved(currencySource->AchievementID))
continue;
if (currencySource->PlayerLevel && player.GetLevel() < currencySource->PlayerLevel)
continue;
if (currencySource->TraitNodeEntryID && !hasTraitNodeEntry(currencySource->TraitNodeEntryID))
continue;
currencies[currencySource->TraitCurrencyID] += currencySource->Amount;
}
}
break;
default:
break;
}
}
}
}
void AddSpentCurrenciesForEntry(WorldPackets::Traits::TraitEntry const& entry, std::map& cachedCurrencies, int32 multiplier)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
for (NodeGroup const* group : node->Groups)
for (TraitCostEntry const* cost : group->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
auto nodeEntryItr = std::ranges::find_if(node->Entries, [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
if (nodeEntryItr != node->Entries.end())
for (TraitCostEntry const* cost : nodeEntryItr->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
for (TraitCostEntry const* cost : node->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
for (TraitCostEntry const* cost : tree->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
}
void FillSpentCurrenciesMap(std::vector const& traitEntries, std::map& cachedCurrencies)
{
for (WorldPackets::Traits::TraitEntry const& entry : traitEntries)
AddSpentCurrenciesForEntry(entry, cachedCurrencies, 1);
}
std::array GetClassAndSpecTreeCurrencies(WorldPackets::Traits::TraitConfig const& traitConfig)
{
std::array currencies = {};
if (std::vector const* trees = GetTreesForConfig(traitConfig))
{
auto dest = currencies.begin();
for (auto treeItr = trees->begin(); treeItr != trees->end() && dest != currencies.end(); ++treeItr)
for (auto currencyItr = (*treeItr)->Currencies.begin(); currencyItr != (*treeItr)->Currencies.end() && dest != currencies.end(); ++currencyItr)
*dest++ = (*currencyItr)->ID;
}
return currencies;
}
std::span GetSubTreeCurrency(int32 traitSubTreeId)
{
SubTree const* subTree = Trinity::Containers::MapGetValuePtr(_traitSubTrees, traitSubTreeId);
if (!subTree)
return {};
return subTree->Currencies;
}
bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player, TraitCondEntry const* condition,
Optional>& cachedCurrencies)
{
if (condition->QuestID && !player.IsQuestRewarded(condition->QuestID))
return false;
if (condition->AchievementID && !player.HasAchieved(condition->AchievementID))
return false;
if (condition->SpecSetID)
{
uint32 chrSpecializationId = player.GetPrimarySpecialization();
if (traitConfig.Type == TraitConfigType::Combat)
chrSpecializationId = traitConfig.ChrSpecializationID;
if (!sDB2Manager.IsSpecSetMember(condition->SpecSetID, chrSpecializationId))
return false;
}
if (condition->TraitCurrencyID && condition->SpentAmountRequired)
{
if (!cachedCurrencies)
FillSpentCurrenciesMap(traitConfig.Entries, cachedCurrencies.emplace());
if (condition->TraitNodeGroupID || condition->TraitNodeID || condition->TraitNodeEntryID)
{
auto itr = cachedCurrencies->try_emplace(condition->TraitCurrencyID, 0).first;
if (itr->second < condition->SpentAmountRequired)
return false;
}
}
if (condition->RequiredLevel && player.GetLevel() < condition->RequiredLevel)
return false;
return true;
}
bool NodeMeetsTraitConditions(WorldPackets::Traits::TraitConfig const& traitConfig, Node const* node, uint32 traitNodeEntryId, PlayerDataAccessor player, Optional>& spentCurrencies)
{
auto meetsConditions = [&](std::vector const& conditions)
{
struct
{
bool IsSufficient = false;
bool HasFailedConditions = false;
} result;
for (TraitCondEntry const* condition : conditions)
{
if (condition->GetCondType() == TraitConditionType::Available || condition->GetCondType() == TraitConditionType::Visible)
{
if (MeetsTraitCondition(traitConfig, player, condition, spentCurrencies))
{
if (condition->GetFlags().HasFlag(TraitCondFlags::IsSufficient))
{
result.IsSufficient = true;
break;
}
continue;
}
result.HasFailedConditions = true;
}
}
return result;
};
bool hasFailedConditions = false;
for (NodeEntry const& entry : node->Entries)
{
if (entry.Data->ID == traitNodeEntryId)
{
auto [IsSufficient, HasFailedConditions] = meetsConditions(entry.Conditions);
if (IsSufficient)
return true;
if (HasFailedConditions)
hasFailedConditions = true;
}
}
if (auto [IsSufficient, HasFailedConditions] = meetsConditions(node->Conditions); IsSufficient)
return true;
else if (HasFailedConditions)
hasFailedConditions = true;
for (NodeGroup const* group : node->Groups)
{
auto [IsSufficient, HasFailedConditions] = meetsConditions(group->Conditions);
if (IsSufficient)
return true;
if (HasFailedConditions)
hasFailedConditions = true;
}
return !hasFailedConditions;
};
std::vector GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player)
{
std::vector entries;
std::vector const* trees = GetTreesForConfig(traitConfig);
if (!trees)
return entries;
auto addGrantedRankToEntry = [&entries](int32 nodeId, NodeEntry const& entry, int32 grantedRanks)
{
auto itr = std::ranges::find_if(entries, [&](UF::TraitEntry const& traitEntry)
{
return traitEntry.TraitNodeID == nodeId && traitEntry.TraitNodeEntryID == int32(entry.Data->ID);
});
if (itr == entries.end())
{
itr = entries.emplace(entries.end());
itr->TraitNodeID = nodeId;
itr->TraitNodeEntryID = int32(entry.Data->ID);
itr->Rank = 0;
itr->GrantedRanks = 0;
}
itr->GrantedRanks += grantedRanks;
if (itr->GrantedRanks > entry.Data->MaxRanks)
itr->GrantedRanks = entry.Data->MaxRanks;
};
Optional> cachedCurrencies;
for (Tree const* tree : *trees)
{
for (Node const* node : tree->Nodes)
{
for (NodeEntry const& entry : node->Entries)
for (TraitCondEntry const* condition : entry.Conditions)
if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
addGrantedRankToEntry(node->Data->ID, entry, condition->GrantedRanks);
for (TraitCondEntry const* condition : node->Conditions)
if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
for (NodeEntry const& entry : node->Entries)
addGrantedRankToEntry(node->Data->ID, entry, condition->GrantedRanks);
for (NodeGroup const* group : node->Groups)
for (TraitCondEntry const* condition : group->Conditions)
if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
for (NodeEntry const& entry : node->Entries)
addGrantedRankToEntry(node->Data->ID, entry, condition->GrantedRanks);
}
}
return entries;
}
bool IsValidEntry(WorldPackets::Traits::TraitEntry const& traitEntry)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
if (!node)
return false;
auto entryItr = std::ranges::find_if(node->Entries, [&](NodeEntry const& entry) { return entry.Data->ID == uint32(traitEntry.TraitNodeEntryID); });
if (entryItr == node->Entries.end())
return false;
if (entryItr->Data->MaxRanks < traitEntry.Rank + traitEntry.GrantedRanks)
return false;
return true;
}
LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, PlayerDataAccessor player, bool requireSpendingAllCurrencies /*= false*/, bool removeInvalidEntries /*= false*/)
{
auto getNodeEntryCount = [&](int32 traitNodeId)
{
return std::ranges::count(traitConfig.Entries, traitNodeId, &WorldPackets::Traits::TraitEntry::TraitNodeID);
};
auto getNodeEntry = [&](int32 traitNodeId, int32 traitNodeEntryId)
{
auto entryItr = std::ranges::find_if(traitConfig.Entries, [=](WorldPackets::Traits::TraitEntry const& traitEntry)
{
return traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId;
});
return entryItr != traitConfig.Entries.end() ? &*entryItr : nullptr;
};
auto isNodeFullyFilled = [&](Node const* node)
{
auto nodeEntryMatches = [&](NodeEntry const& nodeEntry)
{
WorldPackets::Traits::TraitEntry const* traitEntry = getNodeEntry(node->Data->ID, nodeEntry.Data->ID);
return traitEntry && (traitEntry->Rank + traitEntry->GrantedRanks) == nodeEntry.Data->MaxRanks;
};
if (node->Data->GetType() == TraitNodeType::Selection)
return std::ranges::any_of(node->Entries, nodeEntryMatches);
return std::ranges::all_of(node->Entries, nodeEntryMatches);
};
Optional> spentCurrencies;
FillSpentCurrenciesMap(traitConfig.Entries, spentCurrencies.emplace());
auto isValidTraitEntry = [&](WorldPackets::Traits::TraitEntry const& traitEntry)
{
if (!IsValidEntry(traitEntry))
return LearnResult::Unknown;
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
if (node->Data->GetType() == TraitNodeType::Selection || node->Data->GetType() == TraitNodeType::SubTreeSelection)
if (getNodeEntryCount(traitEntry.TraitNodeID) != 1)
return LearnResult::Unknown;
if (!NodeMeetsTraitConditions(traitConfig, node, traitEntry.TraitNodeEntryID, player, spentCurrencies))
return LearnResult::Unknown;
if (!node->ParentNodes.empty())
{
bool hasAnyParentTrait = false;
for (auto const& [parentNode, edgeType] : node->ParentNodes)
{
if (!isNodeFullyFilled(parentNode))
{
if (edgeType == TraitEdgeType::RequiredForAvailability)
return LearnResult::NotEnoughTalentsInPrimaryTree;
continue;
}
hasAnyParentTrait = true;
}
if (!hasAnyParentTrait)
return LearnResult::NotEnoughTalentsInPrimaryTree;
}
return LearnResult::Ok;
};
for (auto itr = traitConfig.Entries.begin(); itr != traitConfig.Entries.end(); )
{
LearnResult result = isValidTraitEntry(*itr);
if (result != LearnResult::Ok)
{
if (!removeInvalidEntries)
return result;
AddSpentCurrenciesForEntry(*itr, *spentCurrencies, -1);
if (!itr->GrantedRanks // fully remove entries that don't have granted ranks
|| !itr->Rank) // ... or entries that do have them and don't have any additional spent ranks (can happen if the same entry is revalidated after first removing all spent ranks)
traitConfig.Entries.erase(itr);
else
itr->Rank = 0;
// revalidate entire config - a removed entry will invalidate all other entries that depend on it
itr = traitConfig.Entries.begin();
}
else
++itr;
}
struct SubtreeValidationData
{
std::vector Entries;
bool IsSelected = false;
};
std::unordered_map subtrees;
for (WorldPackets::Traits::TraitEntry const& traitEntry : traitConfig.Entries)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
auto entryItr = std::ranges::find(node->Entries, traitEntry.TraitNodeEntryID, [](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID); });
ASSERT(entryItr != node->Entries.end());
if (node->Data->GetType() == TraitNodeType::SubTreeSelection)
subtrees[entryItr->Data->TraitSubTreeID].IsSelected = true;
if (node->Data->TraitSubTreeID)
subtrees[node->Data->TraitSubTreeID].Entries.push_back(traitEntry);
}
for (WorldPackets::Traits::TraitSubTreeCache& subTree : traitConfig.SubTrees)
subTree.Active = false;
for (auto&& [selectedSubTreeId, data] : subtrees)
{
auto subtreeDataItr = std::ranges::find(traitConfig.SubTrees, selectedSubTreeId, &WorldPackets::Traits::TraitSubTreeCache::TraitSubTreeID);
if (subtreeDataItr == std::ranges::end(traitConfig.SubTrees))
{
subtreeDataItr = traitConfig.SubTrees.emplace(traitConfig.SubTrees.end());
subtreeDataItr->TraitSubTreeID = selectedSubTreeId;
}
subtreeDataItr->Entries = std::move(data.Entries);
subtreeDataItr->Active = data.IsSelected;
}
std::map grantedCurrencies;
FillOwnedCurrenciesMap(traitConfig, player, grantedCurrencies);
for (auto [traitCurrencyId, spentAmount] : *spentCurrencies)
{
if (sTraitCurrencyStore.AssertEntry(traitCurrencyId)->GetType() != TraitCurrencyType::TraitSourced)
continue;
if (!spentAmount)
continue;
int32* grantedCount = Trinity::Containers::MapGetValuePtr(grantedCurrencies, traitCurrencyId);
if (!grantedCount || *grantedCount < spentAmount)
return LearnResult::NotEnoughTalentsInPrimaryTree;
}
if (requireSpendingAllCurrencies && traitConfig.Type == TraitConfigType::Combat)
{
// client checks only first two currencies for trait tree
for (int32 traitCurrencyId : GetClassAndSpecTreeCurrencies(traitConfig))
{
int32* grantedAmount = Trinity::Containers::MapGetValuePtr(grantedCurrencies, traitCurrencyId);
if (!grantedAmount)
continue;
int32* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, traitCurrencyId);
if (!spentAmount || *spentAmount != *grantedAmount)
return LearnResult::UnspentTalentPoints;
}
for (auto&& [selectedTraitSubTreeId, data] : subtrees)
{
if (!data.IsSelected)
continue;
for (TraitCurrencyEntry const* subTreeCurrency : GetSubTreeCurrency(selectedTraitSubTreeId))
{
int32* grantedAmount = Trinity::Containers::MapGetValuePtr(grantedCurrencies, subTreeCurrency->ID);
if (!grantedAmount)
continue;
int32* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, subTreeCurrency->ID);
if (!spentAmount || *spentAmount != *grantedAmount)
return LearnResult::UnspentTalentPoints;
}
}
}
return LearnResult::Ok;
}
bool CanApplyTraitNode(UF::TraitConfig const& traitConfig, UF::TraitEntry const& traitEntry)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
if (!node)
return false;
if (node->Data->TraitSubTreeID)
{
auto subTreeItr = std::ranges::find(traitConfig.SubTrees, node->Data->TraitSubTreeID, &UF::TraitSubTreeCache::TraitSubTreeID);
if (subTreeItr == std::ranges::end(traitConfig.SubTrees) || !subTreeItr->Active)
return false;
}
return true;
}
std::vector const* GetTraitDefinitionEffectPointModifiers(int32 traitDefinitionId)
{
return Trinity::Containers::MapGetValuePtr(_traitDefinitionEffectPointModifiers, traitDefinitionId);
}
void InitializeStarterBuildTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig, PlayerDataAccessor player)
{
traitConfig.Entries.clear();
std::vector const* trees = GetTreesForConfig(traitConfig);
if (!trees)
return;
for (UF::TraitEntry const& grant : GetGrantedTraitEntriesForConfig(traitConfig, player))
{
WorldPackets::Traits::TraitEntry& newEntry = traitConfig.Entries.emplace_back();
newEntry.TraitNodeID = grant.TraitNodeID;
newEntry.TraitNodeEntryID = grant.TraitNodeEntryID;
newEntry.GrantedRanks = grant.GrantedRanks;
}
std::map currencies;
FillOwnedCurrenciesMap(traitConfig, player, currencies);
if (std::vector const* loadoutEntries = Trinity::Containers::MapGetValuePtr(_traitTreeLoadoutsByChrSpecialization, traitConfig.ChrSpecializationID))
{
auto findEntry = [](WorldPackets::Traits::TraitConfig& config, int32 traitNodeId, int32 traitNodeEntryId) -> WorldPackets::Traits::TraitEntry*
{
auto entryItr = std::ranges::find_if(config.Entries, [=](WorldPackets::Traits::TraitEntry const& traitEntry)
{
return traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId;
});
return entryItr != config.Entries.end() ? &*entryItr : nullptr;
};
for (TraitTreeLoadoutEntryEntry const* loadoutEntry : *loadoutEntries)
{
int32 addedRanks = loadoutEntry->NumPoints;
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, loadoutEntry->SelectedTraitNodeID);
WorldPackets::Traits::TraitEntry newEntry;
newEntry.TraitNodeID = loadoutEntry->SelectedTraitNodeID;
newEntry.TraitNodeEntryID = loadoutEntry->SelectedTraitNodeEntryID;
if (!newEntry.TraitNodeEntryID)
newEntry.TraitNodeEntryID = node->Entries[0].Data->ID;
WorldPackets::Traits::TraitEntry* entryInConfig = findEntry(traitConfig, newEntry.TraitNodeID, newEntry.TraitNodeEntryID);
if (entryInConfig)
addedRanks -= entryInConfig->Rank;
newEntry.Rank = addedRanks;
if (!HasEnoughCurrency(newEntry, currencies))
continue;
if (entryInConfig)
entryInConfig->Rank += addedRanks;
else
traitConfig.Entries.push_back(newEntry);
TakeCurrencyCost(newEntry, currencies);
}
}
}
}