From fd43d2b5adcd358a2493ab5c3d6e6b0f90edaba8 Mon Sep 17 00:00:00 2001 From: Axel Cocat Date: Sun, 20 Sep 2026 14:22:32 +0200 Subject: [PATCH] fix(Core/Scripting): ask OnPlayerCanLearnSpell before a trainer casts an entry (#27707) Co-authored-by: Claude Opus 5 --- src/server/game/Entities/Creature/Trainer.cpp | 10 ++++++---- .../Scripting/ScriptDefines/PlayerScript.h | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/server/game/Entities/Creature/Trainer.cpp b/src/server/game/Entities/Creature/Trainer.cpp index b9e45c361b..3f1a0ddb5c 100644 --- a/src/server/game/Entities/Creature/Trainer.cpp +++ b/src/server/game/Entities/Creature/Trainer.cpp @@ -111,11 +111,13 @@ namespace Trainer npc->SendPlaySpellVisual(179); // 53 SpellCastDirected npc->SendPlaySpellImpact(player->GetGUID(), 362); // 113 EmoteSalute - // learn explicitly or cast explicitly - if (trainerSpell->IsCastable()) - player->CastSpell(player, trainerSpell->SpellId, true); - else + // learn explicitly or cast explicitly. A cast puts each wrapped spell through learnSpell and + // its hook, but also runs whatever else the entry does (a class mount steps the Riding skill), + // so the entry itself is put to the hook first and a refusal skips the cast as a whole + if (!trainerSpell->IsCastable()) player->learnSpell(trainerSpell->SpellId, false); + else if (sScriptMgr->OnPlayerCanLearnSpell(player, trainerSpell->SpellId)) + player->CastSpell(player, trainerSpell->SpellId, true); SendTeachSucceeded(npc, player, spellId); diff --git a/src/server/game/Scripting/ScriptDefines/PlayerScript.h b/src/server/game/Scripting/ScriptDefines/PlayerScript.h index ba164ac252..5edac9a9cd 100644 --- a/src/server/game/Scripting/ScriptDefines/PlayerScript.h +++ b/src/server/game/Scripting/ScriptDefines/PlayerScript.h @@ -891,18 +891,21 @@ public: * did. A trainer has taken the money and still reports success, and Player::LearnTalent spends * the point and records the talent anyway -- use OnPlayerCanLearnTalent to stop a talent. * - * Only Player::learnSpell reaches it; spells written through addSpell do not. Those bypasses - * include character loading (_LoadSpells, and the initial class and skill-rewarded spells, - * which run on every login) and talent spells from _addTalentAurasAndSpells, reapplied on - * every dual-spec switch -- so a refusal in world is undone the next time one of them runs. + * Player::learnSpell reaches it, and so does a trainer about to cast a wrapper entry (below); + * spells written through addSpell do not. Those bypasses include character loading + * (_LoadSpells, and the initial class and skill-rewarded spells, which run on every login) and + * talent spells from _addTalentAurasAndSpells, reapplied on every dual-spec switch -- so a + * refusal in world is undone the next time one of them runs. * * What it sees can be indirect: a trainer entry that wraps other spells (a paladin's Summon - * Warhorse) is cast, so the hook sees the wrapped spells instead. A talent rank arrives only - * when it lands in the spell book, which most do not -- but a learn-spell talent still - * delivers its additional talent spells. + * Warhorse) is cast, so the hook sees the entry first and then each wrapped spell as the cast + * teaches it. Refusing the entry skips the cast as a whole, and with it whatever else the entry + * does: a class mount also steps the Riding skill, which refusing the wrapped spells alone does + * not stop. A talent rank arrives only when it lands in the spell book, which most do not -- but + * a learn-spell talent still delivers its additional talent spells. * * @param player Contains information about the Player - * @param spellId The id of the spell about to be learned + * @param spellId The id of the spell about to be learned, or of the trainer entry about to be cast * * @return true if the player is allowed to learn the spell */