using Framework.Constants; using Game.DataStorage; using Game.Entities; using Game.Networking.Packets; using System; using System.Collections.Generic; using System.Linq; namespace Game { class TraitMgr { public static uint COMMIT_COMBAT_TRAIT_CONFIG_CHANGES_SPELL_ID = 384255u; public static uint MAX_COMBAT_TRAIT_CONFIGS = 10u; static Dictionary _traitGroups = new(); static Dictionary _traitNodes = new(); static Dictionary _traitSubTrees = new(); static Dictionary _traitTrees = new(); static uint[] _skillLinesByClass = new uint[(int)Class.Max]; static MultiMap _traitTreesBySkillLine = new(); static MultiMap _traitTreesByTraitSystem = new(); static int _configIdGenerator; static MultiMap _traitCurrencySourcesByCurrency = new(); static MultiMap _traitDefinitionEffectPointModifiers = new(); static MultiMap _traitTreeLoadoutsByChrSpecialization = new(); public static void Load() { _configIdGenerator = int.MaxValue; MultiMap nodeEntryConditions = new(); foreach (TraitNodeEntryXTraitCondRecord traitNodeEntryXTraitCondEntry in CliDB.TraitNodeEntryXTraitCondStorage.Values) { TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeEntryXTraitCondEntry.TraitCondID); if (traitCondEntry != null) nodeEntryConditions.Add(traitNodeEntryXTraitCondEntry.TraitNodeEntryID, traitCondEntry); } MultiMap nodeEntryCosts = new(); foreach (TraitNodeEntryXTraitCostRecord traitNodeEntryXTraitCostEntry in CliDB.TraitNodeEntryXTraitCostStorage.Values) { TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeEntryXTraitCostEntry.TraitCostID); if (traitCostEntry != null) nodeEntryCosts.Add(traitNodeEntryXTraitCostEntry.TraitNodeEntryID, traitCostEntry); } MultiMap nodeGroupConditions = new(); foreach (TraitNodeGroupXTraitCondRecord traitNodeGroupXTraitCondEntry in CliDB.TraitNodeGroupXTraitCondStorage.Values) { TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeGroupXTraitCondEntry.TraitCondID); if (traitCondEntry != null) nodeGroupConditions.Add(traitNodeGroupXTraitCondEntry.TraitNodeGroupID, traitCondEntry); } MultiMap nodeGroupCosts = new(); foreach (TraitNodeGroupXTraitCostRecord traitNodeGroupXTraitCostEntry in CliDB.TraitNodeGroupXTraitCostStorage.Values) { TraitCostRecord traitCondEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeGroupXTraitCostEntry.TraitCostID); if (traitCondEntry != null) nodeGroupCosts.Add(traitNodeGroupXTraitCostEntry.TraitNodeGroupID, traitCondEntry); } MultiMap nodeGroups = new(); foreach (TraitNodeGroupXTraitNodeRecord traitNodeGroupXTraitNodeEntry in CliDB.TraitNodeGroupXTraitNodeStorage.Values) nodeGroups.Add(traitNodeGroupXTraitNodeEntry.TraitNodeID, traitNodeGroupXTraitNodeEntry.TraitNodeGroupID); MultiMap nodeConditions = new(); foreach (TraitNodeXTraitCondRecord traitNodeXTraitCondEntry in CliDB.TraitNodeXTraitCondStorage.Values) { TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeXTraitCondEntry.TraitCondID); if (traitCondEntry != null) nodeConditions.Add(traitNodeXTraitCondEntry.TraitNodeID, traitCondEntry); } MultiMap nodeCosts = new(); foreach (TraitNodeXTraitCostRecord traitNodeXTraitCostEntry in CliDB.TraitNodeXTraitCostStorage.Values) { TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeXTraitCostEntry.TraitCostID); if (traitCostEntry != null) nodeCosts.Add(traitNodeXTraitCostEntry.TraitNodeID, traitCostEntry); } MultiMap nodeEntries = new(); foreach (TraitNodeXTraitNodeEntryRecord traitNodeXTraitNodeEntryEntry in CliDB.TraitNodeXTraitNodeEntryStorage.Values) { TraitNodeEntryRecord traitNodeEntryEntry = CliDB.TraitNodeEntryStorage.LookupByKey(traitNodeXTraitNodeEntryEntry.TraitNodeEntryID); if (traitNodeEntryEntry != null) nodeEntries.Add(traitNodeXTraitNodeEntryEntry.TraitNodeID, traitNodeEntryEntry); } MultiMap treeCosts = new(); foreach (TraitTreeXTraitCostRecord traitTreeXTraitCostEntry in CliDB.TraitTreeXTraitCostStorage.Values) { TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitTreeXTraitCostEntry.TraitCostID); if (traitCostEntry != null) treeCosts.Add(traitTreeXTraitCostEntry.TraitTreeID, traitCostEntry); } MultiMap treeCurrencies = new(); foreach (TraitTreeXTraitCurrencyRecord traitTreeXTraitCurrencyEntry in CliDB.TraitTreeXTraitCurrencyStorage.Values) { if (CliDB.TraitCurrencyStorage.HasRecord((uint)traitTreeXTraitCurrencyEntry.TraitCurrencyID)) treeCurrencies.Add(traitTreeXTraitCurrencyEntry.TraitTreeID, traitTreeXTraitCurrencyEntry); } MultiMap traitTreesIdsByTraitSystem = new(); foreach (TraitTreeRecord traitTree in CliDB.TraitTreeStorage.Values) { Tree tree = new(); tree.Data = traitTree; var costs = treeCosts.LookupByKey(traitTree.Id); if (costs != null) tree.Costs = costs; var currencies = treeCurrencies.LookupByKey(traitTree.Id); if (currencies != null) { currencies.OrderBy(p => p.Index); tree.Currencies.AddRange(currencies.Select(p => CliDB.TraitCurrencyStorage.LookupByKey(p.TraitCurrencyID))); } if (traitTree.TraitSystemID != 0) { traitTreesIdsByTraitSystem.Add(traitTree.TraitSystemID, (int)traitTree.Id); tree.ConfigType = TraitConfigType.Generic; } _traitTrees[(int)traitTree.Id] = tree; } foreach (var (_, traitSubTree) in CliDB.TraitSubTreeStorage) { SubTree subTree = new(); subTree.Data = traitSubTree; Tree tree = _traitTrees.LookupByKey(traitSubTree.TraitTreeID); if (tree != null) tree.SubTrees.Add(subTree); _traitSubTrees[(int)traitSubTree.Id] = subTree; } foreach (var (_, traitNodeGroup) in CliDB.TraitNodeGroupStorage) { NodeGroup nodeGroup = new(); nodeGroup.Data = traitNodeGroup; var conditions = nodeGroupConditions.LookupByKey(traitNodeGroup.Id); if (conditions != null) nodeGroup.Conditions = conditions; var costs = nodeGroupCosts.LookupByKey(traitNodeGroup.Id); if (costs != null) nodeGroup.Costs = costs; _traitGroups[traitNodeGroup.Id] = nodeGroup; } foreach (TraitNodeRecord traitNode in CliDB.TraitNodeStorage.Values) { Node node = new(); node.Data = traitNode; Tree tree = _traitTrees.LookupByKey(traitNode.TraitTreeID); if (tree != null) tree.Nodes.Add(node); foreach (var traitNodeEntry in nodeEntries.LookupByKey(traitNode.Id)) { NodeEntry entry = new(); entry.Data = traitNodeEntry; var conditions = nodeEntryConditions.LookupByKey(traitNodeEntry.Id); if (conditions != null) entry.Conditions = conditions; var costs = nodeEntryCosts.LookupByKey(traitNodeEntry.Id); if (costs != null) entry.Costs = costs; node.Entries.Add(entry); } foreach (var nodeGroupId in nodeGroups.LookupByKey(traitNode.Id)) { NodeGroup nodeGroup = _traitGroups.LookupByKey(nodeGroupId); if (nodeGroup == null) continue; nodeGroup.Nodes.Add(node); node.Groups.Add(nodeGroup); } var conditions1 = nodeConditions.LookupByKey(traitNode.Id); if (conditions1 != null) node.Conditions = conditions1; var costs1 = nodeCosts.LookupByKey(traitNode.Id); if (costs1 != null) node.Costs = costs1; SubTree subTree = _traitSubTrees.LookupByKey(traitNode.TraitSubTreeID); if (subTree != null) { subTree.Nodes.Add(node); foreach (NodeEntry nodeEntry in node.Entries) { foreach (TraitCostRecord cost in nodeEntry.Costs) { TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID); if (traitCurrency != null) subTree.Currencies.Add(traitCurrency); } } foreach (NodeGroup nodeGroup in node.Groups) { foreach (TraitCostRecord cost in nodeGroup.Costs) { TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID); if (traitCurrency != null) subTree.Currencies.Add(traitCurrency); } } foreach (TraitCostRecord cost in node.Costs) { TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID); if (traitCurrency != null) subTree.Currencies.Add(traitCurrency); } Tree tree1 = _traitTrees.LookupByKey(traitNode.TraitTreeID); if (tree1 != null) { foreach (TraitCostRecord cost in tree1.Costs) { TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID); if (traitCurrency != null) subTree.Currencies.Add(traitCurrency); } } } _traitNodes[(int)traitNode.Id] = node; } foreach (TraitEdgeRecord traitEdgeEntry in CliDB.TraitEdgeStorage.Values) { Node left = _traitNodes.LookupByKey(traitEdgeEntry.LeftTraitNodeID); Node right = _traitNodes.LookupByKey(traitEdgeEntry.RightTraitNodeID); if (left == null || right == null) continue; right.ParentNodes.Add(Tuple.Create(left, (TraitEdgeType)traitEdgeEntry.Type)); } foreach (SkillLineXTraitTreeRecord skillLineXTraitTreeEntry in CliDB.SkillLineXTraitTreeStorage.Values) { Tree tree = _traitTrees.LookupByKey(skillLineXTraitTreeEntry.TraitTreeID); if (tree == null) continue; SkillLineRecord skillLineEntry = CliDB.SkillLineStorage.LookupByKey(skillLineXTraitTreeEntry.SkillLineID); if (skillLineEntry == null) continue; _traitTreesBySkillLine.Add(skillLineXTraitTreeEntry.SkillLineID, tree); if (skillLineEntry.CategoryID == SkillCategory.Class) { foreach (SkillRaceClassInfoRecord skillRaceClassInfo in Global.DB2Mgr.GetSkillRaceClassInfo(skillLineEntry.Id)) for (int i = 1; i < (int)Class.Max; ++i) if ((skillRaceClassInfo.ClassMask & (1 << (i - 1))) != 0) _skillLinesByClass[i] = skillLineXTraitTreeEntry.SkillLineID; tree.ConfigType = TraitConfigType.Combat; } else tree.ConfigType = TraitConfigType.Profession; } foreach (var (traitSystemId, traitTreeId) in traitTreesIdsByTraitSystem) { Tree tree = _traitTrees.LookupByKey(traitTreeId); if (tree != null) _traitTreesByTraitSystem.Add(traitSystemId, tree); } foreach (TraitCurrencySourceRecord traitCurrencySource in CliDB.TraitCurrencySourceStorage.Values) _traitCurrencySourcesByCurrency.Add(traitCurrencySource.TraitCurrencyID, traitCurrencySource); foreach (TraitDefinitionEffectPointsRecord traitDefinitionEffectPoints in CliDB.TraitDefinitionEffectPointsStorage.Values) _traitDefinitionEffectPointModifiers.Add(traitDefinitionEffectPoints.TraitDefinitionID, traitDefinitionEffectPoints); MultiMap traitTreeLoadoutEntries = new(); foreach (TraitTreeLoadoutEntryRecord traitTreeLoadoutEntry in CliDB.TraitTreeLoadoutEntryStorage.Values) traitTreeLoadoutEntries[traitTreeLoadoutEntry.TraitTreeLoadoutID].Add(traitTreeLoadoutEntry); foreach (TraitTreeLoadoutRecord traitTreeLoadout in CliDB.TraitTreeLoadoutStorage.Values) { var entries = traitTreeLoadoutEntries.LookupByKey(traitTreeLoadout.Id); if (entries != null) { entries.Sort((left, right) => { return left.OrderIndex.CompareTo(right.OrderIndex); }); // there should be only one loadout per spec, we take last one encountered _traitTreeLoadoutsByChrSpecialization[traitTreeLoadout.ChrSpecializationID] = entries; } } } /** * Generates new TraitConfig identifier. * Because this only needs to be unique for each character we let it overflow */ public static int GenerateNewTraitConfigId() { if (_configIdGenerator == int.MaxValue) _configIdGenerator = 0; return ++_configIdGenerator; } public static TraitConfigType GetConfigTypeForTree(int traitTreeId) { Tree tree = _traitTrees.LookupByKey(traitTreeId); if (tree == null) return TraitConfigType.Invalid; return tree.ConfigType; } /** * @brief Finds relevant TraitTree identifiers * @param traitConfig config data * @return Trait tree data */ public static List GetTreesForConfig(TraitConfigPacket traitConfig) { switch (traitConfig.Type) { case TraitConfigType.Combat: ChrSpecializationRecord chrSpecializationEntry = CliDB.ChrSpecializationStorage.LookupByKey(traitConfig.ChrSpecializationID); if (chrSpecializationEntry != null) return _traitTreesBySkillLine.LookupByKey(_skillLinesByClass[chrSpecializationEntry.ClassID]); break; case TraitConfigType.Profession: return _traitTreesBySkillLine.LookupByKey(traitConfig.SkillLineID); case TraitConfigType.Generic: return _traitTreesByTraitSystem.LookupByKey(traitConfig.TraitSystemID); default: break; } return null; } public static bool HasEnoughCurrency(TraitEntryPacket entry, Dictionary currencies) { int getCurrencyCount(int currencyId) { return currencies.LookupByKey(currencyId); } Node node = _traitNodes.LookupByKey(entry.TraitNodeID); foreach (NodeGroup group in node.Groups) foreach (TraitCostRecord cost in group.Costs) if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank) return false; var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID); if (nodeEntryItr != null) foreach (TraitCostRecord cost in nodeEntryItr.Costs) if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank) return false; foreach (TraitCostRecord cost in node.Costs) if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank) return false; Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID); if (tree != null) foreach (TraitCostRecord cost in tree.Costs) if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank) return false; return true; } public static void TakeCurrencyCost(TraitEntryPacket entry, Dictionary currencies) { Node node = _traitNodes.LookupByKey(entry.TraitNodeID); foreach (NodeGroup group in node.Groups) foreach (TraitCostRecord cost in group.Costs) currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank; var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID); if (nodeEntryItr != null) foreach (TraitCostRecord cost in nodeEntryItr.Costs) currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank; foreach (TraitCostRecord cost in node.Costs) currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank; Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID); if (tree != null) foreach (TraitCostRecord cost in tree.Costs) currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank; } public static void FillOwnedCurrenciesMap(TraitConfigPacket traitConfig, Player player, Dictionary currencies) { List trees = GetTreesForConfig(traitConfig); if (trees == null) return; bool hasTraitNodeEntry(int traitNodeEntryId) { return traitConfig.Entries.Any(traitEntry => traitEntry.TraitNodeEntryID == traitNodeEntryId && (traitEntry.Rank > 0 || traitEntry.GrantedRanks > 0)); } foreach (Tree tree in trees) { foreach (TraitCurrencyRecord currency in tree.Currencies) { switch (currency.GetCurrencyType()) { case TraitCurrencyType.Gold: { if (!currencies.ContainsKey((int)currency.Id)) currencies[currency.Id] = 0; int amount = currencies[currency.Id]; if (player.GetMoney() > (ulong)(int.MaxValue - amount)) amount = int.MaxValue; else amount += (int)player.GetMoney(); break; } case TraitCurrencyType.CurrencyTypesBased: if (!currencies.ContainsKey((int)currency.Id)) currencies[currency.Id] = 0; currencies[currency.Id] += (int)player.GetCurrencyQuantity((uint)currency.CurrencyTypesID); break; case TraitCurrencyType.TraitSourced: var currencySources = _traitCurrencySourcesByCurrency.LookupByKey(currency.Id); if (currencySources != null) { foreach (TraitCurrencySourceRecord currencySource in currencySources) { if (currencySource.QuestID != 0 && !player.IsQuestRewarded(currencySource.QuestID)) continue; if (currencySource.AchievementID != 0 && !player.HasAchieved(currencySource.AchievementID)) continue; if (currencySource.PlayerLevel != 0 && player.GetLevel() < currencySource.PlayerLevel) continue; if (currencySource.TraitNodeEntryID != 0 && !hasTraitNodeEntry(currencySource.TraitNodeEntryID)) continue; if (!currencies.ContainsKey(currencySource.TraitCurrencyID)) currencies[currencySource.TraitCurrencyID] = 0; currencies[currencySource.TraitCurrencyID] += currencySource.Amount; } } break; case TraitCurrencyType.TraitSourcedPlayerDataElement: if (currency.PlayerDataElementAccountID != 0) currencies[currency.Id] += player.GetDataElementAccount((uint)currency.CurrencyTypesID); else if (currency.PlayerDataElementCharacterID != 0) currencies[currency.Id] += player.GetDataElementCharacter((uint)currency.CurrencyTypesID); break; default: break; } } } } public static void AddSpentCurrenciesForEntry(TraitEntryPacket entry, Dictionary cachedCurrencies, int multiplier) { Node node = _traitNodes.LookupByKey(entry.TraitNodeID); foreach (NodeGroup group in node.Groups) { foreach (TraitCostRecord cost in group.Costs) { if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID)) cachedCurrencies[cost.TraitCurrencyID] = 0; cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier; } } var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID); if (nodeEntryItr != null) { foreach (TraitCostRecord cost in nodeEntryItr.Costs) { if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID)) cachedCurrencies[cost.TraitCurrencyID] = 0; cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier; } } foreach (TraitCostRecord cost in node.Costs) { if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID)) cachedCurrencies[cost.TraitCurrencyID] = 0; cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier; } Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID); if (tree != null) { foreach (TraitCostRecord cost in tree.Costs) { if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID)) cachedCurrencies[cost.TraitCurrencyID] = 0; cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier; } } } public static void FillSpentCurrenciesMap(List traitEntries, Dictionary cachedCurrencies) { foreach (TraitEntryPacket entry in traitEntries) AddSpentCurrenciesForEntry(entry, cachedCurrencies, 1); } public static int[] GetClassAndSpecTreeCurrencies(TraitConfigPacket traitConfig) { int[] currencies = new int[2]; List trees = GetTreesForConfig(traitConfig); if (trees != null) { int destIndex = 0; foreach (var tree in trees) { if (destIndex >= currencies.Length) break; foreach (var currency in tree.Currencies) { if (destIndex >= currencies.Length) break; currencies[destIndex++] = (int)currency.Id; } } } return currencies; } public static List GetSubTreeCurrency(int traitSubTreeId) { SubTree subTree = _traitSubTrees.LookupByKey(traitSubTreeId); if (subTree == null) return null; return subTree.Currencies; } public static bool MeetsTraitCondition(TraitConfigPacket traitConfig, Player player, TraitCondRecord condition, ref Dictionary cachedCurrencies) { if (condition.QuestID != 0 && !player.IsQuestRewarded(condition.QuestID)) return false; if (condition.AchievementID != 0 && !player.HasAchieved(condition.AchievementID)) return false; if (condition.SpecSetID != 0) { uint chrSpecializationId = (uint)player.GetPrimarySpecialization(); if (traitConfig.Type == TraitConfigType.Combat) chrSpecializationId = (uint)traitConfig.ChrSpecializationID; if (!Global.DB2Mgr.IsSpecSetMember(condition.SpecSetID, chrSpecializationId)) return false; } if (condition.TraitCurrencyID != 0 && condition.SpentAmountRequired != 0) { if (cachedCurrencies == null) { cachedCurrencies = new(); FillSpentCurrenciesMap(traitConfig.Entries, cachedCurrencies); } if (condition.TraitNodeGroupID != 0 || condition.TraitNodeID != 0 || condition.TraitNodeEntryID != 0) { cachedCurrencies.TryAdd(condition.TraitCurrencyID, 0); if (cachedCurrencies[condition.TraitCurrencyID] < condition.SpentAmountRequired) return false; } } if (condition.RequiredLevel != 0 && player.GetLevel() < condition.RequiredLevel) return false; return true; } public static bool NodeMeetsTraitConditions(TraitConfigPacket traitConfig, Node node, uint traitNodeEntryId, Player player, Dictionary spentCurrencies) { var meetsConditions = (bool isSufficient, bool hasFailedConditions) (List conditions) => { bool isSufficient = false; bool hasFailedConditions = false; foreach (var condition in conditions) { if (condition.GetCondType() == TraitConditionType.Available || condition.GetCondType() == TraitConditionType.Visible) { if (MeetsTraitCondition(traitConfig, player, condition, ref spentCurrencies)) { if (condition.HasFlag(TraitCondFlags.IsSufficient)) { isSufficient = true; break; } continue; } hasFailedConditions = true; } } return (isSufficient, hasFailedConditions); }; bool IsSufficient; bool HasFailedConditions; bool hasFailedConditions = false; foreach (NodeEntry entry in node.Entries) { if (entry.Data.Id == traitNodeEntryId) { (IsSufficient, HasFailedConditions) = meetsConditions(entry.Conditions); if (IsSufficient) return true; if (HasFailedConditions) hasFailedConditions = true; } } (IsSufficient, HasFailedConditions) = meetsConditions(node.Conditions); if (IsSufficient) return true; else if (HasFailedConditions) hasFailedConditions = true; foreach (NodeGroup group in node.Groups) { (IsSufficient, HasFailedConditions) = meetsConditions(group.Conditions); if (IsSufficient) return true; if (HasFailedConditions) hasFailedConditions = true; } return !hasFailedConditions; } public static List GetGrantedTraitEntriesForConfig(TraitConfigPacket traitConfig, Player player) { List entries = new(); var trees = GetTreesForConfig(traitConfig); if (trees == null) return entries; void addGrantedRankToEntry(uint nodeId, NodeEntry entry, int grantedRanks) { TraitEntry foundTraitEntry = entries.Find(traitEntry => traitEntry.TraitNodeID == nodeId && traitEntry.TraitNodeEntryID == entry.Data.Id); if (foundTraitEntry == null) { foundTraitEntry = new(); foundTraitEntry.TraitNodeID = (int)nodeId; foundTraitEntry.TraitNodeEntryID = (int)entry.Data.Id; foundTraitEntry.Rank = 0; foundTraitEntry.GrantedRanks = 0; entries.Add(foundTraitEntry); } foundTraitEntry.GrantedRanks += grantedRanks; if (foundTraitEntry.GrantedRanks > entry.Data.MaxRanks) foundTraitEntry.GrantedRanks = entry.Data.MaxRanks; } Dictionary cachedCurrencies = null; foreach (Tree tree in trees) { foreach (Node node in tree.Nodes) { foreach (NodeEntry entry in node.Entries) foreach (TraitCondRecord condition in entry.Conditions) if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies)) addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks); foreach (TraitCondRecord condition in node.Conditions) if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies)) foreach (NodeEntry entry in node.Entries) addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks); foreach (NodeGroup group in node.Groups) foreach (TraitCondRecord condition in group.Conditions) if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies)) foreach (NodeEntry entry in node.Entries) addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks); } } return entries; } public static bool IsValidEntry(TraitEntryPacket traitEntry) { Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID); if (node == null) return false; var entryItr = node.Entries.Find(entry => entry.Data.Id == traitEntry.TraitNodeEntryID); if (entryItr == null) return false; if (entryItr.Data.MaxRanks < traitEntry.Rank + traitEntry.GrantedRanks) return false; return true; } public static TalentLearnResult ValidateConfig(TraitConfigPacket traitConfig, Player player, bool requireSpendingAllCurrencies = false, bool removeInvalidEntries = false) { int getNodeEntryCount(int traitNodeId) { return traitConfig.Entries.Count(traitEntry => traitEntry.TraitNodeID == traitNodeId); } TraitEntryPacket getNodeEntry(uint traitNodeId, uint traitNodeEntryId) { return traitConfig.Entries.Find(traitEntry => traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId); } bool isNodeFullyFilled(Node node) { bool nodeEntryMatches(NodeEntry nodeEntry) { TraitEntryPacket traitEntry = getNodeEntry(node.Data.Id, nodeEntry.Data.Id); return traitEntry != null && (traitEntry.Rank + traitEntry.GrantedRanks) == nodeEntry.Data.MaxRanks; }; if (node.Data.GetNodeType() == TraitNodeType.Selection) return node.Entries.Any(nodeEntryMatches); return node.Entries.All(nodeEntryMatches); } Dictionary spentCurrencies = new(); FillSpentCurrenciesMap(traitConfig.Entries, spentCurrencies); var isValidTraitEntry = TalentLearnResult (TraitEntryPacket traitEntry) => { if (!IsValidEntry(traitEntry)) return TalentLearnResult.FailedUnknown; Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID); if (node.Data.GetNodeType() == TraitNodeType.Selection || node.Data.GetNodeType() == TraitNodeType.SubTreeSelection) if (getNodeEntryCount(traitEntry.TraitNodeID) != 1) return TalentLearnResult.FailedUnknown; if (!NodeMeetsTraitConditions(traitConfig, node, (uint)traitEntry.TraitNodeEntryID, player, spentCurrencies)) return TalentLearnResult.FailedUnknown; if (!node.ParentNodes.Empty()) { bool hasAnyParentTrait = false; foreach (var (parentNode, edgeType) in node.ParentNodes) { if (!isNodeFullyFilled(parentNode)) { if (edgeType == TraitEdgeType.RequiredForAvailability) return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree; continue; } hasAnyParentTrait = true; } if (!hasAnyParentTrait) return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree; } return TalentLearnResult.LearnOk; }; for (var i = 0; i != traitConfig.Entries.Count;) { TalentLearnResult result = isValidTraitEntry(traitConfig.Entries[i]); if (result != TalentLearnResult.LearnOk) { if (!removeInvalidEntries) return result; AddSpentCurrenciesForEntry(traitConfig.Entries[i], spentCurrencies, -1); if (traitConfig.Entries[i].GrantedRanks == 0 // fully remove entries that don't have granted ranks || traitConfig.Entries[i].Rank == 0) // ... 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.RemoveAt(i); else traitConfig.Entries[i].Rank = 0; // revalidate entire config - a removed entry will invalidate all other entries that depend on it i = 0; } else ++i; } Dictionary subtrees = new(); foreach (TraitEntryPacket traitEntry in traitConfig.Entries) { Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID); var entryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == traitEntry.TraitNodeEntryID); Cypher.Assert(entryItr != null); if (!subtrees.ContainsKey(entryItr.Data.TraitSubTreeID)) subtrees[entryItr.Data.TraitSubTreeID] = new(); if (node.Data.GetNodeType() == TraitNodeType.SubTreeSelection) subtrees[entryItr.Data.TraitSubTreeID].IsSelected = true; if (node.Data.TraitSubTreeID != 0) subtrees[node.Data.TraitSubTreeID].Entries.Add(traitEntry); } foreach (TraitSubTreeCachePacket subTree in traitConfig.SubTrees) subTree.Active = false; foreach (var (selectedSubTreeId, data) in subtrees) { var subtreeDataItr = traitConfig.SubTrees.Find(p => p.TraitSubTreeID == selectedSubTreeId); if (subtreeDataItr == null) { subtreeDataItr = new TraitSubTreeCachePacket(); subtreeDataItr.TraitSubTreeID = selectedSubTreeId; traitConfig.SubTrees.Add(subtreeDataItr); } subtreeDataItr.Entries = data.Entries; subtreeDataItr.Active = data.IsSelected; } Dictionary grantedCurrencies = new(); FillOwnedCurrenciesMap(traitConfig, player, grantedCurrencies); foreach (var (traitCurrencyId, spentAmount) in spentCurrencies) { if (CliDB.TraitCurrencyStorage.LookupByKey(traitCurrencyId).GetCurrencyType() != TraitCurrencyType.TraitSourced) continue; if (spentAmount == 0) continue; int grantedCount = grantedCurrencies.LookupByKey(traitCurrencyId); if (grantedCount == 0 || grantedCount < spentAmount) return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree; } if (requireSpendingAllCurrencies && traitConfig.Type == TraitConfigType.Combat) { // client checks only first two currencies for trait tree foreach (int traitCurrencyId in GetClassAndSpecTreeCurrencies(traitConfig)) { int grantedAmount = grantedCurrencies.LookupByKey(traitCurrencyId); if (grantedAmount == 0) continue; int spentAmount = spentCurrencies.LookupByKey(traitCurrencyId); if (spentAmount == 0 || spentAmount != grantedAmount) return TalentLearnResult.UnspentTalentPoints; } foreach (var (selectedTraitSubTreeId, data) in subtrees) { if (!data.IsSelected) continue; foreach (TraitCurrencyRecord subTreeCurrency in GetSubTreeCurrency(selectedTraitSubTreeId)) { int grantedAmount = grantedCurrencies.LookupByKey(subTreeCurrency.Id); if (grantedAmount == 0) continue; int spentAmount = spentCurrencies.LookupByKey(subTreeCurrency.Id); if (spentAmount == 0 || spentAmount != grantedAmount) return TalentLearnResult.UnspentTalentPoints; } } } return TalentLearnResult.LearnOk; } public static bool CanApplyTraitNode(TraitConfig traitConfig, TraitEntry traitEntry) { Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID); if (node == null) return false; if (node.Data.TraitSubTreeID != 0) { var subTreeItr = traitConfig.SubTrees._values.Find(p => p.TraitSubTreeID == node.Data.TraitSubTreeID); if (subTreeItr == null || subTreeItr.Active == 0) return false; } return true; } public static List GetTraitDefinitionEffectPointModifiers(int traitDefinitionId) { return _traitDefinitionEffectPointModifiers.LookupByKey(traitDefinitionId); } public static void InitializeStarterBuildTraitConfig(TraitConfigPacket traitConfig, Player player) { traitConfig.Entries.Clear(); var trees = GetTreesForConfig(traitConfig); if (trees == null) return; foreach (TraitEntry grant in GetGrantedTraitEntriesForConfig(traitConfig, player)) { TraitEntryPacket newEntry = new(); newEntry.TraitNodeID = grant.TraitNodeID; newEntry.TraitNodeEntryID = grant.TraitNodeEntryID; newEntry.GrantedRanks = grant.GrantedRanks; traitConfig.Entries.Add(newEntry); } Dictionary currencies = new(); FillOwnedCurrenciesMap(traitConfig, player, currencies); var loadoutEntries = _traitTreeLoadoutsByChrSpecialization.LookupByKey(traitConfig.ChrSpecializationID); if (loadoutEntries != null) { TraitEntryPacket findEntry(TraitConfigPacket config, int traitNodeId, int traitNodeEntryId) { return config.Entries.Find(traitEntry => traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId); } foreach (TraitTreeLoadoutEntryRecord loadoutEntry in loadoutEntries) { int addedRanks = loadoutEntry.NumPoints; Node node = _traitNodes.LookupByKey(loadoutEntry.SelectedTraitNodeID); TraitEntryPacket newEntry = new(); newEntry.TraitNodeID = loadoutEntry.SelectedTraitNodeID; newEntry.TraitNodeEntryID = loadoutEntry.SelectedTraitNodeEntryID; if (newEntry.TraitNodeEntryID == 0) newEntry.TraitNodeEntryID = (int)node.Entries[0].Data.Id; TraitEntryPacket entryInConfig = findEntry(traitConfig, newEntry.TraitNodeID, newEntry.TraitNodeEntryID); if (entryInConfig != null) addedRanks -= entryInConfig.Rank; newEntry.Rank = addedRanks; if (!HasEnoughCurrency(newEntry, currencies)) continue; if (entryInConfig != null) entryInConfig.Rank += addedRanks; else traitConfig.Entries.Add(newEntry); TakeCurrencyCost(newEntry, currencies); } } } } class NodeEntry { public TraitNodeEntryRecord Data; public List Conditions = new(); public List Costs = new(); } class Node { public TraitNodeRecord Data; public List Entries = new(); public List Groups = new(); public List> ParentNodes = new(); // TraitEdge::LeftTraitNodeID public List Conditions = new(); public List Costs = new(); } class NodeGroup { public TraitNodeGroupRecord Data; public List Conditions = new(); public List Costs = new(); public List Nodes = new(); } class SubTree { public TraitSubTreeRecord Data; public List Nodes = new(); public List Currencies = new(); } class Tree { public TraitTreeRecord Data; public List Nodes = new(); public List Costs = new(); public List Currencies = new(); public List SubTrees = new(); public TraitConfigType ConfigType; } class SubtreeValidationData { public List Entries = new(); public bool IsSelected; } }