From 4991fb6dd52d05b738c5d6da4686cc522df5fd6c Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 29 Mar 2026 12:55:56 +0200 Subject: [PATCH 1/5] feat(Core/Scripting): Add `OnPlayerBeforeGetLevelForXPGain` hook --- .../game/Entities/Player/KillRewarder.cpp | 19 +++++++++++++------ .../game/Entities/Player/KillRewarder.h | 1 + src/server/game/Entities/Player/Player.cpp | 9 ++++++--- .../game/Entities/Player/PlayerQuest.cpp | 5 ++++- src/server/game/Miscellaneous/Formulas.cpp | 5 ++++- .../Scripting/ScriptDefines/PlayerScript.cpp | 5 +++++ .../Scripting/ScriptDefines/PlayerScript.h | 9 +++++++++ src/server/game/Scripting/ScriptMgr.h | 1 + 8 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/server/game/Entities/Player/KillRewarder.cpp b/src/server/game/Entities/Player/KillRewarder.cpp index 78d24b4dbc5ede..5d867c2d82f005 100644 --- a/src/server/game/Entities/Player/KillRewarder.cpp +++ b/src/server/game/Entities/Player/KillRewarder.cpp @@ -89,7 +89,7 @@ void KillRewarder::_InitGroupData() if (Player* member = itr->GetSource()) if ((_killer == member || member->IsAtGroupRewardDistance(_victim))) { - const uint8 lvl = member->GetLevel(); + const uint8 lvl = _GetPlayerLevel(member); if (member->IsAlive()) { // 2.1. _count - number of alive group members within reward distance; @@ -104,7 +104,7 @@ void KillRewarder::_InitGroupData() // 2.4. _maxNotGrayMember - maximum level of alive group member within reward distance, // for whom victim is not gray; uint32 grayLevel = Acore::XP::GetGrayLevel(lvl); - if (_victim->GetLevel() > grayLevel && (!_maxNotGrayMember || _maxNotGrayMember->GetLevel() < lvl)) + if (_victim->GetLevel() > grayLevel && (!_maxNotGrayMember || _GetPlayerLevel(_maxNotGrayMember) < lvl)) { _maxNotGrayMember = member; } @@ -114,7 +114,7 @@ void KillRewarder::_InitGroupData() } // 2.6. _isFullXP - flag identifying that for all group members victim is not gray, // so 100% XP will be rewarded (50% otherwise). - _isFullXP = _maxNotGrayMember && (_maxLevel == _maxNotGrayMember->GetLevel()); + _isFullXP = _maxNotGrayMember && (_maxLevel == _GetPlayerLevel(_maxNotGrayMember)); } else _count = 1; @@ -153,7 +153,7 @@ void KillRewarder::_RewardXP(Player* player, float rate) // * set to 0 if player's level is more than maximum level of not gray member; // * cut XP in half if _isFullXP is false. if (_maxNotGrayMember && player->IsAlive() && - _maxNotGrayMember->GetLevel() >= player->GetLevel()) + _GetPlayerLevel(_maxNotGrayMember) >= _GetPlayerLevel(player)) xp = _isFullXP ? uint32(xp * rate) : // Reward FULL XP if all group members are not gray. uint32(xp * rate / 2) + 1; // Reward only HALF of XP if some of group members are gray. @@ -208,8 +208,8 @@ void KillRewarder::_RewardPlayer(Player* player, bool isDungeon) // Give reputation and kill credit only in PvE. if (!_isPvP || _isBattleGround) { - float xpRate = _group ? _groupRate * float(player->GetLevel()) / _aliveSumLevel : /*Personal rate is 100%.*/ 1.0f; // Group rate depends on the sum of levels. - sScriptMgr->OnPlayerRewardKillRewarder(player, this, isDungeon, xpRate); // Personal rate is 100%. + float xpRate = _group ? _groupRate * float(_GetPlayerLevel(player)) / _aliveSumLevel : /*Personal rate is 100%.*/ 1.0f; // Group rate depends on the sum of levels. + sScriptMgr->OnPlayerRewardKillRewarder(player, this, isDungeon, xpRate); // Personal rate is 100%. if (_xp) { @@ -264,6 +264,13 @@ void KillRewarder::_RewardGroup() } } +uint8 KillRewarder::_GetPlayerLevel(Player const* player) +{ + uint8 level = player->GetLevel(); + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(player, level); + return level; +} + void KillRewarder::Reward() { // 3. Reward killer (and group, if necessary). diff --git a/src/server/game/Entities/Player/KillRewarder.h b/src/server/game/Entities/Player/KillRewarder.h index a91a2a04522ce1..910e26b2b8679d 100644 --- a/src/server/game/Entities/Player/KillRewarder.h +++ b/src/server/game/Entities/Player/KillRewarder.h @@ -43,6 +43,7 @@ class AC_GAME_API KillRewarder void _RewardKillCredit(Player* player); void _RewardPlayer(Player* player, bool isDungeon); void _RewardGroup(); + uint8 _GetPlayerLevel(Player const* player); Player* _killer; Unit* _victim; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index c32f0402ad3d26..82d9dd261a0c81 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -5775,17 +5775,20 @@ void Player::CheckAreaExploreAndOutdoor() if (areaEntry->area_level > 0) { - if (GetLevel() >= sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + uint8 playerLevel = GetLevel(); + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(this, playerLevel); + + if (playerLevel >= sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) { SendExplorationExperience(areaId, 0); } else { - int32 diff = int32(GetLevel()) - areaEntry->area_level; + int32 diff = int32(playerLevel) - areaEntry->area_level; uint32 XP = 0; if (diff < -5) { - XP = uint32(sObjectMgr->GetBaseXP(GetLevel() + 5) * sWorld->getRate(RATE_XP_EXPLORE)); + XP = uint32(sObjectMgr->GetBaseXP(playerLevel + 5) * sWorld->getRate(RATE_XP_EXPLORE)); } else if (diff > 5) { diff --git a/src/server/game/Entities/Player/PlayerQuest.cpp b/src/server/game/Entities/Player/PlayerQuest.cpp index edfe48c9ab3e6b..1a6ff32f197e3f 100644 --- a/src/server/game/Entities/Player/PlayerQuest.cpp +++ b/src/server/game/Entities/Player/PlayerQuest.cpp @@ -1435,8 +1435,11 @@ bool Player::TakeQuestSourceItem(uint32 questId, bool msg) uint32 Player::CalculateQuestRewardXP(Quest const* quest) { + uint8 level = GetLevel(); + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(this, level); + // apply world quest rate - uint32 xp = uint32(quest->XPValue(GetLevel()) * GetQuestRate(quest->IsDFQuest())); + uint32 xp = uint32(quest->XPValue(level) * GetQuestRate(quest->IsDFQuest())); // handle SPELL_AURA_MOD_XP_QUEST_PCT auras xp *= GetTotalAuraMultiplier(SPELL_AURA_MOD_XP_QUEST_PCT); diff --git a/src/server/game/Miscellaneous/Formulas.cpp b/src/server/game/Miscellaneous/Formulas.cpp index e8b6ea568a18f0..f2ee58c49ff30b 100644 --- a/src/server/game/Miscellaneous/Formulas.cpp +++ b/src/server/game/Miscellaneous/Formulas.cpp @@ -21,6 +21,7 @@ #include "Creature.h" #include "Log.h" #include "Player.h" +#include "ScriptMgr.h" #include "World.h" uint32 Acore::XP::BaseGain(uint8 pl_level, uint8 mob_level, ContentLevels content) @@ -79,7 +80,9 @@ uint32 Acore::XP::Gain(Player* player, Unit* unit, bool isBattleGround /*= false { float xpMod = 1.0f; - gain = BaseGain(player->GetLevel(), unit->GetLevel(), GetContentLevelsForMapAndZone(unit->GetMapId(), unit->GetZoneId())); + uint8 playerLevel = player->GetLevel(); + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(player, playerLevel); + gain = BaseGain(playerLevel, unit->GetLevel(), GetContentLevelsForMapAndZone(unit->GetMapId(), unit->GetZoneId())); if (gain && creature) { diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp index c33c13d7c08c73..61418a74ca05bf 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp @@ -930,6 +930,11 @@ void ScriptMgr::OnPlayerLearnTaxiNode(Player const* player, uint32 nodeId) CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_LEARN_TAXI_NODE, script->OnPlayerLearnTaxiNode(player, nodeId)); } +void ScriptMgr::OnPlayerBeforeGetLevelForXPGain(Player const* player, uint8& level) +{ + CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_BEFORE_GET_LEVEL_FOR_XP_GAIN, script->OnPlayerBeforeGetLevelForXPGain(player, level)); +} + PlayerScript::PlayerScript(const char* name, std::vector enabledHooks) : ScriptObject(name, PLAYERHOOK_END) { diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.h b/src/server/game/Scripting/ScriptDefines/PlayerScript.h index e25fc82b80a67a..d50e83a147996f 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.h +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.h @@ -211,6 +211,7 @@ enum PlayerHook PLAYERHOOK_ON_GIVE_REPUTATION, PLAYERHOOK_ON_GET_REPUTATION_PRICE_DISCOUNT, PLAYERHOOK_ON_LEARN_TAXI_NODE, + PLAYERHOOK_ON_BEFORE_GET_LEVEL_FOR_XP_GAIN, PLAYERHOOK_END }; @@ -829,6 +830,14 @@ class PlayerScript : public ScriptObject * @param nodeId The id of the learned taxi node */ virtual void OnPlayerLearnTaxiNode(Player const* /*player*/, uint32 /*nodeId*/) {} + + /** + * @brief This hook is called when XP is calculated for the player, and is used to modify the player level used in the XP formulas. + * + * @param player Contains information about the Player + * @param level The level that should be used for XP gain calculations + */ + virtual void OnPlayerBeforeGetLevelForXPGain(Player const* /*player*/, uint8& /*level*/) {} }; #endif diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index 070a97c09c0402..e4a1976e12f2a1 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -467,6 +467,7 @@ class ScriptMgr void OnPlayerGetReputationPriceDiscount(Player const* player, Creature const* creature, float& discount); void OnPlayerGetReputationPriceDiscount(Player const* player, FactionTemplateEntry const* factionTemplate, float& discount); void OnPlayerLearnTaxiNode(Player const* player, uint32 nodeId); + void OnPlayerBeforeGetLevelForXPGain(Player const* player, uint8& level); // Anti cheat void AnticheatSetCanFlybyServer(Player* player, bool apply); From 708d78f64fe228f6fac809e773710bc3fa960fcb Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 29 Mar 2026 16:56:27 +0200 Subject: [PATCH 2/5] fix: cache _maxNotGrayMemberLevel --- src/server/game/Entities/Player/KillRewarder.cpp | 9 +++++---- src/server/game/Entities/Player/KillRewarder.h | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/server/game/Entities/Player/KillRewarder.cpp b/src/server/game/Entities/Player/KillRewarder.cpp index 5d867c2d82f005..0bbb4c5413a0e9 100644 --- a/src/server/game/Entities/Player/KillRewarder.cpp +++ b/src/server/game/Entities/Player/KillRewarder.cpp @@ -67,7 +67,7 @@ KillRewarder::KillRewarder(Player* killer, Unit* victim, bool isBattleGround) : // 1. Initialize internal variables to default values. _killer(killer), _victim(victim), _group(killer->GetGroup()), - _groupRate(1.0f), _maxNotGrayMember(nullptr), _count(0), _aliveSumLevel(0), _sumLevel(0), _xp(0), + _groupRate(1.0f), _maxNotGrayMember(nullptr), _maxNotGrayMemberLevel(0), _count(0), _aliveSumLevel(0), _sumLevel(0), _xp(0), _isFullXP(false), _maxLevel(0), _isBattleGround(isBattleGround), _isPvP(false) { // mark the credit as pvp if victim is player @@ -104,9 +104,10 @@ void KillRewarder::_InitGroupData() // 2.4. _maxNotGrayMember - maximum level of alive group member within reward distance, // for whom victim is not gray; uint32 grayLevel = Acore::XP::GetGrayLevel(lvl); - if (_victim->GetLevel() > grayLevel && (!_maxNotGrayMember || _GetPlayerLevel(_maxNotGrayMember) < lvl)) + if (_victim->GetLevel() > grayLevel && (!_maxNotGrayMember || _maxNotGrayMemberLevel < lvl)) { _maxNotGrayMember = member; + _maxNotGrayMemberLevel = lvl; } } // 2.5. _sumLevel - sum of levels of group members within reward distance; @@ -114,7 +115,7 @@ void KillRewarder::_InitGroupData() } // 2.6. _isFullXP - flag identifying that for all group members victim is not gray, // so 100% XP will be rewarded (50% otherwise). - _isFullXP = _maxNotGrayMember && (_maxLevel == _GetPlayerLevel(_maxNotGrayMember)); + _isFullXP = _maxNotGrayMember && (_maxLevel == _maxNotGrayMemberLevel); } else _count = 1; @@ -153,7 +154,7 @@ void KillRewarder::_RewardXP(Player* player, float rate) // * set to 0 if player's level is more than maximum level of not gray member; // * cut XP in half if _isFullXP is false. if (_maxNotGrayMember && player->IsAlive() && - _GetPlayerLevel(_maxNotGrayMember) >= _GetPlayerLevel(player)) + _maxNotGrayMemberLevel >= _GetPlayerLevel(player)) xp = _isFullXP ? uint32(xp * rate) : // Reward FULL XP if all group members are not gray. uint32(xp * rate / 2) + 1; // Reward only HALF of XP if some of group members are gray. diff --git a/src/server/game/Entities/Player/KillRewarder.h b/src/server/game/Entities/Player/KillRewarder.h index 910e26b2b8679d..269d82604fc1e4 100644 --- a/src/server/game/Entities/Player/KillRewarder.h +++ b/src/server/game/Entities/Player/KillRewarder.h @@ -50,6 +50,7 @@ class AC_GAME_API KillRewarder Group* _group; float _groupRate; Player* _maxNotGrayMember; + uint8 _maxNotGrayMemberLevel; uint32 _count; uint32 _aliveSumLevel; uint32 _sumLevel; From 11e5d0259df544858c23107931a71f57e9ee76e5 Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 29 Mar 2026 17:39:32 +0200 Subject: [PATCH 3/5] fix: use OnPlayerBeforeGetLevelForXPGain in GiveXP --- src/server/game/Entities/Player/Player.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 82d9dd261a0c81..b0e2b6696c4cbb 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -2370,6 +2370,7 @@ void Player::GiveXP(uint32 xp, Unit* victim, float group_rate, bool isLFGReward) } uint8 level = GetLevel(); + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(this, level); // Favored experience increase START uint32 zone = GetZoneId(); From 51c0f7af3b04fda2685554cc65c55ad12fadff90 Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 29 Mar 2026 18:42:13 +0200 Subject: [PATCH 4/5] fix: use OnPlayerBeforeGetLevelForXPGain in lfg xp reward functions --- src/server/game/Handlers/LFGHandler.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/server/game/Handlers/LFGHandler.cpp b/src/server/game/Handlers/LFGHandler.cpp index 934f6b84c6a688..befc84be1e319f 100644 --- a/src/server/game/Handlers/LFGHandler.cpp +++ b/src/server/game/Handlers/LFGHandler.cpp @@ -23,6 +23,7 @@ #include "ObjectMgr.h" #include "Opcodes.h" #include "Player.h" +#include "ScriptMgr.h" #include "WorldPacket.h" #include "WorldSession.h" @@ -188,10 +189,13 @@ void WorldSession::HandleLfgPlayerLockInfoRequestOpcode(WorldPacket& /*recvData* if (quest) { uint8 playerLevel = GetPlayer() ? GetPlayer()->GetLevel() : 0; + uint8 playerLevelForXP = playerLevel; + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(GetPlayer(), playerLevelForXP); + data << uint8(done); data << uint32(quest->GetRewOrReqMoney(playerLevel)); - if (!GetPlayer()->IsMaxLevel()) - data << uint32(quest->XPValue(playerLevel)); + if (playerLevelForXP < GetPlayer()->GetUInt32Value(PLAYER_FIELD_MAX_LEVEL)) + data << uint32(quest->XPValue(playerLevelForXP)); else data << uint32(0); data << uint32(0); @@ -479,6 +483,8 @@ void WorldSession::SendLfgPlayerReward(lfg::LfgPlayerRewardData const& rewardDat uint8 itemNum = rewardData.quest->GetRewItemsCount(); uint8 playerLevel = GetPlayer() ? GetPlayer()->GetLevel() : 0; + uint8 playerLevelForXP = playerLevel; + sScriptMgr->OnPlayerBeforeGetLevelForXPGain(GetPlayer(), playerLevelForXP); WorldPacket data(SMSG_LFG_PLAYER_REWARD, 4 + 4 + 1 + 4 + 4 + 4 + 4 + 4 + 1 + itemNum * (4 + 4 + 4)); data << uint32(rewardData.rdungeonEntry); // Random Dungeon Finished @@ -486,7 +492,7 @@ void WorldSession::SendLfgPlayerReward(lfg::LfgPlayerRewardData const& rewardDat data << uint8(rewardData.done); data << uint32(1); data << uint32(rewardData.quest->GetRewOrReqMoney(playerLevel)); - data << uint32(rewardData.quest->XPValue(playerLevel)); + data << uint32(rewardData.quest->XPValue(playerLevelForXP)); data << uint32(0); data << uint32(0); data << uint8(itemNum); From 521f502cc0ca3f7b094dc8d09bf21da162c1c509 Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 29 Mar 2026 18:57:10 +0200 Subject: [PATCH 5/5] fix: clamp result level in OnPlayerBeforeGetLevelForXPGain --- src/server/game/Scripting/ScriptDefines/PlayerScript.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp index 61418a74ca05bf..310a6919c802b6 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.cpp @@ -18,6 +18,9 @@ #include "PlayerScript.h" #include "ScriptMgr.h" #include "ScriptMgrMacros.h" +#include "World.h" + +#include void ScriptMgr::OnPlayerBeforeDurabilityRepair(Player* player, ObjectGuid npcGUID, ObjectGuid itemGUID, float& discountMod, uint8 guildBank) { @@ -933,6 +936,7 @@ void ScriptMgr::OnPlayerLearnTaxiNode(Player const* player, uint32 nodeId) void ScriptMgr::OnPlayerBeforeGetLevelForXPGain(Player const* player, uint8& level) { CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_BEFORE_GET_LEVEL_FOR_XP_GAIN, script->OnPlayerBeforeGetLevelForXPGain(player, level)); + level = std::clamp(level, uint8(1), uint8(sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))); } PlayerScript::PlayerScript(const char* name, std::vector enabledHooks)