Paragon: cascade guard for class skill lines + panel catalog backfill

The skill-line cascade in Player::learnSkillRewardedSpells re-fires from
_LoadSkills (every login), UpdateSkillsForLevel (every level-up),
UpdateSkillPro (every weapon-skill tick on a training dummy), and
SetSkill (first time a class skill is granted). Each pass re-grants
every SkillLineAbility-tagged class ability on the matching skill line,
which leaks Blood Presence / Death Coil / Death Grip / etc. back into
the spellbook within seconds even after the player intentionally
refunded them via the Character Advancement panel.

Path B fix: a 5-line guard at the top of learnSkillRewardedSpells skips
the cascade for class-category skill lines on CLASS_PARAGON characters.
mod-paragon already calls Player::learnSpell directly for the abilities
the player actually purchased (and their attached passives), so the
panel becomes the sole authority over class abilities. Profession,
weapon, language, and racial cascades stay enabled so recipe auto-learn,
weapon proficiencies, and racial perks still work.

Side effect: passives that previously rode along on the cascade
(Forceful Deflection on Blood Strike, Runic Focus on Icy Touch) must be
force-attached the same way Blood Plague / Frost Fever already are.
Extend kAttached and kFixup in Paragon_Essence.cpp to do that; existing
characters self-heal on next login.

