diff --git a/sql/updates/hotfixes/master/2026_02_03_00_hotfixes.sql b/sql/updates/hotfixes/master/2026_02_03_00_hotfixes.sql new file mode 100644 index 0000000000..f6a91af018 --- /dev/null +++ b/sql/updates/hotfixes/master/2026_02_03_00_hotfixes.sql @@ -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; diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp index 09b1132068..86df74b89e 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.cpp +++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp @@ -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); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h index 5765cd438b..bfefe5cd34 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.h +++ b/src/server/database/Database/Implementation/HotfixDatabase.h @@ -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, diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h index cd2029ca71..bdfec57070 100644 --- a/src/server/game/DataStores/DB2LoadInfo.h +++ b/src/server/game/DataStores/DB2LoadInfo.h @@ -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] = diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp index 737c587d23..2060dbd97b 100644 --- a/src/server/game/DataStores/DB2Stores.cpp +++ b/src/server/game/DataStores/DB2Stores.cpp @@ -353,6 +353,7 @@ DB2Storage sTaxiPathNodeStore("TaxiPathNode DB2Storage sTotemCategoryStore("TotemCategory.db2", &TotemCategoryLoadInfo::Instance); DB2Storage sToyStore("Toy.db2", &ToyLoadInfo::Instance); DB2Storage sTraitCondStore("TraitCond.db2", &TraitCondLoadInfo::Instance); +DB2Storage sTraitCondAccountElementStore("TraitCondAccountElement.db2", &TraitCondAccountElementLoadInfo::Instance); DB2Storage sTraitCostStore("TraitCost.db2", &TraitCostLoadInfo::Instance); DB2Storage sTraitCurrencyStore("TraitCurrency.db2", &TraitCurrencyLoadInfo::Instance); DB2Storage 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); diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h index 3c62c08744..b6a1788332 100644 --- a/src/server/game/DataStores/DB2Stores.h +++ b/src/server/game/DataStores/DB2Stores.h @@ -276,6 +276,7 @@ TC_GAME_API extern DB2Storage sTaxiNodesSt TC_GAME_API extern DB2Storage sTaxiPathStore; TC_GAME_API extern DB2Storage sTaxiPathNodeStore; TC_GAME_API extern DB2Storage sTraitCondStore; +TC_GAME_API extern DB2Storage sTraitCondAccountElementStore; TC_GAME_API extern DB2Storage sTraitCostStore; TC_GAME_API extern DB2Storage sTraitCurrencyStore; TC_GAME_API extern DB2Storage sTraitCurrencySourceStore; diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h index 97ff0c37ac..a9f295514b 100644 --- a/src/server/game/DataStores/DB2Structure.h +++ b/src/server/game/DataStores/DB2Structure.h @@ -4419,6 +4419,16 @@ struct TraitCondEntry EnumFlag GetFlags() const { return static_cast(Flags); } }; +struct TraitCondAccountElementEntry +{ + int64 ElementValueInt; + uint32 ID; + uint32 PlayerDataElementAccountID; + uint8 Comparison; + int32 Unused1110; + int32 PlayerDataElementCharacterID; +}; + struct TraitCostEntry { char const* InternalName; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index d66797464f..614e3afb20 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -28860,10 +28860,10 @@ void Player::ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits:: if (consumeCurrencies) { - std::map currencies; + std::map 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; diff --git a/src/server/game/Spells/TraitMgr.cpp b/src/server/game/Spells/TraitMgr.cpp index 825206942d..835f74a77e 100644 --- a/src/server/game/Spells/TraitMgr.cpp +++ b/src/server/game/Spells/TraitMgr.cpp @@ -472,27 +472,74 @@ void FillOwnedCurrenciesMap(WorldPackets::Traits::TraitConfig const& traitConfig } } -void AddSpentCurrenciesForEntry(WorldPackets::Traits::TraitEntry const& entry, std::map& cachedCurrencies, int32 multiplier) +std::vector GetGateConditionsForNode(Node const* node) +{ + std::vector gateConditions; + + auto fillConditions = [&](std::vector 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& cachedCurrencies, int32 multiplier) { Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID); + std::vector gateConditions = GetGateConditionsForNode(node); + + auto addCurrencies = [&](std::vector 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 const& traitEntries, std::map& cachedCurrencies) +void FillSpentCurrenciesMap(std::vector const& traitEntries, std::map& cachedCurrencies) { for (WorldPackets::Traits::TraitEntry const& entry : traitEntries) AddSpentCurrenciesForEntry(entry, cachedCurrencies, 1); @@ -523,7 +570,7 @@ std::span GetSubTreeCurrency(int32 traitSubTree } bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player, TraitCondEntry const* condition, - Optional>& cachedCurrencies) + Optional>& 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(v); }, player.GetDataElementAccount(accountElementCond->PlayerDataElementAccountID)); + else if (accountElementCond->PlayerDataElementCharacterID) + value = std::visit([](auto v) { return static_cast(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>& spentCurrencies) +bool NodeMeetsTraitConditions(WorldPackets::Traits::TraitConfig const& traitConfig, Node const* node, uint32 traitNodeEntryId, PlayerDataAccessor player, + Optional>& spentCurrencies) { - auto meetsConditions = [&](std::vector const& conditions) + auto meetsConditions = [&](std::vector 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 GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, PlayerDataAccessor player) { @@ -647,7 +727,7 @@ std::vector GetGrantedTraitEntriesForConfig(WorldPackets::Traits itr->GrantedRanks = entry.Data->MaxRanks; }; - Optional> cachedCurrencies; + Optional> 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> spentCurrencies; + Optional> 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 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; } } diff --git a/src/server/game/Spells/TraitMgr.h b/src/server/game/Spells/TraitMgr.h index f2f9634de8..d061f91f34 100644 --- a/src/server/game/Spells/TraitMgr.h +++ b/src/server/game/Spells/TraitMgr.h @@ -77,10 +77,16 @@ private: Player const* _player; }; +struct SpentCurrency +{ + int32 Total = 0; + std::vector> ByGate; +}; + void Load(); int32 GenerateNewTraitConfigId(); TraitConfigType GetConfigTypeForTree(int32 traitTreeId); -void FillSpentCurrenciesMap(std::vector const& traitEntries, std::map& cachedCurrencies); +void FillSpentCurrenciesMap(std::vector const& traitEntries, std::map& cachedCurrencies); std::vector 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);