From 81457d95f2e326d90d7e00f4f3860cf8b2822ac2 Mon Sep 17 00:00:00 2001 From: Taras Date: Sat, 13 Jun 2026 15:30:34 +0200 Subject: [PATCH] fix(Core/Commands): Learn profession rank spells in .learn all recipes (#25114) Co-authored-by: Ludwig Co-authored-by: Claude Opus 4.8 (1M context) --- src/server/game/Spells/SpellMgr.cpp | 20 ++++++++++++++++++++ src/server/game/Spells/SpellMgr.h | 1 + src/server/scripts/Commands/cs_learn.cpp | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 43c15c686..69f16604a 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -33,6 +33,8 @@ #include "Tokenize.h" #include "World.h" +#include + bool IsPrimaryProfessionSkill(uint32 skill) { SkillLineEntry const* pSkill = sSkillLineStore.LookupEntry(skill); @@ -687,6 +689,24 @@ SpellLearnSkillNode const* SpellMgr::GetSpellLearnSkill(uint32 spell_id) const return nullptr; } +std::vector SpellMgr::GetSkillRankSpells(uint32 skillId) const +{ + // Returns every spell that grants this skill via SPELL_EFFECT_SKILL, + // i.e. the profession rank/proficiency spells (Apprentice -> Grand Master), + // ordered by step. Not strictly limited to the six ranks: any skill-granting + // spell for the line is included. + std::vector result; + for (auto const& [spellId, node] : mSpellLearnSkills) + if (node.skill == skillId) + result.push_back(spellId); + + std::ranges::sort(result, [this](uint32 a, uint32 b) + { + return mSpellLearnSkills.at(a).step < mSpellLearnSkills.at(b).step; + }); + return result; +} + SpellTargetPosition const* SpellMgr::GetSpellTargetPosition(uint32 spell_id, SpellEffIndex effIndex) const { SpellTargetPositionMap::const_iterator itr = mSpellTargetPositions.find(std::make_pair(spell_id, effIndex)); diff --git a/src/server/game/Spells/SpellMgr.h b/src/server/game/Spells/SpellMgr.h index f4004c1a9..27c049497 100644 --- a/src/server/game/Spells/SpellMgr.h +++ b/src/server/game/Spells/SpellMgr.h @@ -687,6 +687,7 @@ public: // Spell learning [[nodiscard]] SpellLearnSkillNode const* GetSpellLearnSkill(uint32 spell_id) const; + [[nodiscard]] std::vector GetSkillRankSpells(uint32 skillId) const; // Spell target coordinates [[nodiscard]] SpellTargetPosition const* GetSpellTargetPosition(uint32 spell_id, SpellEffIndex effIndex) const; diff --git a/src/server/scripts/Commands/cs_learn.cpp b/src/server/scripts/Commands/cs_learn.cpp index b86e56662..874729b63 100644 --- a/src/server/scripts/Commands/cs_learn.cpp +++ b/src/server/scripts/Commands/cs_learn.cpp @@ -309,6 +309,8 @@ public: static bool HandleLearnAllCraftsCommand(ChatHandler* handler) { + Player* target = handler->GetSession()->GetPlayer(); + for (uint32 i = 0; i < sSkillLineStore.GetNumRows(); ++i) { SkillLineEntry const* skillInfo = sSkillLineStore.LookupEntry(i); @@ -318,7 +320,10 @@ public: if ((skillInfo->categoryId == SKILL_CATEGORY_PROFESSION || skillInfo->categoryId == SKILL_CATEGORY_SECONDARY) && skillInfo->canLink) // only prof. with recipes have { - HandleLearnSkillRecipesHelper(handler->GetSession()->GetPlayer(), skillInfo->id); + HandleLearnSkillRecipesHelper(target, skillInfo->id); + + uint16 const maxLevel = target->GetPureMaxSkillValue(skillInfo->id); + target->SetSkill(skillInfo->id, target->GetSkillStep(skillInfo->id), maxLevel, maxLevel); } } @@ -388,6 +393,15 @@ public: static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId) { + // Rank spells (Apprentice -> Grand Master) must be learned so that the + // skill-cleanup loop in Player::SetSkill (which calls removeSpell on the + // first spell in each chain) can walk forward and strip every rank on + // profession unlearn. Without the first rank in the spellbook that loop + // bails out and the leftover rank spells re-grant the skill after relog + // (issue #2330). + for (uint32 rankSpell : sSpellMgr->GetSkillRankSpells(skillId)) + player->learnSpell(rankSpell); + uint32 classmask = player->getClassMask(); for (SkillLineAbilityEntry const* skillLine : GetSkillLineAbilitiesBySkillLine(skillId))