From 751f063936d62c03b4c4c31508ea0e5dc1c0d44e Mon Sep 17 00:00:00 2001 From: luis Date: Wed, 8 Jul 2026 21:41:23 -0300 Subject: [PATCH] feat(covenant): P3 - grant RenownRewards on covenant renown level-up The covenant renown LEVEL is already a renown-reputation handled by TC's ReputationMgr and synced to the client by the standard reputation packets, so the only covenant-specific gap was granting the per-level rewards. - Add the RenownRewards.db2 store (Meta 0x30AFBCF8, ParentIndex=CovenantID) and a DB2Manager (covenantId, level) -> rewards map (GetRenownRewards). - Track the highest renown level whose rewards were granted, per covenant (Player::m_renownRewardsGranted), persisted to a new character_covenant_renown table and loaded at login, so each level's rewards are granted exactly once. - Player::ReputationChanged now grants rewards for every renown level crossed when a covenant renown reputation increases; a login catch-up (UpdateAllRenownRewards, run in-world after SendInitialPacketsAfterAddToMap) grants anything earned before this feature existed. UpdateRenownRewards is guarded on IsInWorld() so nothing is granted mid-load. - GrantRenownReward grants item / spell / title / mount via the existing Player + CollectionMgr APIs. The cosmetic-collection fields RenownRewards also carries (TransmogID/TransmogSetID/TransmogIllusionID, GarrFollowerID, QuestID) are documented as follow-ups rather than half-implemented. --- .../master/2026_07_08_00_characters.sql | 8 ++ .../Implementation/CharacterDatabase.cpp | 3 + .../Implementation/CharacterDatabase.h | 3 + src/server/game/DataStores/DB2Stores.cpp | 9 ++ src/server/game/DataStores/DB2Stores.h | 1 + src/server/game/Entities/Player/Player.cpp | 90 +++++++++++++++++++ src/server/game/Entities/Player/Player.h | 9 ++ src/server/game/Handlers/CharacterHandler.cpp | 10 +++ 8 files changed, 133 insertions(+) create mode 100644 sql/updates/characters/master/2026_07_08_00_characters.sql diff --git a/sql/updates/characters/master/2026_07_08_00_characters.sql b/sql/updates/characters/master/2026_07_08_00_characters.sql new file mode 100644 index 000000000..a6c485ab6 --- /dev/null +++ b/sql/updates/characters/master/2026_07_08_00_characters.sql @@ -0,0 +1,8 @@ + +DROP TABLE IF EXISTS `character_covenant_renown`; +CREATE TABLE `character_covenant_renown` ( + `guid` bigint unsigned NOT NULL DEFAULT '0', + `covenantId` int unsigned NOT NULL DEFAULT '0', + `grantedLevel` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`covenantId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Highest covenant renown level whose rewards were granted'; \ No newline at end of file diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index f29595770..e7e712fdc 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -1087,6 +1087,9 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_UPD_RECENT_ALLY_NOTE, "UPDATE character_recent_allies SET note = ? WHERE ownerGuid = ? AND allyGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_RECENT_ALLY_SETTING, "SELECT allowSeeLocation FROM character_recent_ally_settings WHERE ownerGuid = ?", CONNECTION_SYNCH); PrepareStatement(CHAR_REP_RECENT_ALLY_SETTING, "REPLACE INTO character_recent_ally_settings (ownerGuid, allowSeeLocation) VALUES (?, ?)", CONNECTION_ASYNC); + + PrepareStatement(CHAR_SEL_CHARACTER_COVENANT_RENOWN, "SELECT covenantId, grantedLevel FROM character_covenant_renown WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_REP_CHARACTER_COVENANT_RENOWN, "REPLACE INTO character_covenant_renown (guid, covenantId, grantedLevel) VALUES (?, ?, ?)", CONNECTION_ASYNC); // WowCommunity end } diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index 9df7271a0..2acdd1935 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -885,6 +885,9 @@ enum CharacterDatabaseStatements : uint32 CHAR_UPD_RECENT_ALLY_NOTE, CHAR_SEL_RECENT_ALLY_SETTING, CHAR_REP_RECENT_ALLY_SETTING, + + CHAR_SEL_CHARACTER_COVENANT_RENOWN, + CHAR_REP_CHARACTER_COVENANT_RENOWN, //WowCommunity end MAX_CHARACTERDATABASE_STATEMENTS diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp index 1d7f529df..2dc17bd95 100644 --- a/src/server/game/DataStores/DB2Stores.cpp +++ b/src/server/game/DataStores/DB2Stores.cpp @@ -689,6 +689,7 @@ namespace std::unordered_map _chromieTimeExpansionInfo; std::unordered_map _delvesSeasonXSpellsBySeasonId; std::unordered_map _conduitsByItem; + std::unordered_map, std::vector> _renownRewards; // Crafting std::unordered_map> _MCRSpellSlotBySpell; std::unordered_map> _MCRCategoryByReagentSlot; @@ -1828,6 +1829,9 @@ void DB2Manager::IndexLoadedStores() if (conduitItem->ItemID > 0 && conduitItem->ConduitID > 0) _conduitsByItem[uint32(conduitItem->ItemID)] = uint32(conduitItem->ConduitID); + for (RenownRewardsEntry const* renownReward : sRenownRewardsStore) + _renownRewards[{ renownReward->CovenantID, renownReward->Level }].push_back(renownReward); + for (SpecializationSpellsEntry const* specSpells : sSpecializationSpellsStore) _specializationSpellsBySpec[specSpells->SpecID].push_back(specSpells); @@ -3850,3 +3854,8 @@ int32 DB2Manager::GetConduitRankForItemLevel(uint32 itemLevel) const } return bestRank; } + +std::vector const* DB2Manager::GetRenownRewards(int32 covenantId, int32 level) const +{ + return Trinity::Containers::MapGetValuePtr(_renownRewards, { covenantId, level }); +} diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h index 6d1fa9467..36dba3724 100644 --- a/src/server/game/DataStores/DB2Stores.h +++ b/src/server/game/DataStores/DB2Stores.h @@ -710,6 +710,7 @@ public: std::vector GetCraftingDataItemIDByCraftingData(uint32 craftingDataID); CraftingReagentQualityEntry const* GetCraftingReagentQualityByItem(uint32 itemID); uint32 GetCraftingQualityTierByDifficultyPercent(int32 craftingDifficultyID, uint32 difficultyPercent); + std::vector const* GetRenownRewards(int32 covenantId, int32 level) const; private: friend class DB2HotfixGeneratorBase; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 209cfa014..7ba3f250e 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -16701,6 +16701,10 @@ void Player::ReputationChanged(FactionEntry const* factionEntry, int32 change) UpdateQuestObjectiveProgress(QUEST_OBJECTIVE_MIN_REPUTATION, factionEntry->ID, change); UpdateQuestObjectiveProgress(QUEST_OBJECTIVE_MAX_REPUTATION, factionEntry->ID, change); UpdateQuestObjectiveProgress(QUEST_OBJECTIVE_INCREASE_REPUTATION, factionEntry->ID, change); + + // A covenant renown gain may cross one or more renown levels -> grant their RenownRewards. + if (change > 0 && GetReputationMgr().IsRenownReputation(factionEntry)) + UpdateRenownRewards(factionEntry); } void Player::CurrencyChanged(uint32 currencyId, int32 change) @@ -18671,6 +18675,7 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol _LoadCovenant(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_COVENANT)); _LoadSoulbindConduits(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SOULBIND_CONDUITS)); _LoadSoulbindConduitSockets(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SOULBIND_CONDUIT_SOCKETS)); + _LoadRenownRewards(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RENOWN_REWARDS)); ApplyConduitSpells(); // spell/aura systems are ready by here (mirrors _LoadGlyphAuras above) //WowCommunity _LoadInventory(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_INVENTORY), @@ -32823,3 +32828,88 @@ void Player::AssignEquipmentSetSpec(uint64 id, int32 specIndex) markChanged(itr->second); } + +void Player::_LoadRenownRewards(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + m_renownRewardsGranted[fields[0].GetUInt32()] = fields[1].GetUInt32(); + } while (result->NextRow()); +} + +void Player::GrantRenownReward(RenownRewardsEntry const* reward) +{ + if (!reward) + return; + + if (reward->SpellID > 0) + LearnSpell(uint32(reward->SpellID), false); + + if (reward->CharTitlesID > 0) + if (CharTitlesEntry const* title = sCharTitlesStore.LookupEntry(uint32(reward->CharTitlesID))) + SetTitle(title); + + if (reward->MountID > 0) + if (MountEntry const* mount = sMountStore.LookupEntry(uint32(reward->MountID))) + if (mount->SourceSpellID > 0) + GetSession()->GetCollectionMgr()->AddMount(uint32(mount->SourceSpellID), MOUNT_STATUS_NONE); + + if (reward->ItemID > 0) + AddItem(uint32(reward->ItemID), 1); + + // Cosmetic/collection reward fields RenownRewards also carries but that each need their own collection/garrison + // grant path (follow-up): TransmogID, TransmogSetID, TransmogIllusionID, GarrFollowerID, QuestID. +} + +void Player::UpdateRenownRewards(FactionEntry const* renownFaction) +{ + // Only act on live renown gains for an in-world player (avoids granting items mid-load); a player who already had + // renown before this feature is caught up on the first live renown change (all ungranted levels grant at once). + if (!renownFaction || !IsInWorld()) + return; + + if (!GetReputationMgr().IsRenownReputation(renownFaction)) + return; + + // Which covenant owns this renown faction? + uint32 covenantId = 0; + for (CovenantEntry const* covenant : sCovenantStore) + { + if (covenant->FactionID == int32(renownFaction->ID)) + { + covenantId = covenant->ID; + break; + } + } + if (!covenantId) + return; + + int32 currentLevel = GetReputationMgr().GetRenownLevel(renownFaction); + uint32& granted = m_renownRewardsGranted[covenantId]; + if (int32(granted) >= currentLevel) + return; + + for (int32 level = int32(granted) + 1; level <= currentLevel; ++level) + if (std::vector const* rewards = sDB2Manager.GetRenownRewards(int32(covenantId), level)) + for (RenownRewardsEntry const* reward : *rewards) + GrantRenownReward(reward); + + granted = uint32(currentLevel); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHARACTER_COVENANT_RENOWN); + stmt->setUInt64(0, GetGUID().GetCounter()); + stmt->setUInt32(1, covenantId); + stmt->setUInt32(2, granted); + CharacterDatabase.Execute(stmt); +} + +void Player::UpdateAllRenownRewards() +{ + for (CovenantEntry const* covenant : sCovenantStore) + if (FactionEntry const* faction = sFactionStore.LookupEntry(uint32(covenant->FactionID))) + UpdateRenownRewards(faction); +} diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 82f23434f..d03fdfac7 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1065,6 +1065,7 @@ enum PlayerLoginQueryIndex PLAYER_LOGIN_QUERY_LOAD_COVENANT, PLAYER_LOGIN_QUERY_LOAD_SOULBIND_CONDUITS, PLAYER_LOGIN_QUERY_LOAD_SOULBIND_CONDUIT_SOCKETS, + PLAYER_LOGIN_QUERY_LOAD_RENOWN_REWARDS, MAX_PLAYER_LOGIN_QUERY }; @@ -3030,6 +3031,11 @@ class TC_GAME_API Player final : public Unit, public GridObject void RemoveConduitSpells(); // strip conduit spells (on soulbind switch) int32 GetConduitSpell(uint32 conduitId) const; // owned rank -> SoulbindConduitRank.SpellID (0 if none) + // Covenant renown rewards. The renown LEVEL itself is a renown-reputation (TC ReputationMgr) and is client-synced + // by the standard reputation packets; this grants the per-level RenownRewards (item/spell/title/mount) once each. + void UpdateRenownRewards(FactionEntry const* renownFaction); + void UpdateAllRenownRewards(); // login catch-up: grant any renown rewards earned before this feature existed + bool IsAdvancedCombatLoggingEnabled() const { return _advancedCombatLoggingEnabled; } void SetAdvancedCombatLogging(bool enabled) { _advancedCombatLoggingEnabled = enabled; } @@ -3325,6 +3331,8 @@ class TC_GAME_API Player final : public Unit, public GridObject void _LoadCovenant(PreparedQueryResult result); void _LoadSoulbindConduits(PreparedQueryResult result); void _LoadSoulbindConduitSockets(PreparedQueryResult result); + void _LoadRenownRewards(PreparedQueryResult result); + void GrantRenownReward(RenownRewardsEntry const* reward); /*********************************************************/ /*** SAVE SYSTEM ***/ /*********************************************************/ @@ -3549,6 +3557,7 @@ class TC_GAME_API Player final : public Unit, public GridObject // Covenant / soulbind (server-authoritative; the client already knows its own active soulbind choice). uint32 m_activeCovenantId = 0; uint32 m_activeSoulbindId = 0; + std::unordered_map m_renownRewardsGranted; std::unordered_map m_soulbindConduits; // garrTalent node id -> (conduitId, garrTalentTreeID); tree id lets us apply only the active soulbind's sockets std::unordered_map> m_soulbindConduitSockets; diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index cc7e75f18..d7dedcb41 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -392,6 +392,11 @@ bool LoginQueryHolder::Initialize() stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SOULBIND_CONDUIT_SOCKET); stmt->setUInt64(0, lowGuid); res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SOULBIND_CONDUIT_SOCKETS, stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_COVENANT_RENOWN); + stmt->setUInt64(0, lowGuid); + res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RENOWN_REWARDS, stmt); + //WowCommunity return res; } @@ -1342,6 +1347,11 @@ void WorldSession::HandlePlayerLogin(LoginQueryHolder const& holder) pCurrChar->SendInitialPacketsAfterAddToMap(); + //WowCommunity + // Now in world: grant any covenant renown rewards earned before this character was last saved / before the feature existed. + pCurrChar->UpdateAllRenownRewards(); + //WowCommunity + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ONLINE); stmt->setUInt64(0, pCurrChar->GetGUID().GetCounter()); CharacterDatabase.Execute(stmt);