From 957ca9898d490fa46192ee38b0cc1d75149841e8 Mon Sep 17 00:00:00 2001 From: Hondacrx Date: Mon, 2 Sep 2024 22:46:11 -0400 Subject: [PATCH] Core/Players: Fixed trait configs getting into invalid state after talent tree changes between client patches Port From (https://github.com/TrinityCore/TrinityCore/commit/a7c8ac3505cb7e177b6f5299989d544a3b8cade9) --- .../Database/Databases/CharacterDatabase.cs | 4 +- Source/Game/Entities/Player/Player.DB.cs | 32 ++++++++++--- Source/Game/Spells/TraitManager.cs | 47 ++++++++++++++----- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index 8859df104..6c3cef083 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -641,8 +641,8 @@ namespace Framework.Database PrepareStatement(CharStatements.UPD_CHAR_LIST_SLOT, "UPDATE characters SET slot = ? WHERE guid = ? AND account = ?"); PrepareStatement(CharStatements.INS_CHAR_FISHINGSTEPS, "INSERT INTO character_fishingsteps (guid, fishingSteps) VALUES (?, ?)"); PrepareStatement(CharStatements.DEL_CHAR_FISHINGSTEPS, "DELETE FROM character_fishingsteps WHERE guid = ?"); - PrepareStatement(CharStatements.SEL_CHAR_TRAIT_ENTRIES, "SELECT traitConfigId, traitNodeId, traitNodeEntryId, `rank`, grantedRanks FROM character_trait_entry WHERE guid = ?"); - PrepareStatement(CharStatements.INS_CHAR_TRAIT_ENTRIES, "INSERT INTO character_trait_entry (guid, traitConfigId, traitNodeId, traitNodeEntryId, `rank`, grantedRanks) VALUES (?, ?, ?, ?, ?, ?)"); + PrepareStatement(CharStatements.SEL_CHAR_TRAIT_ENTRIES, "SELECT traitConfigId, traitNodeId, traitNodeEntryId, `rank` FROM character_trait_entry WHERE guid = ?"); + PrepareStatement(CharStatements.INS_CHAR_TRAIT_ENTRIES, "INSERT INTO character_trait_entry (guid, traitConfigId, traitNodeId, traitNodeEntryId, `rank`) VALUES (?, ?, ?, ?, ?)"); PrepareStatement(CharStatements.DEL_CHAR_TRAIT_ENTRIES, "DELETE FROM character_trait_entry WHERE guid = ? AND traitConfigId = ?"); PrepareStatement(CharStatements.DEL_CHAR_TRAIT_ENTRIES_BY_CHAR, "DELETE FROM character_trait_entry WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHAR_TRAIT_CONFIGS, "SELECT traitConfigId, type, chrSpecializationId, combatConfigFlags, localIdentifier, skillLineId, traitSystemId, `name` FROM character_trait_config WHERE guid = ?"); diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 99ad0f699..c7dee2747 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -1057,15 +1057,14 @@ namespace Game.Entities MultiMap traitEntriesByConfig = new(); if (!entriesResult.IsEmpty()) { - // 0 1, 2 3 4 - // SELECT traitConfigId, traitNodeId, traitNodeEntryId, rank, grantedRanks FROM character_trait_entry WHERE guid = ? + // 0 1 2 3 + // SELECT traitConfigId, traitNodeId, traitNodeEntryId, rank FROM character_trait_entry WHERE guid = ? do { TraitEntryPacket traitEntry = new(); traitEntry.TraitNodeID = entriesResult.Read(1); traitEntry.TraitNodeEntryID = entriesResult.Read(2); traitEntry.Rank = entriesResult.Read(3); - traitEntry.GrantedRanks = entriesResult.Read(4); if (!TraitMgr.IsValidEntry(traitEntry)) continue; @@ -1103,10 +1102,26 @@ namespace Game.Entities traitConfig.Name = configsResult.Read(7); - foreach (var traitEntry in traitEntriesByConfig.LookupByKey(traitConfig.ID)) - traitConfig.Entries.Add(traitEntry); + foreach (var grantedEntry in TraitMgr.GetGrantedTraitEntriesForConfig(traitConfig, this)) + traitConfig.Entries.Add(new TraitEntryPacket(grantedEntry)); - if (TraitMgr.ValidateConfig(traitConfig, this) != TalentLearnResult.LearnOk) + var loadedEntriesNode = traitEntriesByConfig.LookupByKey(traitConfig.ID); + if (loadedEntriesNode != null) + { + foreach (var loadedEntry in loadedEntriesNode) + { + var itr = traitConfig.Entries.Find(entry => entry.TraitNodeID == loadedEntry.TraitNodeID && entry.TraitNodeEntryID == loadedEntry.TraitNodeEntryID); + if (itr == null) + { + traitConfig.Entries.Add(traitConfig.Entries.Last()); + traitConfig.Entries[^1].TraitNodeID = loadedEntry.TraitNodeID; + traitConfig.Entries[^1].TraitNodeEntryID = loadedEntry.TraitNodeEntryID; + } + itr.Rank = loadedEntry.Rank; + } + } + + if (TraitMgr.ValidateConfig(traitConfig, this, false, true) != TalentLearnResult.LearnOk) { traitConfig.Entries.Clear(); foreach (TraitEntry grantedEntry in TraitMgr.GetGrantedTraitEntriesForConfig(traitConfig, this)) @@ -1118,6 +1133,10 @@ namespace Game.Entities } while (configsResult.NextRow()); } + // Remove orphaned trait entries from database + foreach (var (traitConfigID, _) in traitEntriesByConfig) + m_traitConfigStates[traitConfigID] = PlayerSpellState.Removed; + bool hasConfigForSpec(int specId) { return m_activePlayerData.TraitConfigs.FindIndexIf(traitConfig => @@ -2430,7 +2449,6 @@ namespace Game.Entities stmt.AddValue(2, traitEntry.TraitNodeID); stmt.AddValue(3, traitEntry.TraitNodeEntryID); stmt.AddValue(4, traitEntry.Rank); - stmt.AddValue(5, traitEntry.GrantedRanks); trans.Append(stmt); } } diff --git a/Source/Game/Spells/TraitManager.cs b/Source/Game/Spells/TraitManager.cs index 10e7d0d73..c846c527b 100644 --- a/Source/Game/Spells/TraitManager.cs +++ b/Source/Game/Spells/TraitManager.cs @@ -360,7 +360,7 @@ namespace Game bool hasTraitNodeEntry(int traitNodeEntryId) { return traitConfig.Entries.Any(traitEntry => traitEntry.TraitNodeEntryID == traitNodeEntryId && (traitEntry.Rank > 0 || traitEntry.GrantedRanks > 0)); - } + } foreach (Tree tree in trees) { @@ -524,19 +524,21 @@ namespace Game if (trees == null) return entries; - TraitEntry getOrCreateEntry(uint nodeId, uint entryId) + void addGrantedRankToEntry(uint nodeId, NodeEntry entry, int grantedRanks) { - TraitEntry foundTraitEntry = entries.Find(traitEntry => traitEntry.TraitNodeID == nodeId && traitEntry.TraitNodeEntryID == entryId); + 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)entryId; + foundTraitEntry.TraitNodeEntryID = (int)entry.Data.Id; foundTraitEntry.Rank = 0; foundTraitEntry.GrantedRanks = 0; entries.Add(foundTraitEntry); } - return foundTraitEntry; + foundTraitEntry.GrantedRanks += grantedRanks; + if (foundTraitEntry.GrantedRanks > entry.Data.MaxRanks) + foundTraitEntry.GrantedRanks = entry.Data.MaxRanks; } Dictionary cachedCurrencies = null; @@ -548,18 +550,18 @@ namespace Game foreach (NodeEntry entry in node.Entries) foreach (TraitCondRecord condition in entry.Conditions) if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies)) - getOrCreateEntry(node.Data.Id, entry.Data.Id).GrantedRanks += condition.GrantedRanks; + 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) - getOrCreateEntry(node.Data.Id, entry.Data.Id).GrantedRanks += condition.GrantedRanks; + 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) - getOrCreateEntry(node.Data.Id, entry.Data.Id).GrantedRanks += condition.GrantedRanks; + addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks); } } @@ -582,7 +584,7 @@ namespace Game return true; } - public static TalentLearnResult ValidateConfig(TraitConfigPacket traitConfig, Player player, bool requireSpendingAllCurrencies = false) + public static TalentLearnResult ValidateConfig(TraitConfigPacket traitConfig, Player player, bool requireSpendingAllCurrencies = false, bool removeInvalidEntries = false) { int getNodeEntryCount(int traitNodeId) { @@ -630,13 +632,13 @@ namespace Game return !hasConditions; } - foreach (TraitEntryPacket traitEntry in traitConfig.Entries) + var isValidTraitEntry = TalentLearnResult (TraitEntryPacket traitEntry) => { if (!IsValidEntry(traitEntry)) return TalentLearnResult.FailedUnknown; Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID); - if (node.Data.GetNodeType() == TraitNodeType.Selection) + if (node.Data.GetNodeType() == TraitNodeType.Selection || node.Data.GetNodeType() == TraitNodeType.SubTreeSelection) if (getNodeEntryCount(traitEntry.TraitNodeID) != 1) return TalentLearnResult.FailedUnknown; @@ -670,6 +672,29 @@ namespace Game 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; + + 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 grantedCurrencies = new();