Backfill paragon_spell_ae_cost for 42 spells newly exposed by the panel
after the ClassMask=0 filter was removed from the client catalog
generator (Lava Burst, Hex, Evocation, Kill Shot, Path of Frost,
Horn of Winter, Rune Strike, Raise Ally, Dark Command, etc.). Migration
is INSERT IGNORE so any per-spell tuning on existing rows is preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-10 23:52:23 -04:00
parent 36ac3dbd1d
commit 8ad6a2aca3
4 changed files with 159 additions and 19 deletions
@@ -21,6 +21,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(10, 1), (10, 1),
(17, 1), (17, 1),
(53, 1), (53, 1),
(66, 1),
(72, 1), (72, 1),
(75, 1), (75, 1),
(78, 1), (78, 1),
@@ -30,6 +31,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(118, 1), (118, 1),
(120, 1), (120, 1),
(122, 1), (122, 1),
(126, 1),
(130, 1), (130, 1),
(131, 1), (131, 1),
(132, 1), (132, 1),
@@ -52,6 +54,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(469, 1), (469, 1),
(475, 1), (475, 1),
(498, 1), (498, 1),
(526, 1),
(527, 1), (527, 1),
(528, 1), (528, 1),
(543, 1), (543, 1),
@@ -73,22 +76,28 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(676, 1), (676, 1),
(686, 1), (686, 1),
(687, 1), (687, 1),
(688, 1),
(689, 1), (689, 1),
(691, 1),
(693, 1), (693, 1),
(694, 1), (694, 1),
(697, 1),
(698, 1), (698, 1),
(702, 1), (702, 1),
(703, 1), (703, 1),
(706, 1), (706, 1),
(710, 1), (710, 1),
(712, 1),
(740, 1), (740, 1),
(755, 1), (755, 1),
(759, 1), (759, 1),
(768, 1),
(770, 1), (770, 1),
(772, 1), (772, 1),
(774, 1), (774, 1),
(779, 1), (779, 1),
(781, 1), (781, 1),
(783, 1),
(845, 1), (845, 1),
(853, 1), (853, 1),
(871, 1), (871, 1),
@@ -104,6 +113,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(1038, 1), (1038, 1),
(1044, 1), (1044, 1),
(1064, 1), (1064, 1),
(1066, 1),
(1079, 1), (1079, 1),
(1082, 1), (1082, 1),
(1098, 1), (1098, 1),
@@ -176,6 +186,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(2812, 1), (2812, 1),
(2825, 1), (2825, 1),
(2893, 1), (2893, 1),
(2894, 1),
(2908, 1), (2908, 1),
(2912, 1), (2912, 1),
(2944, 1), (2944, 1),
@@ -194,6 +205,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(3565, 1), (3565, 1),
(3566, 1), (3566, 1),
(3567, 1), (3567, 1),
(3714, 1),
(3738, 1), (3738, 1),
(4987, 1), (4987, 1),
(5116, 1), (5116, 1),
@@ -205,7 +217,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(5185, 1), (5185, 1),
(5209, 1), (5209, 1),
(5211, 1), (5211, 1),
(5215, 1), (5215, 1);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(5217, 1), (5217, 1),
(5221, 1), (5221, 1),
(5225, 1), (5225, 1),
@@ -215,11 +229,10 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(5308, 1), (5308, 1),
(5384, 1), (5384, 1),
(5484, 1), (5484, 1),
(5487, 1),
(5500, 1), (5500, 1),
(5502, 1), (5502, 1),
(5504, 1); (5504, 1),
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(5675, 1), (5675, 1),
(5676, 1), (5676, 1),
(5697, 1), (5697, 1),
@@ -244,6 +257,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(6770, 1), (6770, 1),
(6785, 1), (6785, 1),
(6789, 1), (6789, 1),
(6795, 1),
(6807, 1),
(6940, 1), (6940, 1),
(7294, 1), (7294, 1),
(7302, 1), (7302, 1),
@@ -283,6 +298,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(11418, 1), (11418, 1),
(11419, 1), (11419, 1),
(11420, 1), (11420, 1),
(12051, 1),
(13159, 1), (13159, 1),
(13161, 1), (13161, 1),
(13163, 1), (13163, 1),
@@ -297,6 +313,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(16857, 1), (16857, 1),
(16914, 1), (16914, 1),
(18499, 1), (18499, 1),
(19263, 1),
(19740, 1), (19740, 1),
(19742, 1), (19742, 1),
(19746, 1), (19746, 1),
@@ -323,7 +340,6 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(20252, 1), (20252, 1),
(20484, 1), (20484, 1),
(20736, 1), (20736, 1),
(21084, 1),
(21562, 1), (21562, 1),
(21849, 1), (21849, 1),
(22568, 1), (22568, 1),
@@ -331,6 +347,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(22812, 1), (22812, 1),
(22842, 1), (22842, 1),
(23028, 1), (23028, 1),
(23161, 1),
(23214, 1),
(23920, 1), (23920, 1),
(23922, 1), (23922, 1),
(24275, 1), (24275, 1),
@@ -349,6 +367,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(29722, 1), (29722, 1),
(29858, 1), (29858, 1),
(29893, 1), (29893, 1),
(30449, 1),
(30451, 1), (30451, 1),
(30455, 1), (30455, 1),
(30482, 1), (30482, 1),
@@ -372,12 +391,14 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(33745, 1), (33745, 1),
(33763, 1), (33763, 1),
(33786, 1), (33786, 1),
(33943, 1),
(34026, 1), (34026, 1),
(34074, 1), (34074, 1),
(34428, 1), (34428, 1),
(34433, 1), (34433, 1),
(34477, 1), (34477, 1),
(34600, 1), (34600, 1),
(34767, 1),
(35715, 1), (35715, 1),
(35717, 1), (35717, 1),
(36936, 1), (36936, 1),
@@ -398,7 +419,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(47541, 1), (47541, 1),
(47568, 1), (47568, 1),
(47897, 1), (47897, 1),
(48018, 1), (48018, 1);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(48020, 1), (48020, 1),
(48045, 1), (48045, 1),
(48263, 1), (48263, 1),
@@ -416,14 +439,19 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(49576, 1), (49576, 1),
(49998, 1), (49998, 1),
(50464, 1), (50464, 1),
(50769, 1),
(50842, 1), (50842, 1),
(51505, 1),
(51514, 1),
(51722, 1), (51722, 1),
(51723, 1), (51723, 1),
(52610, 1); (51730, 1),
(52127, 1),
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (52610, 1),
(53140, 1), (53140, 1),
(53142, 1), (53142, 1),
(53271, 1),
(53351, 1),
(53407, 1), (53407, 1),
(53408, 1), (53408, 1),
(53600, 1), (53600, 1),
@@ -432,15 +460,23 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(54428, 1), (54428, 1),
(55342, 1), (55342, 1),
(55694, 1), (55694, 1),
(56222, 1),
(56641, 1), (56641, 1),
(56815, 1),
(57330, 1),
(57755, 1), (57755, 1),
(57934, 1), (57934, 1),
(57994, 1), (57994, 1),
(60192, 1), (60192, 1),
(61846, 1), (61846, 1),
(61999, 1),
(62078, 1), (62078, 1),
(62124, 1), (62124, 1),
(62757, 1), (62757, 1),
(64382, 1), (64382, 1),
(64843, 1); (64843, 1),
(64901, 1),
(66842, 1),
(66843, 1),
(66844, 1);
@@ -0,0 +1,62 @@
-- mod-paragon: backfill paragon_spell_ae_cost rows for spells newly exposed
-- by the Character Advancement panel after removing the over-aggressive
-- ClassMask=0 filter from tools/_gen_paragon_advancement_spells_lua.py.
--
-- The base file (data/sql/db-world/base/paragon_spell_ae_cost.sql) was
-- regenerated alongside this migration so fresh deployments already have
-- these rows. Existing servers do not re-run base files on content change,
-- so this update inserts the new (spell_id, ae_cost) pairs idempotently.
-- INSERT IGNORE keeps any per-row tuning a server operator may have already
-- applied to spell_ids that happen to overlap.
--
-- New ids include: 51505 Lava Burst (Shaman), 12051 Evocation / 1066 Aqueous
-- Form / Hex / Mage Ward / Spellsteal (Mage), 53351 Kill Shot / 19263
-- Deterrence / 53271 Master's Call (Hunter), 3714 Path of Frost / 57330
-- Horn of Winter / 56815 Rune Strike / 61999 Raise Ally / 56222 Dark Command
-- (DK), and 39 other trainer-taught class abilities whose stock
-- SkillLineAbility.dbc rows have ClassMask=0 (the skill line itself pins the
-- class for these rows; ClassMask is redundant on class-spec lines).
INSERT IGNORE INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(66, 1), -- Invisibility (Mage)
(126, 1), -- Eye of Kilrogg (Warlock)
(526, 1), -- Cure Toxins (Shaman)
(688, 1), -- Summon Imp (Warlock)
(691, 1), -- Summon Felhunter (Warlock)
(697, 1), -- Summon Voidwalker (Warlock)
(712, 1), -- Summon Succubus (Warlock)
(768, 1), -- Cat Form (Druid)
(783, 1), -- Travel Form (Druid)
(1066, 1), -- Aqueous Form (Mage)
(2894, 1), -- Fire Resistance Totem (Shaman)
(3714, 1), -- Path of Frost (DK)
(5215, 1), -- Prowl (Druid)
(5487, 1), -- Bear Form (Druid)
(5504, 1), -- Conjure Refreshment (Mage)
(6795, 1), -- Growl (Druid)
(6807, 1), -- Maul (Druid)
(12051, 1), -- Evocation (Mage)
(19263, 1), -- Deterrence (Hunter)
(23161, 1), -- Summon Dreadsteed (Warlock)
(23214, 1), -- Summon Charger (Paladin)
(30449, 1), -- Spellsteal (Mage)
(33943, 1), -- Flight Form (Druid)
(34767, 1), -- Summon Felguard (Warlock)
(48018, 1), -- Demonic Circle: Summon (Warlock)
(50769, 1), -- Revive (Druid)
(51505, 1), -- Lava Burst (Shaman)
(51514, 1), -- Hex (Shaman)
(51730, 1), -- Earthliving Weapon (Shaman)
(52127, 1), -- Water Shield (Shaman)
(52610, 1), -- Savage Roar (Druid)
(53271, 1), -- Master's Call (Hunter)
(53351, 1), -- Kill Shot (Hunter)
(56222, 1), -- Dark Command (DK)
(56815, 1), -- Rune Strike (DK)
(57330, 1), -- Horn of Winter (DK)
(61999, 1), -- Raise Ally (DK)
(64843, 1), -- Divine Hymn (Priest)
(64901, 1), -- Hymn of Hope (Priest)
(66842, 1), -- Call of the Elements (Shaman totem set)
(66843, 1), -- Call of the Ancestors (Shaman totem set)
(66844, 1); -- Call of the Spirits (Shaman totem set)
+28 -8
View File
@@ -1203,10 +1203,21 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
// spellbook icons. The correct *passive* spellbook entries the // spellbook icons. The correct *passive* spellbook entries the
// player is supposed to see are 59879 / 59921 (the descriptive // player is supposed to see are 59879 / 59921 (the descriptive
// "Passive disease" rows; SPELL_ATTR0_PASSIVE bit set). // "Passive disease" rows; SPELL_ATTR0_PASSIVE bit set).
// After the Paragon class-skill cascade guard landed in
// Player::learnSkillRewardedSpells, NONE of the DK skill-line
// cascade rewards are auto-granted any more — so passives that
// used to ride along on a class skill cascade (Forceful
// Deflection on Blood Strike, Runic Focus on Icy Touch) must be
// explicitly attached here, the same way Blood Plague / Frost
// Fever are. Add new entries when a panel-purchased active is
// expected to come with a passive spellbook entry that no
// SPELL_EFFECT_LEARN_SPELL on the parent provides.
struct AttachedPassive { uint32 parentHead; uint32 attachedSpell; }; struct AttachedPassive { uint32 parentHead; uint32 attachedSpell; };
static AttachedPassive const kAttached[] = { static AttachedPassive const kAttached[] = {
{ 45462, 59879 }, // Plague Strike -> Blood Plague (passive entry) { 45462, 59879 }, // Plague Strike -> Blood Plague (passive entry)
{ 45477, 59921 }, // Icy Touch -> Frost Fever (passive entry) { 45477, 59921 }, // Icy Touch -> Frost Fever (passive entry)
{ 45477, 61455 }, // Icy Touch -> Runic Focus (parry from spell power)
{ 45902, 49410 }, // Blood Strike -> Forceful Deflection (parry from strength)
}; };
// Self-heal: a previous build of mod-paragon (briefly shipped) // Self-heal: a previous build of mod-paragon (briefly shipped)
@@ -3889,16 +3900,25 @@ public:
lb.child, lb.parent, player->GetName()); lb.child, lb.parent, player->GetName());
} }
} }
// 2b) Re-attach the correct passive spellbook entry (59879 / // 2b) Re-attach the correct passive spellbook entries for any
// 59921) for any panel-purchased Plague Strike / Icy Touch // panel-purchased parent that is missing them. After the
// that's missing it. `learnSpell` here can re-fire the DK // class-skill cascade guard in
// skill-line cascade and re-grant Blood Presence / Death // Player::learnSkillRewardedSpells, the cascade no longer
// Coil / Death Grip / Forceful Deflection — Step 3's // fires for Paragons, so these attachments are the ONLY
// scoped sweep is what cleans those up. // source for the disease passive icons (Blood Plague /
// Frost Fever) and the small DK weapon passives (Forceful
// Deflection from Blood Strike, Runic Focus from Icy
// Touch). Existing characters predating the guard may
// have FD/RF in their spellbook from the cascade but no
// panel_spell_children row tying them to the parent;
// re-running learnSpell when they already have the spell
// just records the child row and is a no-op otherwise.
struct LegacyFix { uint32 parent; uint32 correctChild; }; struct LegacyFix { uint32 parent; uint32 correctChild; };
static LegacyFix const kFixup[] = { static LegacyFix const kFixup[] = {
{ 45462, 59879 }, { 45462, 59879 }, // Plague Strike -> Blood Plague (passive)
{ 45477, 59921 }, { 45477, 59921 }, // Icy Touch -> Frost Fever (passive)
{ 45477, 61455 }, // Icy Touch -> Runic Focus
{ 45902, 49410 }, // Blood Strike -> Forceful Deflection
}; };
for (auto const& lf : kFixup) for (auto const& lf : kFixup)
{ {
@@ -12017,6 +12017,28 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
uint32 raceMask = getRaceMask(); uint32 raceMask = getRaceMask();
uint32 classMask = getClassMask(); uint32 classMask = getClassMask();
// Fractured / Paragon: the Character Advancement panel is the sole
// authority over which class abilities a Paragon owns. The skill-line
// cascade re-fires from _LoadSkills (every login), UpdateSkillsForLevel
// (every level-up), UpdateSkillPro (every weapon-skill tick on a
// training dummy), and SetSkill (first time a class skill is granted).
// Each of those re-grants every SLA-tagged class ability on the
// matching skill line — leaking Blood Presence / Death Coil / Death
// Grip / etc. back into the spellbook within seconds even after the
// player intentionally refunded them via the panel. Skip the cascade
// for class-category skill lines on Paragon characters; mod-paragon
// calls Player::learnSpell directly for the abilities the player
// actually purchased, including their attached passives. Profession,
// weapon, language, and racial skill cascades stay enabled so things
// like recipe auto-learn, weapon proficiencies, and racial perks
// still work.
if (getClass() == CLASS_PARAGON)
{
if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
if (sl->categoryId == SKILL_CATEGORY_CLASS)
return;
}
// Get all abilities for this skill and sort by MinSkillLineRank (lowest to highest) // Get all abilities for this skill and sort by MinSkillLineRank (lowest to highest)
auto abilities = GetSkillLineAbilitiesBySkillLine(skill_id); auto abilities = GetSkillLineAbilitiesBySkillLine(skill_id);
std::vector<SkillLineAbilityEntry const*> sortedAbilities(abilities.begin(), abilities.end()); std::vector<SkillLineAbilityEntry const*> sortedAbilities(abilities.begin(), abilities.end());