Paragon: server-authoritative CP/rune sync + cascade-spell revoke hardening

mod-paragon Paragon_Essence.cpp:
- Broaden SkillLinesLinkedToSpell: collect every SkillLineAbility row for
  an anchor spell regardless of AcquireMethod, so anchor spells whose
  primary SLA uses AcquireMethod 0 (e.g. Blood Strike) correctly identify
  their skill lines and let the dependent classifier do its job.
- IsSpellSkillLineCascadeDependent / RevokeUnwantedCascadeSpellsForPlayer
  use the broadened helper. HandleCommit calls the post-purchase sweep
  immediately so the spellbook never carries lingering cascade dependents
  (Blood Presence / Forceful Deflection / Death Coil / Death Grip).
- New character_paragon_panel_spell_revoked table tracks which active
  dependents we've revoked per (guid, parent) so OnPlayerLogin can
  re-revoke them after AC's _LoadSkills -> learnSkillRewardedSpells
  silently re-grants them.
- OnPlayerLogin opens the client SILENCE window via SendSilenceOpenForCommit
  with an empty allow list and intentionally omits the matching
  SendSilenceClose: the chat frame buffers CHAT_MSG_SYSTEM during the
  loading screen and only flushes after PLAYER_ENTERING_WORLD, so a paired
  CLOSE would shut the filter before the buffered "you have unlearned X"
  toasts hit it. The addon's 8s fail-open closes the window after the flush.
- New `.paragon hat` chat command for diagnosing Honor Among Thieves
  triggers (talent rank, learned spell, applied aura, proc table entry).

mod-paragon Paragon_SC.cpp:
- OnPlayerUpdate pushes server-authoritative combo points to the client
  via PARAA "R CP <n>" whenever the count changes. The client-side
  ComboFrame Paragon simulator listens for this and updates the target
  frame, fixing HAT-generated CP not displaying (HAT's trigger casts
  with a null target, which the combat-log inference path can't see).
- OnPlayerUpdate also pushes "R RUNES <cd0..cd5>" (ms remaining per
  rune slot) on rune mask changes, so the client RuneFrame simulator
  stays in lock-step with Spell::TakeRunePower instead of drifting
  through combat-log latency.

mod-paragon SQL:
- New updates/2026_05_09_00.sql migration creates
  character_paragon_panel_spell_revoked for AC's auto-DBUpdater so a
  fresh checkout can stand up an existing characters DB without
  manual intervention. Matching CREATE TABLE IF NOT EXISTS in
  base/character_paragon_panel_learned.sql for fresh installs.

mod-paragon conf:
- New Paragon.Diag.PanelLearn flag traces every PanelLearnSpellChain
  commit (chain ids, before/after spell-map sizes, side-spell
  classification) for diagnosing "spell reappears on relog" bugs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-09 11:22:57 -04:00
parent 81df32963f
commit 4d2a80ddb8
5 changed files with 676 additions and 16 deletions
+51 -4
View File
@@ -5,15 +5,20 @@
* so Paragon can reuse other classes' mechanics in narrowly scoped contexts.
*/
#include "Chat.h"
#include "Config.h"
#include "GameTime.h"
#include "Log.h"
#include "ObjectGuid.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "UnitDefines.h"
#include "Config.h"
#include "Log.h"
#include "GameTime.h"
#include "ObjectGuid.h"
#include "WorldPacket.h"
#include "WorldSession.h"
#include <fmt/format.h>
#include <string>
#include <unordered_map>
class Paragon_PlayerScript : public PlayerScript
@@ -184,6 +189,45 @@ public:
{
player->ResyncRunes(MAX_RUNES);
st.lastReadyMask = readyMask;
// Authoritative rune CD pump (PARAA "R RUNES cd0 cd1 ... cd5",
// ms remaining per slot, 0 = ready). The 3.3.5 client engine
// class-gates SMSG_RESYNC_RUNES / SMSG_SPELL_GO RUNE_LIST to DK,
// so the Paragon RuneFrame sim drives the visual entirely off
// COMBAT_LOG_EVENT_UNFILTERED:SPELL_CAST_SUCCESS. The combat log
// arrives ~100200ms after the server already started the
// cooldown, so the client's local timer trails the server. When
// the user spams a rune spell, the server's slot refreshes
// first, accepts the next cast, but the client UI still shows
// CD remaining → "leak-through" past a greyed icon. Pushing the
// actual remaining ms on every mask transition keeps the
// visual locked to server state.
std::string body = "R RUNES";
for (uint8 i = 0; i < MAX_RUNES; ++i)
body += " " + std::to_string(player->GetRuneCooldown(i));
std::string const payload = std::string(kParagonAddonPrefix) + "\t" + body;
WorldPacket runePkt;
ChatHandler::BuildChatPacket(runePkt, CHAT_MSG_WHISPER, LANG_ADDON, player, player, payload);
player->SendDirectMessage(&runePkt);
}
// Combo point pump: the 3.3.5 client engine class-gates SMSG_UPDATE_COMBO_POINTS
// to rogue / druid, so the Paragon UI sim never sees CP changes from
// Honor Among Thieves / Mutilate / etc. via either the engine state or
// the client-side combat-log inference (HAT's 51699 trigger fires with a
// null target and doesn't always emit SPELL_CAST_SUCCESS in the log).
// Push the count over PARAA whenever it changes; the addon's combo
// simulator listens for "R CP <n>" and overwrites paragonCP, so the
// ComboFrame on the target frame paints reliably.
int8 const cp = player->GetComboPoints();
if (cp != st.lastCp)
{
std::string const payload = std::string(kParagonAddonPrefix) + "\t"
+ fmt::format("R CP {}", int32(cp));
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, LANG_ADDON, player, player, payload);
player->SendDirectMessage(&data);
st.lastCp = cp;
}
if (!sConfigMgr->GetOption<bool>("Paragon.Diag.RuneTrace", false))
@@ -214,8 +258,11 @@ private:
struct ParagonRuneSyncState
{
uint8 lastReadyMask{0xFFu}; // sentinel: no prior snapshot
int8 lastCp{-1}; // sentinel: no prior snapshot
};
static constexpr char const* kParagonAddonPrefix = "PARAA";
static std::unordered_map<ObjectGuid, ParagonRuneSyncState> runeSyncByGuid;
};