Fractured: Paragon core hooks, mod-paragon, mod-ale, Docker build cap

- Track mod-paragon and mod-ale (un-ignore modules in .gitignore).
- Ship docker-compose.override.yml with CMAKE_EXTRA_OPTIONS for LuaJIT (mod-ale).
- Dockerfile: CBUILD_PARALLEL default to limit OOM under Docker/WSL2.
- Core: CLASS_PARAGON sticky combo points (DetachComboTarget), selection rebind,
  Spell::CheckPower rune path for multi-resource Paragon.
- spell_dk_death_rune: IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY) for
  Blood of the North / Reaping / DRM on Paragon.
- Remove temporary Paragon CheckPower logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-08 00:03:09 -04:00
parent f9f2bc5e0c
commit 8e4c8f57e4
163 changed files with 54817 additions and 10 deletions
@@ -11501,6 +11501,25 @@ void Player::SetSelection(ObjectGuid guid)
if (NeedSendSpectatorData())
ArenaSpectator::SendCommand_GUID(FindMap(), GetGUID(), "TRG", guid);
// mod-paragon: sticky combo points must follow the player when targets
// change. The client filters SMSG_UPDATE_COMBO_POINTS by the embedded
// target GUID — without this re-bind the dots vanish off the new target
// frame on each swap even though Unit::m_comboPoints is intact. Mirrors
// the rebind path inside Unit::AddComboPoints. Applies to every class
// that participates in the sticky-CP pool (Paragon + Rogue + Druid),
// gated behind the same `Paragon.StickyComboPoints` config switch.
if (sConfigMgr->GetOption<bool>("Paragon.StickyComboPoints", true)
&& GetComboPoints() > 0
&& !guid.IsEmpty())
{
uint8 const cls = getClass();
if (cls == CLASS_PARAGON || cls == CLASS_ROGUE || cls == CLASS_DRUID)
{
if (Unit* newTarget = ObjectAccessor::GetUnit(*this, guid))
RebindComboTarget(newTarget);
}
}
}
void Player::SetGroup(Group* group, int8 subgroup)
+107 -1
View File
@@ -25,6 +25,7 @@
#include "CellImpl.h"
#include "CharacterCache.h"
#include "CharmInfo.h"
#include "Config.h"
#include "Chat.h"
#include "ChatPackets.h"
#include "ChatTextBuilder.h"
@@ -13224,6 +13225,37 @@ void Unit::RestoreDisplayId()
SetDisplayId(GetNativeDisplayId());
}
// mod-paragon: returns true if this unit is a player whose combo-point pool
// should persist across target swaps. Originally Paragon-only; widened to
// cover the natively combo-point-using classes (Rogue, Druid) so the same
// "stored CP pool" UX applies to them. Gate retains the existing config key
// `Paragon.StickyComboPoints` for backward compatibility.
static bool IsStickyComboPointsClass(Unit const* unit)
{
if (!unit || !unit->IsPlayer())
return false;
if (!sConfigMgr->GetOption<bool>("Paragon.StickyComboPoints", true))
return false;
uint8 cls = unit->ToPlayer()->getClass();
return cls == CLASS_PARAGON || cls == CLASS_ROGUE || cls == CLASS_DRUID;
}
uint8 Unit::GetComboPoints(Unit const* who) const
{
if (IsStickyComboPointsClass(this))
return m_comboPoints;
return (who && m_comboTarget != who) ? 0 : m_comboPoints;
}
uint8 Unit::GetComboPoints(ObjectGuid const& guid) const
{
if (IsStickyComboPointsClass(this))
return m_comboPoints;
return (m_comboTarget && m_comboTarget->GetGUID() == guid) ? m_comboPoints : 0;
}
void Unit::AddComboPoints(Unit* target, int8 count)
{
if (!count)
@@ -13231,6 +13263,32 @@ void Unit::AddComboPoints(Unit* target, int8 count)
return;
}
if (IsStickyComboPointsClass(this))
{
if (target)
{
if (target != m_comboTarget)
{
if (m_comboTarget)
m_comboTarget->RemoveComboPointHolder(this);
m_comboTarget = target;
target->AddComboPointHolder(this);
}
}
m_comboPoints = std::max<int8>(std::min<int8>(m_comboPoints + count, 5), 0);
if (!m_comboPoints && m_comboTarget)
{
m_comboTarget->RemoveComboPointHolder(this);
m_comboTarget = nullptr;
}
SendComboPoints();
return;
}
if (target && target != m_comboTarget)
{
if (m_comboTarget)
@@ -13250,6 +13308,20 @@ void Unit::AddComboPoints(Unit* target, int8 count)
SendComboPoints();
}
void Unit::RebindComboTarget(Unit* newTarget)
{
if (!newTarget || newTarget == m_comboTarget || m_comboPoints <= 0)
return;
if (m_comboTarget)
m_comboTarget->RemoveComboPointHolder(this);
m_comboTarget = newTarget;
newTarget->AddComboPointHolder(this);
SendComboPoints();
}
void Unit::ClearComboPoints()
{
if (!m_comboTarget)
@@ -13267,6 +13339,26 @@ void Unit::ClearComboPoints()
m_comboTarget = nullptr;
}
void Unit::DetachComboTarget()
{
// mod-paragon: sticky-CP holder cleanup. Used when the target unit is
// dying / despawning but we want to keep the holder's m_comboPoints
// intact so they can finish on a fresh target. Pool re-binds via
// Unit::RebindComboTarget on the next SetSelection (or via the next
// AddComboPoints if the player generates more before tabbing).
//
// This intentionally does NOT touch SPELL_AURA_RETAIN_COMBO_POINTS and
// does NOT call SendComboPoints — sticky-CP classes drive their target-
// frame paint client-side (ComboFrame.lua's simulator for Paragon, the
// sticky cache for Rogue/Druid), and a fake "binding to nullguid" packet
// would just confuse the engine.
if (m_comboTarget)
{
m_comboTarget->RemoveComboPointHolder(this);
m_comboTarget = nullptr;
}
}
void Unit::SendComboPoints()
{
if (m_cleanupDone)
@@ -13309,7 +13401,21 @@ void Unit::ClearComboPointHolders()
{
while (!m_ComboPointHolders.empty())
{
(*m_ComboPointHolders.begin())->ClearComboPoints(); // this also removes it from m_comboPointHolders
Unit* holder = *m_ComboPointHolders.begin();
// mod-paragon: this is the *target* dying / despawning, iterating
// every player who has CPs anchored to it. Stock behavior wipes the
// holder's pool — that's the literal opposite of what
// Paragon.StickyComboPoints promises. For sticky-CP classes
// (Paragon / Rogue / Druid) detach the binding only and keep the
// count, so the next finisher on a fresh target still has fuel.
if (IsStickyComboPointsClass(holder))
{
holder->DetachComboTarget(); // also removes holder from m_ComboPointHolders
}
else
{
holder->ClearComboPoints(); // this also removes it from m_comboPointHolders
}
}
}
+14 -2
View File
@@ -1011,8 +1011,8 @@ public:
void AddExtraAttacks(uint32 count);
// Combot points system
[[nodiscard]] uint8 GetComboPoints(Unit const* who = nullptr) const { return (who && m_comboTarget != who) ? 0 : m_comboPoints; }
[[nodiscard]] uint8 GetComboPoints(ObjectGuid const& guid) const { return (m_comboTarget && m_comboTarget->GetGUID() == guid) ? m_comboPoints : 0; }
[[nodiscard]] uint8 GetComboPoints(Unit const* who = nullptr) const;
[[nodiscard]] uint8 GetComboPoints(ObjectGuid const& guid) const;
[[nodiscard]] Unit* GetComboTarget() const { return m_comboTarget; }
[[nodiscard]] ObjectGuid const GetComboTargetGUID() const { return m_comboTarget ? m_comboTarget->GetGUID() : ObjectGuid::Empty; }
@@ -1020,6 +1020,18 @@ public:
void AddComboPoints(int8 count) { AddComboPoints(nullptr, count); }
void ClearComboPoints();
// mod-paragon: re-anchor an existing combo-point pool to a different
// target without changing the count, then push SMSG_UPDATE_COMBO_POINTS.
// Used by Player::SetSelection so sticky combo points keep displaying on
// the new target frame after a target swap.
void RebindComboTarget(Unit* newTarget);
// mod-paragon: sticky-CP holder cleanup when the *target* dies / despawns.
// Detaches m_comboTarget (and removes self from the dying unit's holder
// set) but intentionally leaves m_comboPoints intact, so the next
// RebindComboTarget on a fresh target still has a pool to anchor.
void DetachComboTarget();
void AddComboPointHolder(Unit* unit) { m_ComboPointHolders.insert(unit); }
void RemoveComboPointHolder(Unit* unit) { m_ComboPointHolders.erase(unit); }
void ClearComboPointHolders();
+7
View File
@@ -7148,6 +7148,13 @@ SpellCastResult Spell::CheckPower()
SpellCastResult failReason = CheckRuneCost(m_spellInfo->RuneCostID);
if (failReason != SPELL_CAST_OK)
return failReason;
// Rune spells: real availability is in Player::m_runes (per-slot cooldowns),
// validated above. Do not compare UNIT_FIELD_POWER7 (POWER_RUNE) to
// ManaCost — that field is mainly a client sync/display value (often 0
// for non-DK primaries) and produces SPELL_FAILED_NO_POWER ("not enough
// runes") even when CheckRuneCost passes (e.g. Paragon multi-resource).
return SPELL_CAST_OK;
}
// Check power amount
+7 -6
View File
@@ -2643,7 +2643,12 @@ class spell_dk_death_rune : public AuraScript
bool Load() override
{
return GetUnitOwner()->IsPlayer() && GetUnitOwner()->ToPlayer()->getClass() == CLASS_DEATH_KNIGHT;
// mod-paragon: Paragon claims CLASS_DEATH_KNIGHT for CLASS_CONTEXT_ABILITY
// (Player::IsClass hook) so DK rune state + talents apply; raw getClass()
// would skip this script and Blood of the North / Reaping / Death Rune
// Mastery would never convert runes server-side.
Player* player = GetUnitOwner()->ToPlayer();
return player && player->IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY);
}
bool CheckProc(ProcEventInfo& eventInfo)
@@ -2652,11 +2657,7 @@ class spell_dk_death_rune : public AuraScript
if (!caster || !caster->IsPlayer())
return false;
Player* player = caster->ToPlayer();
if (player->getClass() != CLASS_DEATH_KNIGHT)
return false;
return true;
return caster->ToPlayer()->IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY);
}
void HandleProc(ProcEventInfo& eventInfo)