|
|
|
@@ -16,15 +16,18 @@
|
|
|
|
|
#include "RBAC.h"
|
|
|
|
|
#include "ScriptMgr.h"
|
|
|
|
|
#include "SharedDefines.h"
|
|
|
|
|
#include "SpellAuras.h"
|
|
|
|
|
#include "SpellInfo.h"
|
|
|
|
|
#include "SpellMgr.h"
|
|
|
|
|
#include "WorldDatabase.h"
|
|
|
|
|
#include "WorldPacket.h"
|
|
|
|
|
#include "Log.h"
|
|
|
|
|
#include "DBCEnums.h"
|
|
|
|
|
#include "DBCStores.h"
|
|
|
|
|
|
|
|
|
|
#include <fmt/format.h>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
@@ -254,6 +257,10 @@ uint32 GetParagonPanelSpellRequiredLevel(SpellInfo const* info)
|
|
|
|
|
// is defined later (after PushSpellSnapshot / PushTalentSnapshot).
|
|
|
|
|
void PushSnapshot(Player* pl);
|
|
|
|
|
|
|
|
|
|
// Forward declaration: the login-time scoped sweep (defined a few helpers
|
|
|
|
|
// down) calls into the chain-walker (defined further down).
|
|
|
|
|
void CollectSpellChainIds(uint32 baseSpellId, std::unordered_set<uint32>& out);
|
|
|
|
|
|
|
|
|
|
// ---- Panel-learn tracking (Overview + resets) ------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Spells / talent ranks bought through Character Advancement are stored in
|
|
|
|
@@ -276,6 +283,282 @@ void DbInsertPanelSpellChild(uint32 lowGuid, uint32 parentSpellId, uint32 childS
|
|
|
|
|
lowGuid, parentSpellId, childSpellId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Persist an "active dependent we revoked" so we can re-revoke it after
|
|
|
|
|
// AC's skill cascade re-grants it on the next login. See
|
|
|
|
|
// `RevokeBlockedSpellsForPlayer` for the redo step.
|
|
|
|
|
void DbInsertPanelSpellRevoked(uint32 lowGuid, uint32 parentSpellId, uint32 revokedSpellId)
|
|
|
|
|
{
|
|
|
|
|
CharacterDatabase.DirectExecute(
|
|
|
|
|
"INSERT IGNORE INTO character_paragon_panel_spell_revoked "
|
|
|
|
|
"(guid, parent_spell_id, revoked_spell_id) VALUES ({}, {}, {})",
|
|
|
|
|
lowGuid, parentSpellId, revokedSpellId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Walk every (guid, *, revoked_spell_id) row and `removeSpell` it if the
|
|
|
|
|
// player still has it. Call sites:
|
|
|
|
|
// * `OnPlayerLogin` -- because `_LoadSkills` -> `learnSkillRewardedSpells`
|
|
|
|
|
// ran during LoadFromDB and may have re-granted Blood Presence /
|
|
|
|
|
// Death Coil / Death Grip / etc. before any of our hooks fired.
|
|
|
|
|
// * `HandleParagonResetAbilities` is NOT a caller; reset clears the
|
|
|
|
|
// table outright so the revoke list starts fresh on next purchase.
|
|
|
|
|
void RevokeBlockedSpellsForPlayer(Player* pl)
|
|
|
|
|
{
|
|
|
|
|
if (!pl)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
|
|
|
|
QueryResult r = CharacterDatabase.Query(
|
|
|
|
|
"SELECT revoked_spell_id FROM character_paragon_panel_spell_revoked WHERE guid = {}",
|
|
|
|
|
lowGuid);
|
|
|
|
|
if (!r)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
uint32 removed = 0;
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
uint32 const sid = r->Fetch()[0].Get<uint32>();
|
|
|
|
|
if (pl->HasSpell(sid))
|
|
|
|
|
{
|
|
|
|
|
pl->removeSpell(sid, SPEC_MASK_ALL, false);
|
|
|
|
|
++removed;
|
|
|
|
|
}
|
|
|
|
|
} while (r->NextRow());
|
|
|
|
|
|
|
|
|
|
if (removed)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"Paragon panel: re-revoked {} skill-cascade dependents for {} on login",
|
|
|
|
|
removed, pl->GetName());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] static bool SkillLineAbilityIsSkillCascadeSigned(SkillLineAbilityEntry const* sla)
|
|
|
|
|
{
|
|
|
|
|
return sla && (sla->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN
|
|
|
|
|
|| sla->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SkillLine ids from every SkillLineAbility row that lists `spellId`
|
|
|
|
|
// (any AcquireMethod). Used as the *anchor* side of cascade detection.
|
|
|
|
|
//
|
|
|
|
|
// Important: the old helper only kept rows with AcquireMethod
|
|
|
|
|
// LEARNED_ON_SKILL_* . Spells like Blood Strike are usually tied to
|
|
|
|
|
// their skill line via a trainer/default row (AcquireMethod 0). That
|
|
|
|
|
// meant the anchor set was empty, so `IsSpellSkillLineCascadeDependent`
|
|
|
|
|
// never matched Forceful Deflection / Blood Presence — those were stored
|
|
|
|
|
// as innocent "children" and never revoked.
|
|
|
|
|
//
|
|
|
|
|
// Dependent detection still requires the *dependent* spell to have a
|
|
|
|
|
// LEARNED_ON_SKILL_* row on one of these lines (same as
|
|
|
|
|
// `learnSkillRewardedSpells`).
|
|
|
|
|
[[nodiscard]] static std::unordered_set<uint32> SkillLinesLinkedToSpell(uint32 spellId)
|
|
|
|
|
{
|
|
|
|
|
std::unordered_set<uint32> out;
|
|
|
|
|
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
|
|
|
|
|
for (auto it = bounds.first; it != bounds.second; ++it)
|
|
|
|
|
{
|
|
|
|
|
SkillLineAbilityEntry const* sla = it->second;
|
|
|
|
|
if (!sla)
|
|
|
|
|
continue;
|
|
|
|
|
out.insert(sla->SkillLine);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// True when `depSpellId` is granted as a skill-line reward on one of the
|
|
|
|
|
// same SkillLines as `anchorSpellId` (e.g. Blood Strike -> Forceful
|
|
|
|
|
// Deflection / Blood Presence). Passives learned only via spell effects
|
|
|
|
|
// (disease auras, etc.) typically return false here.
|
|
|
|
|
[[nodiscard]] static bool IsSpellSkillLineCascadeDependent(uint32 anchorSpellId, uint32 depSpellId)
|
|
|
|
|
{
|
|
|
|
|
std::unordered_set<uint32> const anchorLines = SkillLinesLinkedToSpell(anchorSpellId);
|
|
|
|
|
if (anchorLines.empty())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SkillLineAbilityMapBounds db = sSpellMgr->GetSkillLineAbilityMapBounds(depSpellId);
|
|
|
|
|
for (auto it = db.first; it != db.second; ++it)
|
|
|
|
|
{
|
|
|
|
|
SkillLineAbilityEntry const* sla = it->second;
|
|
|
|
|
if (!SkillLineAbilityIsSkillCascadeSigned(sla))
|
|
|
|
|
continue;
|
|
|
|
|
if (anchorLines.count(sla->SkillLine))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Older builds recorded skill-line cascade passives (Forceful Deflection,
|
|
|
|
|
// Runic Focus, ...) as `panel_spell_children`. Strip those rows and
|
|
|
|
|
// revoke the spell so the login sweep + reset logic match current policy.
|
|
|
|
|
static void PruneSkillLineCascadeChildrenFromDb(Player* pl, uint32 lowGuid)
|
|
|
|
|
{
|
|
|
|
|
if (!pl)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
QueryResult r = CharacterDatabase.Query(
|
|
|
|
|
"SELECT parent_spell_id, child_spell_id FROM character_paragon_panel_spell_children WHERE guid = {}",
|
|
|
|
|
lowGuid);
|
|
|
|
|
if (!r)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
uint32 const parent = r->Fetch()[0].Get<uint32>();
|
|
|
|
|
uint32 const child = r->Fetch()[1].Get<uint32>();
|
|
|
|
|
if (!IsSpellSkillLineCascadeDependent(parent, child))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
CharacterDatabase.DirectExecute(
|
|
|
|
|
"DELETE FROM character_paragon_panel_spell_children WHERE guid = {} AND parent_spell_id = {} AND child_spell_id = {}",
|
|
|
|
|
lowGuid, parent, child);
|
|
|
|
|
|
|
|
|
|
if (pl->HasSpell(child))
|
|
|
|
|
{
|
|
|
|
|
pl->removeSpell(child, SPEC_MASK_ALL, false);
|
|
|
|
|
DbInsertPanelSpellRevoked(lowGuid, parent, child);
|
|
|
|
|
}
|
|
|
|
|
} while (r->NextRow());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login-time scoped sweep for cascade-granted dependents that the
|
|
|
|
|
// commit-time diff missed.
|
|
|
|
|
//
|
|
|
|
|
// Background: AC's `_addSpell` calls `LearnDefaultSkill(skill, 0)` once
|
|
|
|
|
// for any SkillLineAbility-linked spell when the player doesn't yet have
|
|
|
|
|
// the skill. `LearnDefaultSkill` -> `SetSkill` -> `learnSkillRewardedSpells`
|
|
|
|
|
// then grants every reward of that skill as PLAYERSPELL_TEMPORARY (which
|
|
|
|
|
// lives in m_spells but is NOT written to character_spell). On every
|
|
|
|
|
// subsequent login `_LoadSkills` re-fires the cascade for any skill row
|
|
|
|
|
// that exists in `character_skills`, silently re-granting Blood Presence
|
|
|
|
|
// / Forceful Deflection / Death Coil / etc. -- including for characters
|
|
|
|
|
// like Test whose first commit predates the per-purchase revoke list.
|
|
|
|
|
//
|
|
|
|
|
// Strategy: from the player's panel-purchased spells, derive the set of
|
|
|
|
|
// SkillLines we've activated (via SkillLineAbility's SkillLine field).
|
|
|
|
|
// Walk those SkillLines' rewards and revoke any spell (active or passive)
|
|
|
|
|
// currently in m_spells that isn't in our allowlist (panel chain ranks +
|
|
|
|
|
// non-cascade passive children). Persist each revoke into
|
|
|
|
|
// `character_paragon_panel_spell_revoked` so the cheaper
|
|
|
|
|
// `RevokeBlockedSpellsForPlayer` path handles it on every subsequent
|
|
|
|
|
// login.
|
|
|
|
|
//
|
|
|
|
|
// Why this is safe (unlike the earlier blanket-temporary sweep):
|
|
|
|
|
// * Only walks SkillLines that we caused to be activated. Racial
|
|
|
|
|
// skills, weapon skills, Defense, etc. live in different SkillLines
|
|
|
|
|
// and are never reached.
|
|
|
|
|
// * Passives that are pure spell-effect side effects stay in
|
|
|
|
|
// `character_paragon_panel_spell_children` and remain allowlisted.
|
|
|
|
|
// * Skill-line cascade passives (Forceful Deflection, ...) are not
|
|
|
|
|
// allowlisted children anymore; see `PruneSkillLineCascadeChildrenFromDb`
|
|
|
|
|
// + `PanelLearnSpellChain`.
|
|
|
|
|
void RevokeUnwantedCascadeSpellsForPlayer(Player* pl)
|
|
|
|
|
{
|
|
|
|
|
if (!pl)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
bool const diag = sConfigMgr->GetOption<bool>("Paragon.Diag.PanelLearn", false);
|
|
|
|
|
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
|
|
|
|
|
|
|
|
|
PruneSkillLineCascadeChildrenFromDb(pl, lowGuid);
|
|
|
|
|
|
|
|
|
|
// Build the allowlist: every chain rank of every panel-purchased spell,
|
|
|
|
|
// plus every recorded passive child.
|
|
|
|
|
std::unordered_set<uint32> allowed;
|
|
|
|
|
if (QueryResult r = CharacterDatabase.Query(
|
|
|
|
|
"SELECT spell_id FROM character_paragon_panel_spells WHERE guid = {}",
|
|
|
|
|
lowGuid))
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
uint32 const base = r->Fetch()[0].Get<uint32>();
|
|
|
|
|
CollectSpellChainIds(base, allowed);
|
|
|
|
|
} while (r->NextRow());
|
|
|
|
|
}
|
|
|
|
|
if (QueryResult r = CharacterDatabase.Query(
|
|
|
|
|
"SELECT child_spell_id FROM character_paragon_panel_spell_children WHERE guid = {}",
|
|
|
|
|
lowGuid))
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
allowed.insert(r->Fetch()[0].Get<uint32>());
|
|
|
|
|
} while (r->NextRow());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allowed.empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// From the allowlist, derive which SkillLines we've activated. Use
|
|
|
|
|
// every SkillLineAbility row for each spell (not only LEARNED_ON_SKILL_*),
|
|
|
|
|
// matching `SkillLinesLinkedToSpell` / `_addSpell`'s LearnDefaultSkill path.
|
|
|
|
|
std::unordered_set<uint32> ourSkillLines;
|
|
|
|
|
for (uint32 spellId : allowed)
|
|
|
|
|
{
|
|
|
|
|
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
|
|
|
|
|
for (auto it = bounds.first; it != bounds.second; ++it)
|
|
|
|
|
{
|
|
|
|
|
SkillLineAbilityEntry const* sla = it->second;
|
|
|
|
|
if (!sla)
|
|
|
|
|
continue;
|
|
|
|
|
ourSkillLines.insert(sla->SkillLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ourSkillLines.empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// For each activated SkillLine, walk its rewards and queue revokes for
|
|
|
|
|
// active spells that aren't allowlisted but are currently in m_spells.
|
|
|
|
|
std::vector<uint32> toRevoke;
|
|
|
|
|
for (uint32 skillLine : ourSkillLines)
|
|
|
|
|
{
|
|
|
|
|
for (SkillLineAbilityEntry const* ab : GetSkillLineAbilitiesBySkillLine(skillLine))
|
|
|
|
|
{
|
|
|
|
|
if (!ab)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
uint32 const sid = ab->Spell;
|
|
|
|
|
if (!sid)
|
|
|
|
|
continue;
|
|
|
|
|
if (allowed.count(sid))
|
|
|
|
|
continue;
|
|
|
|
|
if (!pl->HasSpell(sid))
|
|
|
|
|
continue; // not in m_spells right now
|
|
|
|
|
|
|
|
|
|
SpellInfo const* info = sSpellMgr->GetSpellInfo(sid);
|
|
|
|
|
if (!info)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
toRevoke.push_back(sid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toRevoke.empty())
|
|
|
|
|
{
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] login sweep: no orphan cascade spells for {} "
|
|
|
|
|
"(skillLines scanned={}, allowlist={})",
|
|
|
|
|
pl->GetName(), ourSkillLines.size(), allowed.size());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dedup (a spell can show up under multiple SkillLines).
|
|
|
|
|
std::sort(toRevoke.begin(), toRevoke.end());
|
|
|
|
|
toRevoke.erase(std::unique(toRevoke.begin(), toRevoke.end()), toRevoke.end());
|
|
|
|
|
|
|
|
|
|
for (uint32 sid : toRevoke)
|
|
|
|
|
{
|
|
|
|
|
pl->removeSpell(sid, SPEC_MASK_ALL, false);
|
|
|
|
|
// parent_spell_id = 0 -> "caught at login by scoped sweep, no
|
|
|
|
|
// specific parent". Distinct PK from the (guid, parent, sid)
|
|
|
|
|
// rows the commit-time diff inserts.
|
|
|
|
|
DbInsertPanelSpellRevoked(lowGuid, 0u, sid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"Paragon panel: scoped sweep revoked {} cascade spells for {} "
|
|
|
|
|
"(skillLines scanned={}, allowlist={})",
|
|
|
|
|
toRevoke.size(), pl->GetName(), ourSkillLines.size(), allowed.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Snapshot of currently-known spell IDs (excluding entries marked removed).
|
|
|
|
|
// Used by PanelLearnSpellChain to detect spells that AzerothCore's
|
|
|
|
|
// addSpell machinery auto-learns alongside the spell we asked for.
|
|
|
|
@@ -340,14 +623,15 @@ void CollectSpellChainIds(uint32 baseSpellId, std::unordered_set<uint32>& out)
|
|
|
|
|
// Death Grip, Blood Plague, Blood Presence, Forceful Deflection, Runic
|
|
|
|
|
// Focus). We diff the player's spell list before/after `learnSpell` to
|
|
|
|
|
// classify those side effects:
|
|
|
|
|
// * Passives (Frost Fever, Blood Presence, Forceful Deflection, Runic
|
|
|
|
|
// Focus, Blood Plague, ...) are kept since they're typically required
|
|
|
|
|
// for the parent ability to function. We record them in
|
|
|
|
|
// character_paragon_panel_spell_children so reset unlearns them
|
|
|
|
|
// alongside the parent.
|
|
|
|
|
// * Active dependents (Death Coil, Death Grip, ...) are revoked
|
|
|
|
|
// immediately via `removeSpell` so the player only ends up with what
|
|
|
|
|
// they actually purchased.
|
|
|
|
|
// * Passives that are pure spell-effect side effects (disease auras,
|
|
|
|
|
// etc.) are kept; we record them in character_paragon_panel_spell_children
|
|
|
|
|
// so reset unlearns them alongside the parent.
|
|
|
|
|
// * Passives that are skill-line cascade rewards on the same SkillLine
|
|
|
|
|
// as the rank being learned (Forceful Deflection with Blood Strike)
|
|
|
|
|
// are revoked like actives — they are not panel children.
|
|
|
|
|
// * Active dependents (Death Coil, Death Grip, Blood Presence, ...) are
|
|
|
|
|
// revoked immediately via `removeSpell` so the player only ends up
|
|
|
|
|
// with what they actually purchased.
|
|
|
|
|
// The "you have learned X" / "you have unlearned X" chat toasts that
|
|
|
|
|
// fire during this dance are silenced client-side via a SILENCE
|
|
|
|
|
// addon-channel window opened around the whole commit (see
|
|
|
|
@@ -365,6 +649,14 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
|
|
|
|
|
std::unordered_set<uint32> chainIds;
|
|
|
|
|
CollectSpellChainIds(trackId, chainIds);
|
|
|
|
|
|
|
|
|
|
bool const diag = sConfigMgr->GetOption<bool>("Paragon.Diag.PanelLearn", false);
|
|
|
|
|
if (diag)
|
|
|
|
|
{
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] PanelLearnSpellChain start: player={} lvl={} class={} baseSpellId={} trackId={} chainSize={}",
|
|
|
|
|
pl->GetName(), playerLevel, uint32(pl->getClass()), baseSpellId, trackId, chainIds.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32 cur = trackId;
|
|
|
|
|
while (cur)
|
|
|
|
|
{
|
|
|
|
@@ -381,27 +673,73 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
|
|
|
|
|
if (!pl->HasSpell(cur))
|
|
|
|
|
{
|
|
|
|
|
std::unordered_set<uint32> before = SnapshotKnownSpells(pl);
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] pre-learn rank={} spellMapSize={}",
|
|
|
|
|
cur, before.size());
|
|
|
|
|
|
|
|
|
|
pl->learnSpell(cur, false);
|
|
|
|
|
std::unordered_set<uint32> after = SnapshotKnownSpells(pl);
|
|
|
|
|
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] post-learn rank={} spellMapSize={} delta={}",
|
|
|
|
|
cur, after.size(),
|
|
|
|
|
int32(after.size()) - int32(before.size()));
|
|
|
|
|
|
|
|
|
|
// Diff: classify each new spell that wasn't in the chain we
|
|
|
|
|
// asked for. Passives stick (recorded as children); actives
|
|
|
|
|
// get revoked.
|
|
|
|
|
// asked for. Pure spell-effect passives stick (children); skill-
|
|
|
|
|
// line cascade passives and actives get revoked.
|
|
|
|
|
for (uint32 spellId : after)
|
|
|
|
|
{
|
|
|
|
|
if (before.count(spellId))
|
|
|
|
|
continue; // already known
|
|
|
|
|
if (chainIds.count(spellId))
|
|
|
|
|
continue; // a rank we asked for
|
|
|
|
|
{
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] +{} (chain rank, kept)", spellId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SpellInfo const* dep = sSpellMgr->GetSpellInfo(spellId);
|
|
|
|
|
if (!dep)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (dep->IsPassive())
|
|
|
|
|
DbInsertPanelSpellChild(lowGuid, trackId, spellId);
|
|
|
|
|
{
|
|
|
|
|
if (IsSpellSkillLineCascadeDependent(cur, spellId))
|
|
|
|
|
{
|
|
|
|
|
pl->removeSpell(spellId, SPEC_MASK_ALL, false);
|
|
|
|
|
DbInsertPanelSpellRevoked(lowGuid, trackId, spellId);
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] +{} (skill-line passive dep, REVOKED, parent={})",
|
|
|
|
|
spellId, trackId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
DbInsertPanelSpellChild(lowGuid, trackId, spellId);
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] +{} (passive dep, kept as child of {})",
|
|
|
|
|
spellId, trackId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
pl->removeSpell(spellId, SPEC_MASK_ALL, false);
|
|
|
|
|
// Persist so we can re-revoke after the next login --
|
|
|
|
|
// AC's _LoadSkills -> learnSkillRewardedSpells will
|
|
|
|
|
// re-grant skill-rewarded actives (Blood Presence,
|
|
|
|
|
// Death Coil, Death Grip, ...) every time the player
|
|
|
|
|
// logs in.
|
|
|
|
|
DbInsertPanelSpellRevoked(lowGuid, trackId, spellId);
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] +{} (active dep, REVOKED, parent={})",
|
|
|
|
|
spellId, trackId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -412,6 +750,10 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DbInsertPanelSpell(lowGuid, trackId);
|
|
|
|
|
|
|
|
|
|
if (diag)
|
|
|
|
|
LOG_INFO("module",
|
|
|
|
|
"[paragon-diag] PanelLearnSpellChain end: trackId={}", trackId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DbUpsertPanelTalent(uint32 lowGuid, uint32 talentId, uint32 rank)
|
|
|
|
@@ -749,6 +1091,12 @@ bool HandleCommit(Player* pl, std::string const& body, std::string* err)
|
|
|
|
|
DbUpsertPanelTalent(lowGuid, tid, r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One scoped sweep after all panel DB rows for this commit exist. Catches
|
|
|
|
|
// cascade dependents the per-rank diff missed when anchor SkillLine rows
|
|
|
|
|
// used a non-LEARN_* AcquireMethod (fixed in SkillLinesLinkedToSpell, but
|
|
|
|
|
// this is cheap insurance for mixed commits / talent side effects).
|
|
|
|
|
RevokeUnwantedCascadeSpellsForPlayer(pl);
|
|
|
|
|
|
|
|
|
|
SaveCurrencyToDb(pl);
|
|
|
|
|
SendSilenceClose(pl);
|
|
|
|
|
return true;
|
|
|
|
@@ -916,8 +1264,25 @@ bool HandleParagonResetAbilities(Player* pl, std::string* err)
|
|
|
|
|
} while (r->NextRow());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Best-effort revoke pass for active dependents the cascade may have
|
|
|
|
|
// re-installed since last commit (Blood Presence, Death Coil, ...).
|
|
|
|
|
// Without this, a player who upgraded to this build with orphan
|
|
|
|
|
// dependents already in their spellbook would still see them after
|
|
|
|
|
// Reset Abilities; with this they're swept the moment they reset.
|
|
|
|
|
if (QueryResult rev = CharacterDatabase.Query(
|
|
|
|
|
"SELECT revoked_spell_id FROM character_paragon_panel_spell_revoked WHERE guid = {}", lowGuid))
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
uint32 const sid = rev->Fetch()[0].Get<uint32>();
|
|
|
|
|
if (pl->HasSpell(sid))
|
|
|
|
|
pl->removeSpell(sid, SPEC_MASK_ALL, false);
|
|
|
|
|
} while (rev->NextRow());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CharacterDatabase.DirectExecute("DELETE FROM character_paragon_panel_spells WHERE guid = {}", lowGuid);
|
|
|
|
|
CharacterDatabase.DirectExecute("DELETE FROM character_paragon_panel_spell_children WHERE guid = {}", lowGuid);
|
|
|
|
|
CharacterDatabase.DirectExecute("DELETE FROM character_paragon_panel_spell_revoked WHERE guid = {}", lowGuid);
|
|
|
|
|
|
|
|
|
|
if (refundAE)
|
|
|
|
|
{
|
|
|
|
@@ -1040,6 +1405,48 @@ public:
|
|
|
|
|
LoadCurrencyFromDb(player);
|
|
|
|
|
PushCurrency(player);
|
|
|
|
|
PushSnapshot(player);
|
|
|
|
|
|
|
|
|
|
// AC's character load sequence runs _LoadSkills (which fires
|
|
|
|
|
// learnSkillRewardedSpells) and _LoadSpells before this hook,
|
|
|
|
|
// so any active dependents we revoked at panel-commit time
|
|
|
|
|
// (Blood Presence / Death Coil / Death Grip / ...) have been
|
|
|
|
|
// silently re-granted by the skill cascade. Walk our persisted
|
|
|
|
|
// revoke list and remove them again.
|
|
|
|
|
//
|
|
|
|
|
// `removeSpell` triggers SMSG_REMOVED_SPELL on the client which
|
|
|
|
|
// generates "You have unlearned X" CHAT_MSG_SYSTEM toasts. The
|
|
|
|
|
// chat frame buffers system messages while the loading screen
|
|
|
|
|
// is up and only flushes them after PLAYER_ENTERING_WORLD, so
|
|
|
|
|
// a paired OPEN/CLOSE we send here would already be CLOSED by
|
|
|
|
|
// the time those buffered toasts reach the filter. We open the
|
|
|
|
|
// silence window from the server side as a defensive measure,
|
|
|
|
|
// but rely on the client to keep the window open across the
|
|
|
|
|
// login flush via its own PA.Silence:Open({}) call in the
|
|
|
|
|
// PLAYER_LOGIN handler. The window auto-closes via the addon's
|
|
|
|
|
// fail-open timer (PA.Silence.WINDOW_SECONDS).
|
|
|
|
|
if (player && player->getClass() == CLASS_PARAGON
|
|
|
|
|
&& sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
|
|
|
|
{
|
|
|
|
|
std::vector<std::pair<uint32, uint32>> emptySpells;
|
|
|
|
|
std::vector<std::pair<uint32, uint32>> emptyTalents;
|
|
|
|
|
SendSilenceOpenForCommit(player, emptySpells, emptyTalents);
|
|
|
|
|
|
|
|
|
|
// Step 1: re-revoke spells we explicitly recorded last commit
|
|
|
|
|
// (cheap, exact: just walks the persisted revoke table).
|
|
|
|
|
RevokeBlockedSpellsForPlayer(player);
|
|
|
|
|
// Step 2: scoped sweep across SkillLines we activated via panel
|
|
|
|
|
// purchases. Catches cascade-granted active spells that the
|
|
|
|
|
// commit-time diff missed -- e.g., legacy characters whose
|
|
|
|
|
// first commit predates the per-purchase revoke list, or any
|
|
|
|
|
// cascade re-fire on relog. Only walks our own SkillLines, so
|
|
|
|
|
// racials / weapon skills / Defense rewards are never touched.
|
|
|
|
|
RevokeUnwantedCascadeSpellsForPlayer(player);
|
|
|
|
|
|
|
|
|
|
// Intentionally NOT calling SendSilenceClose here -- the chat
|
|
|
|
|
// frame buffers system messages during the loading screen and
|
|
|
|
|
// would flush them after the CLOSE arrives. The addon's
|
|
|
|
|
// fail-open timer (8s) closes the window after the flush.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnPlayerLogout(Player* player) override
|
|
|
|
@@ -1149,6 +1556,21 @@ public:
|
|
|
|
|
PushSnapshot(player);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Combat lockdown: any mutating control message ("C ...") is
|
|
|
|
|
// rejected while the player is in combat. The client also
|
|
|
|
|
// self-gates the buttons that would emit these (see
|
|
|
|
|
// ParagonAdvancement.lua), but the authoritative check lives
|
|
|
|
|
// here so a hand-crafted addon message can't bypass it.
|
|
|
|
|
// Read-only "Q ..." queries above are allowed in combat so
|
|
|
|
|
// the panel can keep its currency / snapshot displays current.
|
|
|
|
|
if (!body.empty() && body[0] == 'C' && player->IsInCombat())
|
|
|
|
|
{
|
|
|
|
|
SendAddonMessage(player,
|
|
|
|
|
"R ERR cannot use Character Advancement while in combat");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (body.compare(0, 9, "C COMMIT ") == 0)
|
|
|
|
|
{
|
|
|
|
|
std::string err;
|
|
|
|
@@ -1250,6 +1672,7 @@ public:
|
|
|
|
|
{ "currency", HandleCurrency, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
|
|
|
|
|
{ "learn", HandleLearn, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
|
|
|
|
|
{ "runes", HandleRunes, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
|
|
|
|
|
{ "hat", HandleHat, rbac::RBAC_PERM_COMMAND_LEARN, Console::No },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static ChatCommandTable commandTable =
|
|
|
|
@@ -1365,6 +1788,152 @@ public:
|
|
|
|
|
handler->PSendSysMessage("Learned spell {} ({} AE spent, {} AE remaining).", spell->Id, cost, GetAE(pl));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// .paragon hat — diagnostic for Honor Among Thieves on a Paragon.
|
|
|
|
|
// Prints HasTalent, HasAura, HasSpell, EquippedItemClass, ProcEvent
|
|
|
|
|
// hookup, and re-applies the talent's passive aura if missing.
|
|
|
|
|
//
|
|
|
|
|
// HAT in 3.3.5: rank ids 51698 (R1) / 51700 (R2) / 51701 (R3).
|
|
|
|
|
// Effect 0 is APPLY_AREA_AURA_PARTY -> SPELL_AURA_PROC_TRIGGER_SPELL
|
|
|
|
|
// (52916) which targets the rogue (the original caster) when ANY
|
|
|
|
|
// party member crits, and 52916 in turn casts 51699 to grant +1 CP.
|
|
|
|
|
static bool HandleHat(ChatHandler* handler)
|
|
|
|
|
{
|
|
|
|
|
Player* pl = handler->GetPlayer();
|
|
|
|
|
if (!pl)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
constexpr std::array<uint32, 3> kHatRanks = { 51698u, 51700u, 51701u };
|
|
|
|
|
constexpr uint32 kHatProc = 52916u;
|
|
|
|
|
constexpr uint32 kHatTrigger = 51699u;
|
|
|
|
|
|
|
|
|
|
handler->PSendSysMessage("|cff00ffff[paragon hat]|r class={} lvl={} group={}",
|
|
|
|
|
uint32(pl->getClass()), pl->GetLevel(),
|
|
|
|
|
pl->GetGroup() ? "yes" : "no");
|
|
|
|
|
|
|
|
|
|
uint32 ownedRank = 0;
|
|
|
|
|
uint32 ownedRankId = 0;
|
|
|
|
|
for (uint32 i = 0; i < kHatRanks.size(); ++i)
|
|
|
|
|
{
|
|
|
|
|
uint32 const id = kHatRanks[i];
|
|
|
|
|
bool hasTalent = pl->HasTalent(id, pl->GetActiveSpec());
|
|
|
|
|
bool hasAura = pl->HasAura(id);
|
|
|
|
|
SpellInfo const* info = sSpellMgr->GetSpellInfo(id);
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" R{} ({}): HasTalent={} HasAura={} HasSpell={} info={}",
|
|
|
|
|
i + 1, id,
|
|
|
|
|
hasTalent ? "Y" : "n",
|
|
|
|
|
hasAura ? "Y" : "n",
|
|
|
|
|
pl->HasSpell(id) ? "Y" : "n",
|
|
|
|
|
info ? "ok" : "MISSING");
|
|
|
|
|
if (hasTalent && !ownedRank)
|
|
|
|
|
{
|
|
|
|
|
ownedRank = i + 1;
|
|
|
|
|
ownedRankId = id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SpellInfo const* procInfo = sSpellMgr->GetSpellInfo(kHatProc);
|
|
|
|
|
SpellInfo const* trigInfo = sSpellMgr->GetSpellInfo(kHatTrigger);
|
|
|
|
|
handler->PSendSysMessage(" proc52916 info={} trig51699 info={}",
|
|
|
|
|
procInfo ? "ok" : "MISSING",
|
|
|
|
|
trigInfo ? "ok" : "MISSING");
|
|
|
|
|
|
|
|
|
|
if (!ownedRank)
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage("|cffff8000No HAT rank in active spec.|r");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SpellInfo const* rank = sSpellMgr->GetSpellInfo(ownedRankId);
|
|
|
|
|
if (!rank)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
// Inspect the talent rank effects so we can see exactly what AC
|
|
|
|
|
// thinks should be applied.
|
|
|
|
|
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
|
|
|
|
{
|
|
|
|
|
SpellEffectInfo const& eff = rank->Effects[i];
|
|
|
|
|
if (eff.Effect == 0)
|
|
|
|
|
continue;
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" eff[{}] effect={} aura={} targetA={} trigger={} basePts={}",
|
|
|
|
|
i, uint32(eff.Effect), uint32(eff.ApplyAuraName),
|
|
|
|
|
uint32(eff.TargetA.GetTarget()), eff.TriggerSpell, eff.BasePoints);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force-apply the rank passive aura if it isn't already there. We
|
|
|
|
|
// use Aura::TryRefreshStackOrCreate so we don't go through CheckCast
|
|
|
|
|
// (which can fail silently for various reasons).
|
|
|
|
|
if (!pl->HasAura(ownedRankId))
|
|
|
|
|
{
|
|
|
|
|
Aura* a = Aura::TryRefreshStackOrCreate(rank, MAX_EFFECT_MASK, pl, pl);
|
|
|
|
|
handler->PSendSysMessage(" force-apply rank aura -> {}",
|
|
|
|
|
a ? "OK" : "FAILED");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(" rank aura already on player.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dump the live SpellProcEntry the engine will use when checking
|
|
|
|
|
// "should this aura proc". Confirms the ProcFlags fallback from
|
|
|
|
|
// SpellInfo, and the SpellPhase / Hit gates.
|
|
|
|
|
if (SpellProcEntry const* pe = sSpellMgr->GetSpellProcEntry(ownedRankId))
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" ProcEntry({}): ProcFlags=0x{:X} SpellTypeMask=0x{:X} SpellPhaseMask=0x{:X} HitMask=0x{:X} chance={} cd={}ms",
|
|
|
|
|
ownedRankId,
|
|
|
|
|
pe->ProcFlags, pe->SpellTypeMask, pe->SpellPhaseMask,
|
|
|
|
|
pe->HitMask, pe->Chance,
|
|
|
|
|
uint32(pe->Cooldown.count()));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" ProcEntry({}): NONE (proc system will not fire this aura)",
|
|
|
|
|
ownedRankId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Walk the player's applied auras and report whether the engine
|
|
|
|
|
// currently sees an effect-mask flag turned on for the rank's
|
|
|
|
|
// PROC_TRIGGER_SPELL effect. If the aura is up but the flag is
|
|
|
|
|
// off, the engine won't proc it.
|
|
|
|
|
if (AuraApplication* aa = pl->GetAuraApplication(ownedRankId))
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" AuraApp: effectMask=0x{:X} effectsToApply=0x{:X} flags=0x{:X}",
|
|
|
|
|
uint32(aa->GetEffectMask()),
|
|
|
|
|
uint32(aa->GetEffectsToApply()),
|
|
|
|
|
uint32(aa->GetFlags()));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(" AuraApp: NONE on player");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force-fire the trigger chain (52916 -> dummy -> 51699 -> +CP) on
|
|
|
|
|
// the player's current target. If this works, the proc machinery
|
|
|
|
|
// is at fault. If THIS fails, the trigger spells are at fault.
|
|
|
|
|
if (Unit* tgt = pl->GetSelectedUnit())
|
|
|
|
|
{
|
|
|
|
|
uint32 const cpBefore = pl->GetComboPoints(tgt);
|
|
|
|
|
// Mimic spell_rog_honor_among_thieves::HandleProc: cast 52916
|
|
|
|
|
// FROM tgt -> tgt with the rogue/Paragon as original caster.
|
|
|
|
|
tgt->CastSpell(tgt, kHatProc, true, nullptr, nullptr, pl->GetGUID());
|
|
|
|
|
uint32 const cpAfter = pl->GetComboPoints(tgt);
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" force-fire 52916 on {}: CP {} -> {}",
|
|
|
|
|
tgt->GetName(), cpBefore, cpAfter);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
handler->PSendSysMessage(
|
|
|
|
|
" no target selected: pick a unit and run again to force-fire 52916.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace (anonymous)
|
|
|
|
|