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:
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* mod-paragon — Paragon (class 12) class hooks
|
||||
*
|
||||
* See README for design. This file wires Player::IsClass / HasActivePowerType
|
||||
* so Paragon can reuse other classes' mechanics in narrowly scoped contexts.
|
||||
*/
|
||||
|
||||
#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 <unordered_map>
|
||||
|
||||
class Paragon_PlayerScript : public PlayerScript
|
||||
{
|
||||
public:
|
||||
Paragon_PlayerScript() : PlayerScript("Paragon_PlayerScript", {
|
||||
PLAYERHOOK_ON_PLAYER_IS_CLASS,
|
||||
PLAYERHOOK_ON_PLAYER_HAS_ACTIVE_POWER_TYPE,
|
||||
PLAYERHOOK_ON_UPDATE,
|
||||
PLAYERHOOK_ON_LOGIN,
|
||||
PLAYERHOOK_ON_LOGOUT,
|
||||
PLAYERHOOK_ON_AFTER_UPDATE_MAX_POWER
|
||||
})
|
||||
{
|
||||
LOG_INFO("module", "[paragon] Paragon_PlayerScript registered "
|
||||
"(MultiResource.HasActivePowers={})",
|
||||
sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", false));
|
||||
}
|
||||
|
||||
[[nodiscard]] Optional<bool> OnPlayerIsClass(Player const* player, Classes unitClass, ClassContext context) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return std::nullopt;
|
||||
|
||||
// Death Knight rune / runic power ability stack (narrow on purpose).
|
||||
if (unitClass == CLASS_DEATH_KNIGHT && context == CLASS_CONTEXT_ABILITY)
|
||||
return true;
|
||||
|
||||
// Warrior ability stack: enables warrior-spec ability gates anywhere
|
||||
// they're checked. None of the currently-traced sites in core/scripts
|
||||
// gate on (CLASS_WARRIOR, CLASS_CONTEXT_ABILITY), so this is a safe
|
||||
// forward-compatible claim. Rage generation itself is gated on
|
||||
// HasActivePowerType(POWER_RAGE) and is wired below.
|
||||
if (unitClass == CLASS_WARRIOR && context == CLASS_CONTEXT_ABILITY)
|
||||
return true;
|
||||
|
||||
// Reactive melee states: Overpower-on-dodge (warrior), Counterattack window (hunter).
|
||||
// We intentionally do NOT claim CLASS_ROGUE here: that context skips the generic
|
||||
// AURA_STATE_DEFENSE update on dodge (Riposte path) in Unit::ProcDamageAndSpellFor.
|
||||
if (context == CLASS_CONTEXT_ABILITY_REACTIVE)
|
||||
{
|
||||
if (unitClass == CLASS_WARRIOR || unitClass == CLASS_HUNTER)
|
||||
return true;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool OnPlayerHasActivePowerType(Player const* player, Powers power) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return false;
|
||||
|
||||
if (power == POWER_RUNIC_POWER || power == POWER_RUNE)
|
||||
return true;
|
||||
|
||||
if (sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", false))
|
||||
{
|
||||
switch (power)
|
||||
{
|
||||
case POWER_MANA:
|
||||
case POWER_RAGE:
|
||||
case POWER_ENERGY:
|
||||
case POWER_FOCUS:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ChrClasses.dbc says POWER_RUNE has no pool (Unit::GetCreatePowers returns 0
|
||||
// for POWER_RUNE), so Player::InitStatsForLevel and the level-reset path
|
||||
// both clobber MaxPower(POWER_RUNE)=0 every login. The 3.3.5 client greys
|
||||
// the action button for any rune-cost spell when UnitPowerMax(player,
|
||||
// SPELL_POWER_RUNES) is 0 — visual rune pips can still animate via
|
||||
// PLAYER_RUNE_REGEN_*, but the spell stays unusable.
|
||||
//
|
||||
// Fix: pin POWER_RUNE max=8 (and current=8 in the easy spots) for Paragon
|
||||
// anywhere UpdateMaxPower runs. Rune availability is still authoritatively
|
||||
// tracked in m_runes; this just keeps the client engine happy.
|
||||
void OnPlayerAfterUpdateMaxPower(Player* player, Powers& power, float& value) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (power == POWER_RUNE)
|
||||
value = 8.0f;
|
||||
else if (power == POWER_RUNIC_POWER)
|
||||
value = 1000.0f;
|
||||
}
|
||||
|
||||
// Login: re-claim MaxPower for POWER_RUNE / POWER_RUNIC_POWER and seed
|
||||
// the rune state to the client. Player::LoadFromDB runs InitStatsForLevel
|
||||
// which calls Unit::SetMaxPower with the value from GetCreatePowers — and
|
||||
// GetCreatePowers returns 0 for POWER_RUNE (engine assumption: only DK has
|
||||
// runes; class is identified via getClass(), not IsClass()). So we have to
|
||||
// overwrite MaxPower here once the player is fully loaded. ResyncRunes is
|
||||
// a one-shot push so the rune frame is correct on first appearance.
|
||||
void OnPlayerLogin(Player* player) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
player->SetMaxPower(POWER_RUNE, 8);
|
||||
if (player->GetMaxPower(POWER_RUNIC_POWER) <= 0)
|
||||
player->SetMaxPower(POWER_RUNIC_POWER, 1000);
|
||||
player->ResyncRunes(MAX_RUNES);
|
||||
|
||||
ParagonRuneSyncState seed;
|
||||
seed.lastReadyMask = 0;
|
||||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||||
if (player->GetRuneCooldown(i) == 0)
|
||||
seed.lastReadyMask |= (1u << i);
|
||||
runeSyncByGuid[player->GetGUID()] = seed;
|
||||
}
|
||||
|
||||
void OnPlayerLogout(Player* player) override
|
||||
{
|
||||
if (!player)
|
||||
return;
|
||||
runeSyncByGuid.erase(player->GetGUID());
|
||||
}
|
||||
|
||||
// Per-tick rune state pump for Paragon.
|
||||
//
|
||||
// Two responsibilities:
|
||||
//
|
||||
// 1. UNIT_FIELD_POWER5 (POWER_RUNE) tracks the live ready-rune count. The
|
||||
// 3.3.5 client's local "is this action usable" check on rune-cost
|
||||
// spells reads UnitPower(player, SPELL_POWER_RUNES); for DK that's
|
||||
// engine-managed but Paragon must own it.
|
||||
//
|
||||
// 2. Push SMSG_RESYNC_RUNES *only* on ready-bitmask transitions (rune
|
||||
// consumed → on cd, or finished cd → ready). We deliberately do NOT
|
||||
// push during the cooldown — Player::ResyncRunes encodes the per-rune
|
||||
// "passed cooldown" byte as `255 - cd_ms * 51`, which is monotonic
|
||||
// only for cd values in seconds (0..5). With cd in milliseconds the
|
||||
// byte oscillates wildly tick-to-tick (cd=10000 → 207, cd=9000 → 7,
|
||||
// cd=8000 → 63). Frequent resyncs feed the client garbage and freeze
|
||||
// the rune frame. One mask-change resync is enough; sweeps run on the
|
||||
// client's own 10s timer started from SMSG_SPELL_GO's CAST_FLAG_RUNE_LIST
|
||||
// payload (which uses correct encoding) plus a Lua-side
|
||||
// GetRuneCooldown poll in RuneFrame.lua.
|
||||
void OnPlayerUpdate(Player* player, uint32 /*p_time*/) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
uint8 readyMask = 0;
|
||||
uint8 readyCount = 0;
|
||||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||||
{
|
||||
if (player->GetRuneCooldown(i) == 0)
|
||||
{
|
||||
readyMask |= (1u << i);
|
||||
++readyCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (uint32(readyCount) != player->GetPower(POWER_RUNE))
|
||||
player->SetPower(POWER_RUNE, readyCount);
|
||||
|
||||
ParagonRuneSyncState& st = runeSyncByGuid[player->GetGUID()];
|
||||
if (readyMask != st.lastReadyMask)
|
||||
{
|
||||
player->ResyncRunes(MAX_RUNES);
|
||||
st.lastReadyMask = readyMask;
|
||||
}
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Diag.RuneTrace", false))
|
||||
return;
|
||||
|
||||
static thread_local time_t lastLogged = 0;
|
||||
time_t const wall = GameTime::GetGameTime().count();
|
||||
if (wall - lastLogged < 5)
|
||||
return;
|
||||
lastLogged = wall;
|
||||
|
||||
std::string out;
|
||||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||||
{
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "[%u: type=%u cur=%u cd=%u]",
|
||||
i,
|
||||
uint32(player->GetBaseRune(i)),
|
||||
uint32(player->GetCurrentRune(i)),
|
||||
uint32(player->GetRuneCooldown(i)));
|
||||
out += buf;
|
||||
}
|
||||
LOG_INFO("module", "[paragon-diag] {} runes: {} mask=0x{:02x}",
|
||||
player->GetName(), out, uint32(readyMask));
|
||||
}
|
||||
|
||||
private:
|
||||
struct ParagonRuneSyncState
|
||||
{
|
||||
uint8 lastReadyMask{0xFFu}; // sentinel: no prior snapshot
|
||||
};
|
||||
|
||||
static std::unordered_map<ObjectGuid, ParagonRuneSyncState> runeSyncByGuid;
|
||||
};
|
||||
|
||||
std::unordered_map<ObjectGuid, Paragon_PlayerScript::ParagonRuneSyncState> Paragon_PlayerScript::runeSyncByGuid;
|
||||
|
||||
void AddSC_paragon()
|
||||
{
|
||||
new Paragon_PlayerScript();
|
||||
}
|
||||
Reference in New Issue
Block a user