diff --git a/Source/Framework/Database/Databases/HotfixDatabase.cs b/Source/Framework/Database/Databases/HotfixDatabase.cs index fac97ffce..40a410f0a 100644 --- a/Source/Framework/Database/Databases/HotfixDatabase.cs +++ b/Source/Framework/Database/Databases/HotfixDatabase.cs @@ -773,6 +773,9 @@ namespace Framework.Database PrepareStatement(HotfixStatements.SEL_OVERRIDE_SPELL_DATA, "SELECT ID, Spells1, Spells2, Spells3, Spells4, Spells5, Spells6, Spells7, Spells8, Spells9, " + "Spells10, PlayerActionBarFileDataID, Flags FROM override_spell_data"); + // ParagonReputation.db2 + PrepareStatement(HotfixStatements.SEL_PARAGON_REPUTATION, "SELECT ID, FactionID, LevelThreshold, QuestID FROM paragon_reputation"); + // Phase.db2 PrepareStatement(HotfixStatements.SEL_PHASE, "SELECT ID, Flags FROM phase"); @@ -1586,6 +1589,8 @@ namespace Framework.Database SEL_OVERRIDE_SPELL_DATA, + SEL_PARAGON_REPUTATION, + SEL_PHASE, SEL_PHASE_X_PHASE_GROUP, diff --git a/Source/Game/Conditions/ConditionManager.cs b/Source/Game/Conditions/ConditionManager.cs index 7ccc96f74..b12d211b2 100644 --- a/Source/Game/Conditions/ConditionManager.cs +++ b/Source/Game/Conditions/ConditionManager.cs @@ -1152,8 +1152,7 @@ namespace Game } case ConditionTypes.ReputationRank: { - FactionRecord factionEntry = CliDB.FactionStorage.LookupByKey(cond.ConditionValue1); - if (factionEntry == null) + if (!CliDB.FactionStorage.ContainsKey(cond.ConditionValue1)) { Log.outError(LogFilter.Sql, "{0} has non existing faction ({1}), skipped.", cond.ToString(true), cond.ConditionValue1); return false; diff --git a/Source/Game/DataStorage/CliDB.cs b/Source/Game/DataStorage/CliDB.cs index cd5839163..22cfce681 100644 --- a/Source/Game/DataStorage/CliDB.cs +++ b/Source/Game/DataStorage/CliDB.cs @@ -227,6 +227,7 @@ namespace Game.DataStorage NamesReservedLocaleStorage = ReadDB2("NamesReservedLocale.db2", HotfixStatements.SEL_NAMES_RESERVED_LOCALE); NumTalentsAtLevelStorage = ReadDB2("NumTalentsAtLevel.db2", HotfixStatements.SEL_NUM_TALENTS_AT_LEVEL); OverrideSpellDataStorage = ReadDB2("OverrideSpellData.db2", HotfixStatements.SEL_OVERRIDE_SPELL_DATA); + ParagonReputationStorage = ReadDB2("ParagonReputation.db2", HotfixStatements.SEL_PARAGON_REPUTATION); PhaseStorage = ReadDB2("Phase.db2", HotfixStatements.SEL_PHASE); PhaseXPhaseGroupStorage = ReadDB2("PhaseXPhaseGroup.db2", HotfixStatements.SEL_PHASE_X_PHASE_GROUP); PlayerConditionStorage = ReadDB2("PlayerCondition.db2", HotfixStatements.SEL_PLAYER_CONDITION, HotfixStatements.SEL_PLAYER_CONDITION_LOCALE); @@ -594,6 +595,7 @@ namespace Game.DataStorage public static DB6Storage NamesReservedLocaleStorage; public static DB6Storage NumTalentsAtLevelStorage; public static DB6Storage OverrideSpellDataStorage; + public static DB6Storage ParagonReputationStorage; public static DB6Storage PhaseStorage; public static DB6Storage PhaseXPhaseGroupStorage; public static DB6Storage PlayerConditionStorage; diff --git a/Source/Game/DataStorage/DB2Manager.cs b/Source/Game/DataStorage/DB2Manager.cs index 21b7619be..0c926e7b0 100644 --- a/Source/Game/DataStorage/DB2Manager.cs +++ b/Source/Game/DataStorage/DB2Manager.cs @@ -414,6 +414,10 @@ namespace Game.DataStorage } } + foreach (ParagonReputationRecord paragonReputation in CliDB.ParagonReputationStorage.Values) + if (CliDB.FactionStorage.HasRecord(paragonReputation.FactionID)) + _paragonReputations[paragonReputation.FactionID] = paragonReputation; + foreach (var group in CliDB.PhaseXPhaseGroupStorage.Values) { PhaseRecord phase = CliDB.PhaseStorage.LookupByKey(group.PhaseId); @@ -1722,6 +1726,11 @@ namespace Game.DataStorage return 0; } + public ParagonReputationRecord GetParagonReputation(uint factionId) + { + return _paragonReputations.LookupByKey(factionId); + } + public PvpDifficultyRecord GetBattlegroundBracketByLevel(uint mapid, uint level) { PvpDifficultyRecord maxEntry = null; // used for level > max listed level case @@ -2275,6 +2284,7 @@ namespace Game.DataStorage MultiMap _mountDisplays = new(); Dictionary[]> _nameGenData = new(); List[] _nameValidators = new List[(int)Locale.Total + 1]; + Dictionary _paragonReputations = new(); MultiMap _phasesByGroup = new(); Dictionary _powerTypes = new(); Dictionary _pvpItemBonus = new(); diff --git a/Source/Game/DataStorage/Structs/P_Records.cs b/Source/Game/DataStorage/Structs/P_Records.cs index e4e1c5b28..409471eb4 100644 --- a/Source/Game/DataStorage/Structs/P_Records.cs +++ b/Source/Game/DataStorage/Structs/P_Records.cs @@ -19,6 +19,14 @@ using Framework.Constants; namespace Game.DataStorage { + public sealed class ParagonReputationRecord + { + public uint Id; + public uint FactionID; + public int LevelThreshold; + public int QuestID; + } + public sealed class PhaseRecord { public uint Id; diff --git a/Source/Game/Globals/ObjectManager.cs b/Source/Game/Globals/ObjectManager.cs index 1e21fe978..d2c4ae145 100644 --- a/Source/Game/Globals/ObjectManager.cs +++ b/Source/Game/Globals/ObjectManager.cs @@ -7442,6 +7442,14 @@ namespace Game } } + // Make all paragon reward quests repeatable + foreach (ParagonReputationRecord paragonReputation in CliDB.ParagonReputationStorage.Values) + { + Quest quest = GetQuestTemplate((uint)paragonReputation.QuestID); + if (quest != null) + quest.SetSpecialFlag(QuestSpecialFlags.Repeatable); + } + Log.outInfo(LogFilter.ServerLoading, "Loaded {0} quests definitions in {1} ms", _questTemplates.Count, Time.GetMSTimeDiffToNow(oldMSTime)); } public void LoadQuestStartersAndEnders() diff --git a/Source/Game/Reputation/ReputationManager.cs b/Source/Game/Reputation/ReputationManager.cs index 49b2d430d..6811745e8 100644 --- a/Source/Game/Reputation/ReputationManager.cs +++ b/Source/Game/Reputation/ReputationManager.cs @@ -121,12 +121,32 @@ namespace Game int GetMaxReputation(FactionRecord factionEntry) { + ParagonReputationRecord paragonReputation = Global.DB2Mgr.GetParagonReputation(factionEntry.Id); + if (paragonReputation != null) + { + // has reward quest, cap is just before threshold for another quest reward + // for example: if current reputation is 12345 and questa are given every 10000 and player has unclaimed reward + // then cap will be 19999 + + // otherwise cap is one theshold level larger + // if current reputation is 12345 and questa are given every 10000 and player does NOT have unclaimed reward + // then cap will be 29999 + + int reputation = GetReputation(factionEntry); + int cap = reputation + paragonReputation.LevelThreshold - reputation % paragonReputation.LevelThreshold - 1; + + if (_player.GetQuestStatus((uint)paragonReputation.QuestID) == QuestStatus.None) + cap += paragonReputation.LevelThreshold; + + return cap; + } + var friendshipReactions = Global.DB2Mgr.GetFriendshipRepReactions(factionEntry.FriendshipRepID); if (!friendshipReactions.Empty()) return friendshipReactions.LastOrDefault().ReactionThreshold; int dataIndex = GetFactionDataIndexForRaceAndClass(factionEntry); - if (dataIndex >= 0 && factionEntry.ReputationMax[dataIndex] != 0) + if (dataIndex >= 0) return factionEntry.ReputationMax[dataIndex]; return ReputationRankThresholds.LastOrDefault(); @@ -162,6 +182,23 @@ namespace Game return GetForcedRankIfAny(factionTemplateEntry.Faction); } + int GetParagonLevel(uint paragonFactionId) + { + return GetParagonLevel(CliDB.FactionStorage.LookupByKey(paragonFactionId)); + } + + int GetParagonLevel(FactionRecord paragonFactionEntry) + { + if (paragonFactionEntry == null) + return 0; + + ParagonReputationRecord paragonReputation = Global.DB2Mgr.GetParagonReputation(paragonFactionEntry.Id); + if (paragonReputation != null) + return GetReputation(paragonFactionEntry) / paragonReputation.LevelThreshold; + + return 0; + } + public void ApplyForceReaction(uint faction_id, ReputationRank rank, bool apply) { if (apply) @@ -172,11 +209,16 @@ namespace Game ReputationFlags GetDefaultStateFlags(FactionRecord factionEntry) { - int dataIndex = GetFactionDataIndexForRaceAndClass(factionEntry); - if (dataIndex < 0) - return ReputationFlags.None; + ReputationFlags flags = ReputationFlags.None; - return (ReputationFlags)factionEntry.ReputationFlags[dataIndex]; + int dataIndex = GetFactionDataIndexForRaceAndClass(factionEntry); + if (dataIndex > 0) + flags = (ReputationFlags)factionEntry.ReputationFlags[dataIndex]; + + if (Global.DB2Mgr.GetParagonReputation(factionEntry.Id) != null) + flags |= ReputationFlags.ShowPropagated; + + return flags; } public void SendForceReactions() @@ -356,12 +398,22 @@ namespace Game var faction = _factions.LookupByKey(factionEntry.ReputationIndex); if (faction != null) { - // if we update spillover only, do not update main reputation (rank exceeds creature reward rate) - if (!spillOverOnly) - res = SetOneFactionReputation(factionEntry, standing, incremental); + FactionRecord primaryFactionToModify = factionEntry; + if (incremental && standing > 0 && CanGainParagonReputationForFaction(factionEntry)) + { + primaryFactionToModify = CliDB.FactionStorage.LookupByKey(factionEntry.ParagonFactionID); + faction = _factions.LookupByKey(primaryFactionToModify.ReputationIndex); + } - // only this faction gets reported to client, even if it has no own visible standing - SendState(faction); + if (faction != null) + { + // if we update spillover only, do not update main reputation (rank exceeds creature reward rate) + if (!spillOverOnly) + res = SetOneFactionReputation(primaryFactionToModify, standing, incremental); + + // only this faction gets reported to client, even if it has no own visible standing + SendState(faction); + } } return res; } @@ -388,6 +440,7 @@ namespace Game ReputationRank old_rank = ReputationToRank(factionEntry, factionState.Standing + BaseRep); ReputationRank new_rank = ReputationToRank(factionEntry, standing); + int oldStanding = factionState.Standing + BaseRep; int newStanding = standing - BaseRep; _player.ReputationChanged(factionEntry, newStanding - factionState.Standing); @@ -404,7 +457,20 @@ namespace Game if (new_rank > old_rank) _sendFactionIncreased = true; - if (factionEntry.FriendshipRepID == 0) + ParagonReputationRecord paragonReputation = Global.DB2Mgr.GetParagonReputation(factionEntry.Id); + if (paragonReputation != null) + { + int oldParagonLevel = oldStanding / paragonReputation.LevelThreshold; + int newParagonLevel = standing / paragonReputation.LevelThreshold; + if (oldParagonLevel != newParagonLevel) + { + Quest paragonRewardQuest = Global.ObjectMgr.GetQuestTemplate((uint)paragonReputation.QuestID); + if (paragonRewardQuest != null) + _player.AddQuestAndCheckCompletion(paragonRewardQuest, null); + } + } + + if (factionEntry.FriendshipRepID == 0 && paragonReputation == null) UpdateRankCounters(old_rank, new_rank); _player.UpdateCriteria(CriteriaTypes.KnownFactions, factionEntry.Id); @@ -451,6 +517,9 @@ namespace Game if (faction.Flags.HasFlag(ReputationFlags.Header) && !faction.Flags.HasFlag(ReputationFlags.HeaderShowsBar)) return; + if (Global.DB2Mgr.GetParagonReputation(faction.Id) != null) + return; + // already set if (faction.Flags.HasFlag(ReputationFlags.Visible)) return; @@ -640,6 +709,25 @@ namespace Game return -1; } + + bool CanGainParagonReputationForFaction(FactionRecord factionEntry) + { + if (!CliDB.FactionStorage.ContainsKey(factionEntry.ParagonFactionID)) + return false; + + if (GetRank(factionEntry) != ReputationRank.Exalted) + return false; + + ParagonReputationRecord paragonReputation = Global.DB2Mgr.GetParagonReputation(factionEntry.ParagonFactionID); + if (paragonReputation == null) + return false; + + Quest quest = Global.ObjectMgr.GetQuestTemplate((uint)paragonReputation.QuestID); + if (quest == null) + return false; + + return _player.GetLevel() >= _player.GetQuestMinLevel(quest); + } public byte GetVisibleFactionCount() { return _visibleFactionCount; } diff --git a/sql/updates/hotfixes/master/2021_06_06_00_hotfixes.sql b/sql/updates/hotfixes/master/2021_06_06_00_hotfixes.sql new file mode 100644 index 000000000..4f98b0770 --- /dev/null +++ b/sql/updates/hotfixes/master/2021_06_06_00_hotfixes.sql @@ -0,0 +1,15 @@ +-- +-- Table structure for table `paragon_reputation` +-- +DROP TABLE IF EXISTS `paragon_reputation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `paragon_reputation` ( + `ID` int(10) unsigned NOT NULL DEFAULT '0', + `FactionID` int(10) unsigned NOT NULL DEFAULT '0', + `LevelThreshold` int(11) NOT NULL DEFAULT '0', + `QuestID` int(11) NOT NULL DEFAULT '0', + `VerifiedBuild` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */;