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,416 @@
|
||||
/*
|
||||
* mod-paragon — Ability Essence (AE) / Talent Essence (TE)
|
||||
*
|
||||
* Inspired by Project Ascension's classless currencies (see Fandom:
|
||||
* Ability Essence / Talent Essence). Server-side only: values persist in
|
||||
* character_paragon_currency; optional per-spell AE costs live in world
|
||||
* table paragon_spell_ae_cost. Client UI for bars / advancement panels is
|
||||
* separate (see README).
|
||||
*/
|
||||
|
||||
#include "CharacterDatabase.h"
|
||||
#include "Chat.h"
|
||||
#include "CommandScript.h"
|
||||
#include "Config.h"
|
||||
#include "Player.h"
|
||||
#include "RBAC.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "WorldDatabase.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace Acore::ChatCommands;
|
||||
|
||||
namespace
|
||||
{
|
||||
struct ParagonCurrencyData
|
||||
{
|
||||
uint32 abilityEssence = 0;
|
||||
uint32 talentEssence = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32, ParagonCurrencyData> gParagonCurrencyCache;
|
||||
|
||||
uint32 ComputeStartingAE(uint8 level)
|
||||
{
|
||||
uint32 base = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.Start", 9);
|
||||
uint32 per = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.PerLevel", 1);
|
||||
uint32 minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
|
||||
if (level >= minLv)
|
||||
base += uint32(level - minLv + 1) * per;
|
||||
return base;
|
||||
}
|
||||
|
||||
uint32 ComputeStartingTE(uint8 level)
|
||||
{
|
||||
uint32 base = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.Start", 0);
|
||||
uint32 per = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.PerLevel", 1);
|
||||
uint32 minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
|
||||
if (level >= minLv)
|
||||
base += uint32(level - minLv + 1) * per;
|
||||
return base;
|
||||
}
|
||||
|
||||
ParagonCurrencyData& GetOrCreateCacheEntry(uint32 lowGuid)
|
||||
{
|
||||
return gParagonCurrencyCache[lowGuid];
|
||||
}
|
||||
|
||||
void DbUpsertCurrency(uint32 lowGuid, uint32 ae, uint32 te)
|
||||
{
|
||||
CharacterDatabase.DirectExecute(
|
||||
"REPLACE INTO character_paragon_currency (guid, ability_essence, talent_essence) VALUES ({}, {}, {})",
|
||||
lowGuid, ae, te);
|
||||
}
|
||||
|
||||
void LoadCurrencyFromDb(Player* player)
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = player->GetGUID().GetCounter();
|
||||
|
||||
if (QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT ability_essence, talent_essence FROM character_paragon_currency WHERE guid = {}", lowGuid))
|
||||
{
|
||||
Field const* f = r->Fetch();
|
||||
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
|
||||
d.abilityEssence = f[0].Get<uint32>();
|
||||
d.talentEssence = f[1].Get<uint32>();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 const ae = ComputeStartingAE(player->GetLevel());
|
||||
uint32 const te = ComputeStartingTE(player->GetLevel());
|
||||
DbUpsertCurrency(lowGuid, ae, te);
|
||||
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
|
||||
d.abilityEssence = ae;
|
||||
d.talentEssence = te;
|
||||
}
|
||||
|
||||
void SaveCurrencyToDb(Player* player)
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = player->GetGUID().GetCounter();
|
||||
auto itr = gParagonCurrencyCache.find(lowGuid);
|
||||
if (itr == gParagonCurrencyCache.end())
|
||||
return;
|
||||
|
||||
DbUpsertCurrency(lowGuid, itr->second.abilityEssence, itr->second.talentEssence);
|
||||
}
|
||||
|
||||
void RemoveCacheEntry(uint32 lowGuid)
|
||||
{
|
||||
gParagonCurrencyCache.erase(lowGuid);
|
||||
}
|
||||
|
||||
uint32 GetAE(Player* player)
|
||||
{
|
||||
if (!player)
|
||||
return 0;
|
||||
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
|
||||
if (itr == gParagonCurrencyCache.end())
|
||||
return 0;
|
||||
return itr->second.abilityEssence;
|
||||
}
|
||||
|
||||
uint32 GetTE(Player* player)
|
||||
{
|
||||
if (!player)
|
||||
return 0;
|
||||
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
|
||||
if (itr == gParagonCurrencyCache.end())
|
||||
return 0;
|
||||
return itr->second.talentEssence;
|
||||
}
|
||||
|
||||
bool TrySpendAE(Player* player, uint32 amount)
|
||||
{
|
||||
if (!amount)
|
||||
return true;
|
||||
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
|
||||
if (itr == gParagonCurrencyCache.end())
|
||||
return false;
|
||||
if (itr->second.abilityEssence < amount)
|
||||
return false;
|
||||
itr->second.abilityEssence -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TrySpendTE(Player* player, uint32 amount)
|
||||
{
|
||||
if (!amount)
|
||||
return true;
|
||||
auto itr = gParagonCurrencyCache.find(player->GetGUID().GetCounter());
|
||||
if (itr == gParagonCurrencyCache.end())
|
||||
return false;
|
||||
if (itr->second.talentEssence < amount)
|
||||
return false;
|
||||
itr->second.talentEssence -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GrantLevelUpEssence(Player* player, uint8 oldLevel, uint8 newLevel)
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return;
|
||||
|
||||
uint32 const minLv = sConfigMgr->GetOption<uint32>("Paragon.Currency.GrantLevelMin", 10);
|
||||
uint32 const aePer = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.PerLevel", 1);
|
||||
uint32 const tePer = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.PerLevel", 1);
|
||||
|
||||
ParagonCurrencyData& d = GetOrCreateCacheEntry(player->GetGUID().GetCounter());
|
||||
|
||||
for (uint32 lvl = uint32(oldLevel) + 1; lvl <= uint32(newLevel); ++lvl)
|
||||
{
|
||||
if (lvl >= minLv)
|
||||
{
|
||||
d.abilityEssence += aePer;
|
||||
d.talentEssence += tePer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32 LookupSpellAECost(uint32 spellId)
|
||||
{
|
||||
uint32 const fallback = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.DefaultSpellCost", 2);
|
||||
QueryResult r = WorldDatabase.Query("SELECT ae_cost FROM paragon_spell_ae_cost WHERE spell_id = {}", spellId);
|
||||
if (!r)
|
||||
return fallback;
|
||||
return std::max<uint32>(r->Fetch()[0].Get<uint32>(), 1u);
|
||||
}
|
||||
|
||||
class Paragon_Essence_PlayerScript : public PlayerScript
|
||||
{
|
||||
public:
|
||||
Paragon_Essence_PlayerScript() : PlayerScript("Paragon_Essence_PlayerScript", {
|
||||
PLAYERHOOK_ON_LOGIN,
|
||||
PLAYERHOOK_ON_LOGOUT,
|
||||
PLAYERHOOK_ON_SAVE,
|
||||
PLAYERHOOK_ON_CREATE,
|
||||
PLAYERHOOK_ON_LEVEL_CHANGED,
|
||||
PLAYERHOOK_CAN_LEARN_TALENT,
|
||||
PLAYERHOOK_ON_PLAYER_LEARN_TALENTS
|
||||
}) { }
|
||||
|
||||
void OnPlayerLogin(Player* player) override
|
||||
{
|
||||
LoadCurrencyFromDb(player);
|
||||
}
|
||||
|
||||
void OnPlayerLogout(Player* player) override
|
||||
{
|
||||
SaveCurrencyToDb(player);
|
||||
RemoveCacheEntry(player->GetGUID().GetCounter());
|
||||
}
|
||||
|
||||
void OnPlayerSave(Player* player) override
|
||||
{
|
||||
SaveCurrencyToDb(player);
|
||||
}
|
||||
|
||||
void OnPlayerCreate(Player* player) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = player->GetGUID().GetCounter();
|
||||
uint32 const ae = ComputeStartingAE(player->GetLevel());
|
||||
uint32 const te = ComputeStartingTE(player->GetLevel());
|
||||
DbUpsertCurrency(lowGuid, ae, te);
|
||||
ParagonCurrencyData& d = GetOrCreateCacheEntry(lowGuid);
|
||||
d.abilityEssence = ae;
|
||||
d.talentEssence = te;
|
||||
}
|
||||
|
||||
void OnPlayerLevelChanged(Player* player, uint8 oldLevel) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
LoadCurrencyFromDb(player);
|
||||
GrantLevelUpEssence(player, oldLevel, player->GetLevel());
|
||||
}
|
||||
|
||||
bool OnPlayerCanLearnTalent(Player* player, TalentEntry const* /*talent*/, uint32 /*rank*/) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return true;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return true;
|
||||
|
||||
uint32 const cost = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.TalentLearnCost", 1);
|
||||
return GetTE(player) >= cost;
|
||||
}
|
||||
|
||||
void OnPlayerLearnTalents(Player* player, uint32 /*talentId*/, uint32 /*talentRank*/, uint32 /*spellid*/) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
return;
|
||||
|
||||
uint32 const cost = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.TalentLearnCost", 1);
|
||||
if (!TrySpendTE(player, cost))
|
||||
{
|
||||
LOG_ERROR("module", "Paragon TE spend failed post-learn for player {} — currency desync?", player->GetName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Paragon_Essence_CommandScript : public CommandScript
|
||||
{
|
||||
public:
|
||||
Paragon_Essence_CommandScript() : CommandScript("Paragon_Essence_CommandScript") { }
|
||||
|
||||
ChatCommandTable GetCommands() const override
|
||||
{
|
||||
static ChatCommandTable paragonSubTable =
|
||||
{
|
||||
{ "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 },
|
||||
};
|
||||
|
||||
static ChatCommandTable commandTable =
|
||||
{
|
||||
{ "paragon", paragonSubTable },
|
||||
};
|
||||
|
||||
return commandTable;
|
||||
}
|
||||
|
||||
static bool HandleCurrency(ChatHandler* handler)
|
||||
{
|
||||
Player* pl = handler->GetPlayer();
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
handler->SendErrorMessage("Paragon currency is only tracked for Paragon characters.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
{
|
||||
handler->SendErrorMessage("Paragon.Currency.Enabled is off.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
LoadCurrencyFromDb(pl);
|
||||
handler->PSendSysMessage("Ability Essence: {} | Talent Essence: {}", GetAE(pl), GetTE(pl));
|
||||
return true;
|
||||
}
|
||||
|
||||
// .paragon runes — diagnostic dump of rune state (server-side truth) and
|
||||
// forced ResyncRunes packet to the client. Helps diagnose visual-vs-server
|
||||
// desync where the rune pip animates to ready but the spell cast still
|
||||
// returns SPELL_FAILED_NO_POWER because GetRuneCooldown(i) > 0 server-side.
|
||||
static bool HandleRunes(ChatHandler* handler)
|
||||
{
|
||||
Player* pl = handler->GetPlayer();
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
handler->SendErrorMessage("Only Paragon characters have a rune block to inspect.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||||
{
|
||||
handler->PSendSysMessage("rune[{}] base={} cur={} cd={}ms grace={}ms",
|
||||
i,
|
||||
uint32(pl->GetBaseRune(i)),
|
||||
uint32(pl->GetCurrentRune(i)),
|
||||
uint32(pl->GetRuneCooldown(i)),
|
||||
uint32(pl->GetGracePeriod(i)));
|
||||
}
|
||||
|
||||
for (uint8 t = 0; t < NUM_RUNE_TYPES; ++t)
|
||||
{
|
||||
handler->PSendSysMessage("regen[{}] = {} (1/cooldown_ms)",
|
||||
uint32(t),
|
||||
pl->GetFloatValue(PLAYER_RUNE_REGEN_1 + t));
|
||||
}
|
||||
|
||||
// Force a fresh client snapshot so we can see if visual catches up.
|
||||
pl->ResyncRunes(MAX_RUNES);
|
||||
handler->PSendSysMessage("|cff00ff00ResyncRunes packet sent.|r");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleLearn(ChatHandler* handler, SpellInfo const* spell)
|
||||
{
|
||||
Player* pl = handler->GetPlayer();
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
handler->SendErrorMessage("Only Paragon characters use AE for this command.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sConfigMgr->GetOption<bool>("Paragon.Currency.Enabled", true))
|
||||
{
|
||||
handler->SendErrorMessage("Paragon.Currency.Enabled is off.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!spell)
|
||||
return false;
|
||||
|
||||
LoadCurrencyFromDb(pl);
|
||||
|
||||
uint32 const cost = LookupSpellAECost(spell->Id);
|
||||
if (GetAE(pl) < cost)
|
||||
{
|
||||
handler->PSendSysMessage("Not enough Ability Essence (need {}, have {}).", cost, GetAE(pl));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SpellMgr::IsSpellValid(spell))
|
||||
{
|
||||
handler->SendErrorMessage("Invalid spell.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pl->HasSpell(spell->Id))
|
||||
{
|
||||
handler->SendErrorMessage("You already know this spell.", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TrySpendAE(pl, cost))
|
||||
return false;
|
||||
|
||||
pl->learnSpell(spell->Id, false);
|
||||
SaveCurrencyToDb(pl);
|
||||
|
||||
handler->PSendSysMessage("Learned spell {} ({} AE spent, {} AE remaining).", spell->Id, cost, GetAE(pl));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace (anonymous)
|
||||
|
||||
void AddSC_paragon_essence()
|
||||
{
|
||||
new Paragon_Essence_PlayerScript();
|
||||
new Paragon_Essence_CommandScript();
|
||||
}
|
||||
Reference in New Issue
Block a user