Core/Players: Improve trait config validation

This commit is contained in:
Shauren
2026-02-03 20:25:46 +01:00
parent 53ee0e43a2
commit 418f891f86
10 changed files with 195 additions and 59 deletions

View File

@@ -0,0 +1,14 @@
--
-- Table structure for table `trait_cond_account_element`
--
DROP TABLE IF EXISTS `trait_cond_account_element`;
CREATE TABLE `trait_cond_account_element` (
`ElementValueInt` bigint NOT NULL DEFAULT '0',
`ID` int unsigned NOT NULL DEFAULT '0',
`PlayerDataElementAccountID` int unsigned NOT NULL DEFAULT '0',
`Comparison` tinyint unsigned NOT NULL DEFAULT '0',
`Unused1110` int NOT NULL DEFAULT '0',
`PlayerDataElementCharacterID` int NOT NULL DEFAULT '0',
`VerifiedBuild` int NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`,`VerifiedBuild`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -1850,6 +1850,11 @@ void HotfixDatabaseConnection::DoPrepareStatements()
"TraitCondAccountElementID FROM trait_cond WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_COND, "SELECT MAX(ID) + 1 FROM trait_cond", CONNECTION_SYNCH);
// TraitCondAccountElement.db2
PrepareStatement(HOTFIX_SEL_TRAIT_COND_ACCOUNT_ELEMENT, "SELECT ElementValueInt, ID, PlayerDataElementAccountID, Comparison, Unused1110, "
"PlayerDataElementCharacterID FROM trait_cond_account_element WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_COND_ACCOUNT_ELEMENT, "SELECT MAX(ID) + 1 FROM trait_cond_account_element", CONNECTION_SYNCH);
// TraitCost.db2
PrepareStatement(HOTFIX_SEL_TRAIT_COST, "SELECT InternalName, ID, Amount, TraitCurrencyID, CurveID FROM trait_cost WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_cost", CONNECTION_SYNCH);

View File

@@ -1066,6 +1066,9 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_TRAIT_COND,
HOTFIX_SEL_TRAIT_COND_MAX_ID,
HOTFIX_SEL_TRAIT_COND_ACCOUNT_ELEMENT,
HOTFIX_SEL_TRAIT_COND_ACCOUNT_ELEMENT_MAX_ID,
HOTFIX_SEL_TRAIT_COST,
HOTFIX_SEL_TRAIT_COST_MAX_ID,

View File

@@ -6181,6 +6181,21 @@ struct TraitCondLoadInfo
static constexpr DB2LoadInfo Instance{ Fields, 17, &TraitCondMeta::Instance, HOTFIX_SEL_TRAIT_COND };
};
struct TraitCondAccountElementLoadInfo
{
static constexpr DB2FieldMeta Fields[6] =
{
{ .IsSigned = true, .Type = FT_LONG, .Name = "ElementValueInt" },
{ .IsSigned = false, .Type = FT_INT, .Name = "ID" },
{ .IsSigned = false, .Type = FT_INT, .Name = "PlayerDataElementAccountID" },
{ .IsSigned = false, .Type = FT_BYTE, .Name = "Comparison" },
{ .IsSigned = true, .Type = FT_INT, .Name = "Unused1110" },
{ .IsSigned = true, .Type = FT_INT, .Name = "PlayerDataElementCharacterID" },
};
static constexpr DB2LoadInfo Instance{ Fields, 6, &TraitCondAccountElementMeta::Instance, HOTFIX_SEL_TRAIT_COND_ACCOUNT_ELEMENT };
};
struct TraitCostLoadInfo
{
static constexpr DB2FieldMeta Fields[5] =

View File

@@ -353,6 +353,7 @@ DB2Storage<TaxiPathNodeEntry> sTaxiPathNodeStore("TaxiPathNode
DB2Storage<TotemCategoryEntry> sTotemCategoryStore("TotemCategory.db2", &TotemCategoryLoadInfo::Instance);
DB2Storage<ToyEntry> sToyStore("Toy.db2", &ToyLoadInfo::Instance);
DB2Storage<TraitCondEntry> sTraitCondStore("TraitCond.db2", &TraitCondLoadInfo::Instance);
DB2Storage<TraitCondAccountElementEntry> sTraitCondAccountElementStore("TraitCondAccountElement.db2", &TraitCondAccountElementLoadInfo::Instance);
DB2Storage<TraitCostEntry> sTraitCostStore("TraitCost.db2", &TraitCostLoadInfo::Instance);
DB2Storage<TraitCurrencyEntry> sTraitCurrencyStore("TraitCurrency.db2", &TraitCurrencyLoadInfo::Instance);
DB2Storage<TraitCurrencySourceEntry> sTraitCurrencySourceStore("TraitCurrencySource.db2", &TraitCurrencySourceLoadInfo::Instance);
@@ -982,6 +983,7 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sTotemCategoryStore);
LOAD_DB2(sToyStore);
LOAD_DB2(sTraitCondStore);
LOAD_DB2(sTraitCondAccountElementStore);
LOAD_DB2(sTraitCostStore);
LOAD_DB2(sTraitCurrencyStore);
LOAD_DB2(sTraitCurrencySourceStore);

View File

@@ -276,6 +276,7 @@ TC_GAME_API extern DB2Storage<TaxiNodesEntry> sTaxiNodesSt
TC_GAME_API extern DB2Storage<TaxiPathEntry> sTaxiPathStore;
TC_GAME_API extern DB2Storage<TaxiPathNodeEntry> sTaxiPathNodeStore;
TC_GAME_API extern DB2Storage<TraitCondEntry> sTraitCondStore;
TC_GAME_API extern DB2Storage<TraitCondAccountElementEntry> sTraitCondAccountElementStore;
TC_GAME_API extern DB2Storage<TraitCostEntry> sTraitCostStore;
TC_GAME_API extern DB2Storage<TraitCurrencyEntry> sTraitCurrencyStore;
TC_GAME_API extern DB2Storage<TraitCurrencySourceEntry> sTraitCurrencySourceStore;

View File

@@ -4419,6 +4419,16 @@ struct TraitCondEntry
EnumFlag<TraitCondFlags> GetFlags() const { return static_cast<TraitCondFlags>(Flags); }
};
struct TraitCondAccountElementEntry
{
int64 ElementValueInt;
uint32 ID;
uint32 PlayerDataElementAccountID;
uint8 Comparison;
int32 Unused1110;
int32 PlayerDataElementCharacterID;
};
struct TraitCostEntry
{
char const* InternalName;

View File

@@ -28860,10 +28860,10 @@ void Player::ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::
if (consumeCurrencies)
{
std::map<int32, int32> currencies;
std::map<int32, TraitMgr::SpentCurrency> currencies;
TraitMgr::FillSpentCurrenciesMap(costEntries, currencies);
for (auto [traitCurrencyId, amount] : currencies)
for (auto const& [traitCurrencyId, amount] : currencies)
{
TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(traitCurrencyId);
if (!traitCurrency)
@@ -28872,10 +28872,10 @@ void Player::ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::
switch (traitCurrency->GetType())
{
case TraitCurrencyType::Gold:
ModifyMoney(-amount);
ModifyMoney(-amount.Total);
break;
case TraitCurrencyType::CurrencyTypesBased:
RemoveCurrency(traitCurrency->CurrencyTypesID, amount /* TODO: CurrencyDestroyReason */);
RemoveCurrency(traitCurrency->CurrencyTypesID, amount.Total /* TODO: CurrencyDestroyReason */);
break;
default:
break;

View File

@@ -472,27 +472,74 @@ void FillOwnedCurrenciesMap(WorldPackets::Traits::TraitConfig const& traitConfig
}
}
void AddSpentCurrenciesForEntry(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, int32>& cachedCurrencies, int32 multiplier)
std::vector<TraitCondEntry const*> GetGateConditionsForNode(Node const* node)
{
std::vector<TraitCondEntry const*> gateConditions;
auto fillConditions = [&](std::vector<TraitCondEntry const*> const& conditions)
{
for (TraitCondEntry const* condition : conditions)
{
if (!condition->GetFlags().HasFlag(TraitCondFlags::IsGate))
continue;
auto cond = std::ranges::find(gateConditions, condition->TraitCurrencyID, &TraitCondEntry::TraitCurrencyID);
if (cond == gateConditions.end())
gateConditions.push_back(condition);
else if ((*cond)->SpentAmountRequired < condition->SpentAmountRequired)
*cond = condition;
}
};
fillConditions(node->Conditions);
for (NodeGroup const* group : node->Groups)
fillConditions(group->Conditions);
return gateConditions;
}
void AddSpentCurrenciesForEntry(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, SpentCurrency>& cachedCurrencies, int32 multiplier)
{
Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
std::vector<TraitCondEntry const*> gateConditions = GetGateConditionsForNode(node);
auto addCurrencies = [&](std::vector<TraitCostEntry const*> const& costs)
{
for (TraitCostEntry const* cost : costs)
{
int32 amount = cost->Amount * entry.Rank * multiplier;
SpentCurrency& cached = cachedCurrencies[cost->TraitCurrencyID];
cached.Total += amount;
int32 gate = 0;
auto gateCondition = std::ranges::find(gateConditions, cost->TraitCurrencyID, &TraitCondEntry::TraitCurrencyID);
if (gateCondition != gateConditions.end())
gate = (*gateCondition)->SpentAmountRequired;
auto gateCost = std::ranges::find(cached.ByGate, gate, Trinity::Containers::MapKey);
if (gateCost == cached.ByGate.end())
cached.ByGate.emplace_back(gate, amount);
else
gateCost->second += amount;
}
};
for (NodeGroup const* group : node->Groups)
for (TraitCostEntry const* cost : group->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
addCurrencies(group->Costs);
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;
addCurrencies(nodeEntryItr->Costs);
for (TraitCostEntry const* cost : node->Costs)
cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank * multiplier;
addCurrencies(node->Costs);
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;
addCurrencies(tree->Costs);
}
void FillSpentCurrenciesMap(std::vector<WorldPackets::Traits::TraitEntry> const& traitEntries, std::map<int32, int32>& cachedCurrencies)
void FillSpentCurrenciesMap(std::vector<WorldPackets::Traits::TraitEntry> const& traitEntries, std::map<int32, SpentCurrency>& cachedCurrencies)
{
for (WorldPackets::Traits::TraitEntry const& entry : traitEntries)
AddSpentCurrenciesForEntry(entry, cachedCurrencies, 1);
@@ -523,7 +570,7 @@ std::span<TraitCurrencyEntry const* const> GetSubTreeCurrency(int32 traitSubTree
}
bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player, TraitCondEntry const* condition,
Optional<std::map<int32, int32>>& cachedCurrencies)
Optional<std::map<int32, SpentCurrency>>& cachedCurrencies)
{
if (condition->QuestID && !player.IsQuestRewarded(condition->QuestID))
return false;
@@ -548,8 +595,14 @@ bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, P
if (condition->TraitNodeGroupID || condition->TraitNodeID || condition->TraitNodeEntryID)
{
auto itr = cachedCurrencies->try_emplace(condition->TraitCurrencyID, 0).first;
if (itr->second < condition->SpentAmountRequired)
int32 spentAmount = 0;
auto itr = cachedCurrencies->find(condition->TraitCurrencyID);
if (itr != cachedCurrencies->end())
for (auto [gate, spentBeforeGate] : itr->second.ByGate)
if (gate < condition->SpentAmountRequired)
spentAmount += spentBeforeGate;
if (spentAmount < condition->SpentAmountRequired)
return false;
}
}
@@ -557,12 +610,34 @@ bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, P
if (condition->RequiredLevel && player.GetLevel() < condition->RequiredLevel)
return false;
if (TraitCondAccountElementEntry const* accountElementCond = sTraitCondAccountElementStore.LookupEntry(condition->TraitCondAccountElementID))
{
int64 value = 0;
if (accountElementCond->PlayerDataElementAccountID)
value = std::visit([](auto v) { return static_cast<int64>(v); }, player.GetDataElementAccount(accountElementCond->PlayerDataElementAccountID));
else if (accountElementCond->PlayerDataElementCharacterID)
value = std::visit([](auto v) { return static_cast<int64>(v); }, player.GetDataElementCharacter(accountElementCond->PlayerDataElementCharacterID));
switch (accountElementCond->Comparison)
{
case 1: if (value != accountElementCond->ElementValueInt) return false; break;
case 2: if (value == accountElementCond->ElementValueInt) return false; break;
case 3: if (value >= accountElementCond->ElementValueInt) return false; break;
case 4: if (value > accountElementCond->ElementValueInt) return false; break;
case 5: if (value <= accountElementCond->ElementValueInt) return false; break;
case 6: if (value < accountElementCond->ElementValueInt) return false; break;
default:
return false;
}
}
return true;
}
bool NodeMeetsTraitConditions(WorldPackets::Traits::TraitConfig const& traitConfig, Node const* node, uint32 traitNodeEntryId, PlayerDataAccessor player, Optional<std::map<int32, int32>>& spentCurrencies)
bool NodeMeetsTraitConditions(WorldPackets::Traits::TraitConfig const& traitConfig, Node const* node, uint32 traitNodeEntryId, PlayerDataAccessor player,
Optional<std::map<int32, SpentCurrency>>& spentCurrencies)
{
auto meetsConditions = [&](std::vector<TraitCondEntry const*> const& conditions)
auto meetsConditions = [&](std::vector<TraitCondEntry const*> const& conditions, TraitConditionType conditionType)
{
struct
{
@@ -572,54 +647,59 @@ bool NodeMeetsTraitConditions(WorldPackets::Traits::TraitConfig const& traitConf
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;
}
if (condition->GetCondType() != conditionType)
continue;
if (!MeetsTraitCondition(traitConfig, player, condition, spentCurrencies))
{
result.HasFailedConditions = true;
continue;
}
if (condition->GetFlags().HasFlag(TraitCondFlags::IsSufficient))
{
result.IsSufficient = true;
break;
}
}
return result;
};
bool hasFailedConditions = false;
for (NodeEntry const& entry : node->Entries)
auto meetsConditionsOfType = [&](TraitConditionType conditionType)
{
if (entry.Data->ID == traitNodeEntryId)
bool hasFailedConditions = false;
if (auto [IsSufficient, HasFailedConditions] = meetsConditions(node->Conditions, conditionType); IsSufficient)
return true;
else if (HasFailedConditions)
hasFailedConditions = true;
for (NodeGroup const* group : node->Groups)
{
auto [IsSufficient, HasFailedConditions] = meetsConditions(entry.Conditions);
auto [IsSufficient, HasFailedConditions] = meetsConditions(group->Conditions, conditionType);
if (IsSufficient)
return true;
if (HasFailedConditions)
hasFailedConditions = true;
}
}
if (auto [IsSufficient, HasFailedConditions] = meetsConditions(node->Conditions); IsSufficient)
return true;
else if (HasFailedConditions)
hasFailedConditions = true;
for (NodeEntry const& entry : node->Entries)
{
if (entry.Data->ID == traitNodeEntryId)
{
auto [IsSufficient, HasFailedConditions] = meetsConditions(entry.Conditions, conditionType);
if (IsSufficient)
return true;
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;
};
return !hasFailedConditions;
};
return meetsConditionsOfType(TraitConditionType::Visible) && meetsConditionsOfType(TraitConditionType::Available);
}
std::vector<UF::TraitEntry> GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player)
{
@@ -647,7 +727,7 @@ std::vector<UF::TraitEntry> GetGrantedTraitEntriesForConfig(WorldPackets::Traits
itr->GrantedRanks = entry.Data->MaxRanks;
};
Optional<std::map<int32, int32>> cachedCurrencies;
Optional<std::map<int32, SpentCurrency>> cachedCurrencies;
for (Tree const* tree : *trees)
{
@@ -720,7 +800,7 @@ LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, Playe
return std::ranges::all_of(node->Entries, nodeEntryMatches);
};
Optional<std::map<int32, int32>> spentCurrencies;
Optional<std::map<int32, SpentCurrency>> spentCurrencies;
FillSpentCurrenciesMap(traitConfig.Entries, spentCurrencies.emplace());
auto isValidTraitEntry = [&](WorldPackets::Traits::TraitEntry const& traitEntry)
@@ -821,16 +901,16 @@ LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, Playe
std::map<int32, int32> grantedCurrencies;
FillOwnedCurrenciesMap(traitConfig, player, grantedCurrencies);
for (auto [traitCurrencyId, spentAmount] : *spentCurrencies)
for (auto const& [traitCurrencyId, spentAmount] : *spentCurrencies)
{
if (sTraitCurrencyStore.AssertEntry(traitCurrencyId)->GetType() != TraitCurrencyType::TraitSourced)
continue;
if (!spentAmount)
if (!spentAmount.Total)
continue;
int32* grantedCount = Trinity::Containers::MapGetValuePtr(grantedCurrencies, traitCurrencyId);
if (!grantedCount || *grantedCount < spentAmount)
if (!grantedCount || *grantedCount < spentAmount.Total)
return LearnResult::NotEnoughTalentsInPrimaryTree;
}
@@ -843,8 +923,8 @@ LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, Playe
if (!grantedAmount)
continue;
int32* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, traitCurrencyId);
if (!spentAmount || *spentAmount != *grantedAmount)
SpentCurrency* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, traitCurrencyId);
if (!spentAmount || spentAmount->Total != *grantedAmount)
return LearnResult::UnspentTalentPoints;
}
@@ -859,8 +939,8 @@ LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, Playe
if (!grantedAmount)
continue;
int32* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, subTreeCurrency->ID);
if (!spentAmount || *spentAmount != *grantedAmount)
SpentCurrency* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, subTreeCurrency->ID);
if (!spentAmount || spentAmount->Total != *grantedAmount)
return LearnResult::UnspentTalentPoints;
}
}

View File

@@ -77,10 +77,16 @@ private:
Player const* _player;
};
struct SpentCurrency
{
int32 Total = 0;
std::vector<std::pair<int32, int32>> ByGate;
};
void Load();
int32 GenerateNewTraitConfigId();
TraitConfigType GetConfigTypeForTree(int32 traitTreeId);
void FillSpentCurrenciesMap(std::vector<WorldPackets::Traits::TraitEntry> const& traitEntries, std::map<int32, int32>& cachedCurrencies);
void FillSpentCurrenciesMap(std::vector<WorldPackets::Traits::TraitEntry> const& traitEntries, std::map<int32, SpentCurrency>& cachedCurrencies);
std::vector<UF::TraitEntry> GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player);
bool IsValidEntry(WorldPackets::Traits::TraitEntry const& traitEntry);
LearnResult ValidateConfig(WorldPackets::Traits::TraitConfig& traitConfig, PlayerDataAccessor player, bool requireSpendingAllCurrencies = false, bool removeInvalidEntries = false);