Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7de018f7eb | |||
| abb25f56d1 | |||
| 7a92231614 | |||
| f2952c905a | |||
| 8abd40f217 |
@@ -13,16 +13,22 @@ This file is the table of contents and install guide.
|
||||
|
||||
| Artifact | Size | Purpose |
|
||||
|---|---|---|
|
||||
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, talent-tab DBC entries, and the Paragon resource bar definitions. Required for character creation as Paragon to even show up. |
|
||||
| `patch-enUS-5.MPQ` | ~50 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, and a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable). |
|
||||
| `patch-enUS-6.MPQ` | ~160 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression. |
|
||||
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, game-table DBCs, and a patched `Spell.dbc`: **(1)** `RuneCostID` zeroed on every rune-cost spell so non–Death Knight clients still send DK casts (rune costs are shown via `RuneFrame.lua`); **(2)** `Reagent[]` / `ReagentCount[]` zeroed on every spell whose `SpellFamilyName` is non-zero (all class abilities), while profession crafts (`SpellFamilyName == 0`) keep their materials. Both edits mirror server load-time corrections so client preflight and server validation stay aligned. Required for character creation as Paragon to even show up. |
|
||||
| `patch-enUS-5.MPQ` | ~57 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, Paragon stat tooltips on the character sheet (including filtering duplicate “attack power from strength” lines so the paper doll matches server AP), a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable), and **PetFrame** re-anchored so the **pet unit frame sits below the rune row** for Paragon (stock layout had runes overlapping the pet portrait). The paper-doll **ammo slot** follows stock visibility rules (shown for hunters / ranged weapons; hidden when `UnitHasRelicSlot` applies). |
|
||||
| `patch-enUS-6.MPQ` | ~134 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression, a **PETS** tab with live hunter pet talent trees (preview learn, no TE/AE cost), a dedicated **Reset Pet Talents** control (server `PARAA` `C RESET PET TALENTS` — instant, no gold, no confirmation; requires matching worldserver), bottom-row **Reset all Abilities / Reset Build / Reset all Talents** disabled while on the PETS tab so those paths cannot dismiss the pet or unlearn Tame Beast, and a **Builds** page (full-pane overlay opened from the bottom-row Builds button) for saving named, icon-tagged loadouts: New Build (+) icon picker reuses `MACRO_ICON_FILENAMES`, right-click for edit/delete, shift-left-click to favorite (favorites bubble to the top), left-click pops a Load Build confirm. Build swaps reset + refund AE/TE, re-spend on the saved recipe, and **park hunter pets** to `PET_SAVE_NOT_IN_SLOT` so their name/talents/exp are preserved across swaps. |
|
||||
| `Wow.exe` | ~7.5 MB | 3.3.5a (build 12340) client byte-patched to skip the MPQ signature check so custom `patch-enUS-N.MPQ` files load. Diff against stock is a few bytes; everything else is unchanged. |
|
||||
|
||||
Server and client work as a pair: the addon talks to `mod-paragon` on the
|
||||
worldserver via `WHISPER` addon-channel messages with the `PARAA` prefix
|
||||
(currency push, spell/talent snapshot, commit, combo points, rune
|
||||
cooldowns, learn-toast silence window). Mismatched versions usually
|
||||
manifest as the panel rendering blank or AE/TE reading 0/0.
|
||||
cooldowns, learn-toast silence window, **`C RESET PET TALENTS`**
|
||||
for hunter pet talent resets from the Character Advancement PETS tab,
|
||||
and the **build catalog** verbs `Q BUILDS` / `C BUILD NEW` / `C BUILD
|
||||
EDIT` / `C BUILD DELETE` / `C BUILD FAVORITE` / `C BUILD LOAD` for the
|
||||
saved-loadout system on the Builds page). Build swaps require the
|
||||
matching worldserver image because the swap path is server-driven
|
||||
(snapshot → reset → re-spend → pet park/unpark). Mismatched versions
|
||||
usually manifest as the panel rendering blank or AE/TE reading 0/0.
|
||||
|
||||
---
|
||||
|
||||
@@ -233,7 +239,12 @@ tools\build_paragon_advancement_patch.ps1 -Deploy # -> patch-enUS-6.MPQ
|
||||
|
||||
`patch-enUS-4.MPQ` is the DBC + GlueXML bake; the bake scripts live with
|
||||
the rest of the dev tooling and are not part of this repo by design
|
||||
(see the repo-tidy policy in `README.txt` next to this file).
|
||||
(see the repo-tidy policy in `README.txt` next to this file). Typical
|
||||
order on a maintainer machine:
|
||||
|
||||
1. `fractured-tooling/from-workspace-root/_patch_spell_dbc_runes.py` — stage `Spell.dbc` with `RuneCostID` cleared.
|
||||
2. `fractured-tooling/from-workspace-root/_patch_spell_dbc_reagents.py` — same staged `Spell.dbc`, clear class-spell reagents for client preflight.
|
||||
3. `fractured-tooling/from-workspace-root/_make_paragon_dbc_patch.py` — rebuild `ChrClasses` / `CharBaseInfo` / game tables, then pack `patch-enUS-4.MPQ`.
|
||||
|
||||
The patched `Wow.exe` is a one-time hex-edit of the stock 3.3.5a
|
||||
client. The diff is publicly documented in the WoW emulation community
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- mod-paragon Character Advancement: Build catalog (saved loadouts).
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- A "build" is a named, icon-tagged loadout of panel-purchased spells and
|
||||
-- talent ranks. Each Paragon character can save many builds and swap
|
||||
-- between them via the Builds page in the Character Advancement panel.
|
||||
--
|
||||
-- Swap workflow (see HandleBuildLoad in Paragon_Builds.cpp):
|
||||
-- 1. If a build is currently active, snapshot the player's current
|
||||
-- panel-purchased spells + per-spec talent ranks into that build's
|
||||
-- recipe rows (overwriting the stored recipe).
|
||||
-- 2. If the active build's hunter pet is currently summoned, unsummon
|
||||
-- it to PET_SAVE_NOT_IN_SLOT and store its `pet_number` on the
|
||||
-- active build row so it can be restored on swap-back.
|
||||
-- 3. Reset all panel-bought abilities and talents (refunding AE/TE).
|
||||
-- 4. Re-buy each spell + talent in the target build's recipe (charging
|
||||
-- AE/TE; aborts if insufficient AE/TE -- player keeps refunded
|
||||
-- currency in that case and active becomes NULL).
|
||||
-- 5. Move the target build's parked pet (if any) back to current.
|
||||
-- 6. Update active_build pointer.
|
||||
--
|
||||
-- Pet ownership: a parked pet sits in `character_pet` with slot=100
|
||||
-- (PET_SAVE_NOT_IN_SLOT), exactly like the engine's stable-master
|
||||
-- offload, but tied to the build via `pet_number` instead of any
|
||||
-- in-game stable slot. Build deletion drops the parked pet rows
|
||||
-- entirely (PET_SAVE_AS_DELETED equivalent) -- player is warned.
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `character_paragon_builds` (
|
||||
`build_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`guid` INT UNSIGNED NOT NULL COMMENT 'characters.guid',
|
||||
`name` VARCHAR(32) NOT NULL,
|
||||
`icon` VARCHAR(64) NOT NULL DEFAULT 'INV_Misc_QuestionMark',
|
||||
`is_favorite` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`pet_number` INT UNSIGNED NULL COMMENT 'character_pet.id of parked hunter pet, NULL when no pet bound to this build',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`build_id`),
|
||||
KEY `idx_guid` (`guid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='mod-paragon: saved Character Advancement build catalog';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `character_paragon_build_spells` (
|
||||
`build_id` INT UNSIGNED NOT NULL,
|
||||
`spell_id` INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`build_id`, `spell_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='mod-paragon: per-build recipe -- panel-purchased spells';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `character_paragon_build_talents` (
|
||||
`build_id` INT UNSIGNED NOT NULL,
|
||||
`spec` TINYINT UNSIGNED NOT NULL COMMENT '0 = primary spec, 1 = secondary (dual spec)',
|
||||
`talent_id` SMALLINT UNSIGNED NOT NULL,
|
||||
`rank` TINYINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`build_id`, `spec`, `talent_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='mod-paragon: per-build recipe -- panel-purchased talent ranks per spec';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `character_paragon_active_build` (
|
||||
`guid` INT UNSIGNED NOT NULL COMMENT 'characters.guid',
|
||||
`build_id` INT UNSIGNED NOT NULL COMMENT 'character_paragon_builds.build_id (per-character active pointer)',
|
||||
PRIMARY KEY (`guid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='mod-paragon: pointer to whichever build is currently loaded (one row per Paragon character)';
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "Chat.h"
|
||||
#include "CommandScript.h"
|
||||
#include "Config.h"
|
||||
#include "Pet.h"
|
||||
#include "Player.h"
|
||||
#include "RBAC.h"
|
||||
#include "ScriptMgr.h"
|
||||
@@ -28,6 +29,8 @@
|
||||
#include <fmt/format.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -1539,6 +1542,743 @@ void PushSnapshot(Player* pl)
|
||||
PushTalentSnapshot(pl);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Build catalog (saved Character Advancement loadouts).
|
||||
// ----------------------------------------------------------------------------
|
||||
// See data/sql/db-characters/updates/2026_05_10_03.sql for the schema and
|
||||
// the architectural overview. The "active build" pointer is a per-character
|
||||
// row in `character_paragon_active_build`; if no row exists the player has
|
||||
// no active build (free-floating loadout, default state).
|
||||
//
|
||||
// Wire format (PARAA addon channel):
|
||||
//
|
||||
// Q BUILDS -- request catalog
|
||||
// C BUILD NEW <name>\t<icon> -- create empty build
|
||||
// C BUILD EDIT <id>\t<name>\t<icon> -- rename / re-icon
|
||||
// C BUILD DELETE <id> -- delete + drop parked pet
|
||||
// C BUILD FAVORITE <id> <0|1> -- toggle favorite flag
|
||||
// C BUILD LOAD <id> -- swap to this build
|
||||
//
|
||||
// Server replies push `R BUILDS` after every mutation. Format:
|
||||
//
|
||||
// R BUILDS active=<id|->\t<id>:<fav>:<haspet>:<name>:<icon>; ...
|
||||
//
|
||||
// Names and icon paths are sanitized server-side: name = ASCII printable
|
||||
// up to 32 chars (no '\t', '\r', '\n', ';', ':' since those are wire
|
||||
// separators); icon = filename suffix only (no slashes), capped at 64
|
||||
// chars. The client renders the icon as
|
||||
// "Interface\\Icons\\<icon>".
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
constexpr char const* kDefaultBuildIcon = "INV_Misc_QuestionMark";
|
||||
constexpr std::size_t kBuildNameMaxLen = 32;
|
||||
constexpr std::size_t kBuildIconMaxLen = 64;
|
||||
|
||||
std::string SanitizeBuildName(std::string s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s)
|
||||
{
|
||||
// Reject wire separators and control characters. Keep printable
|
||||
// ASCII + space; everything else dropped silently. WoW's font
|
||||
// engine handles UTF-8 input but our wire format is ; : \t so
|
||||
// we conservatively limit to ASCII to keep the serializer
|
||||
// simple and the parser unambiguous.
|
||||
if (c == '\t' || c == '\r' || c == '\n' || c == ';' || c == ':')
|
||||
continue;
|
||||
if (c < 0x20 || c == 0x7F)
|
||||
continue;
|
||||
out += c;
|
||||
}
|
||||
if (out.size() > kBuildNameMaxLen)
|
||||
out.resize(kBuildNameMaxLen);
|
||||
// Trim leading/trailing whitespace.
|
||||
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
|
||||
auto first = std::find_if(out.begin(), out.end(), notSpace);
|
||||
auto last = std::find_if(out.rbegin(), out.rend(), notSpace).base();
|
||||
if (first >= last)
|
||||
return std::string();
|
||||
return std::string(first, last);
|
||||
}
|
||||
|
||||
std::string SanitizeBuildIcon(std::string s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s)
|
||||
{
|
||||
// Icon file paths are alnum + underscore + hyphen + dot only.
|
||||
// No slashes (we always prepend "Interface\\Icons\\" client-side).
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.')
|
||||
out += c;
|
||||
}
|
||||
if (out.size() > kBuildIconMaxLen)
|
||||
out.resize(kBuildIconMaxLen);
|
||||
if (out.empty())
|
||||
out = kDefaultBuildIcon;
|
||||
return out;
|
||||
}
|
||||
|
||||
uint32 GetActiveBuildId(uint32 lowGuid)
|
||||
{
|
||||
if (QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT build_id FROM character_paragon_active_build WHERE guid = {}", lowGuid))
|
||||
return r->Fetch()[0].Get<uint32>();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SetActiveBuildId(uint32 lowGuid, uint32 buildId)
|
||||
{
|
||||
if (buildId)
|
||||
CharacterDatabase.DirectExecute(
|
||||
"REPLACE INTO character_paragon_active_build (guid, build_id) VALUES ({}, {})",
|
||||
lowGuid, buildId);
|
||||
else
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_active_build WHERE guid = {}", lowGuid);
|
||||
}
|
||||
|
||||
bool BuildBelongsToPlayer(uint32 lowGuid, uint32 buildId)
|
||||
{
|
||||
if (!buildId)
|
||||
return false;
|
||||
QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT 1 FROM character_paragon_builds WHERE build_id = {} AND guid = {}",
|
||||
buildId, lowGuid);
|
||||
return r != nullptr;
|
||||
}
|
||||
|
||||
void PushBuildCatalog(Player* pl)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
uint32 const active = GetActiveBuildId(lowGuid);
|
||||
|
||||
std::string activeStr = active ? std::to_string(active) : std::string("-");
|
||||
std::string body = fmt::format("R BUILDS active={}\t", activeStr);
|
||||
|
||||
if (QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT build_id, is_favorite, pet_number, name, icon "
|
||||
"FROM character_paragon_builds WHERE guid = {} "
|
||||
"ORDER BY is_favorite DESC, build_id ASC", lowGuid))
|
||||
{
|
||||
bool first = true;
|
||||
do
|
||||
{
|
||||
Field const* f = r->Fetch();
|
||||
uint32 id = f[0].Get<uint32>();
|
||||
uint8 fav = f[1].Get<uint8>();
|
||||
bool haspet = !f[2].IsNull() && f[2].Get<uint32>() != 0;
|
||||
std::string name = f[3].Get<std::string>();
|
||||
std::string icon = f[4].Get<std::string>();
|
||||
if (!first)
|
||||
body += ';';
|
||||
first = false;
|
||||
body += fmt::format("{}:{}:{}:{}:{}", id,
|
||||
static_cast<unsigned>(fav),
|
||||
haspet ? 1 : 0,
|
||||
name, icon);
|
||||
} while (r->NextRow());
|
||||
}
|
||||
|
||||
SendAddonMessage(pl, body);
|
||||
}
|
||||
|
||||
bool HandleBuildNew(Player* pl, std::string const& payload, std::string* err)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
*err = "not a Paragon";
|
||||
return false;
|
||||
}
|
||||
auto tab = payload.find('\t');
|
||||
if (tab == std::string::npos)
|
||||
{
|
||||
*err = "BUILD NEW malformed";
|
||||
return false;
|
||||
}
|
||||
std::string name = SanitizeBuildName(payload.substr(0, tab));
|
||||
std::string icon = SanitizeBuildIcon(payload.substr(tab + 1));
|
||||
if (name.empty())
|
||||
{
|
||||
*err = "build name is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
|
||||
// Soft cap: prevent a runaway script from creating thousands of
|
||||
// empty builds. 64 is far above what fits in the UI grid; the
|
||||
// client also enforces its own limit based on visible cells.
|
||||
if (QueryResult cnt = CharacterDatabase.Query(
|
||||
"SELECT COUNT(*) FROM character_paragon_builds WHERE guid = {}", lowGuid))
|
||||
{
|
||||
if (cnt->Fetch()[0].Get<uint32>() >= 64)
|
||||
{
|
||||
*err = "build limit reached (64)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"INSERT INTO character_paragon_builds (guid, name, icon) VALUES ({}, '{}', '{}')",
|
||||
lowGuid, name, icon);
|
||||
|
||||
PushBuildCatalog(pl);
|
||||
LOG_INFO("module", "Paragon build: {} created build '{}'", pl->GetName(), name);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HandleBuildEdit(Player* pl, std::string const& payload, std::string* err)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
*err = "not a Paragon";
|
||||
return false;
|
||||
}
|
||||
// payload is "<id>\t<name>\t<icon>"
|
||||
auto t1 = payload.find('\t');
|
||||
if (t1 == std::string::npos)
|
||||
{
|
||||
*err = "BUILD EDIT malformed";
|
||||
return false;
|
||||
}
|
||||
auto t2 = payload.find('\t', t1 + 1);
|
||||
if (t2 == std::string::npos)
|
||||
{
|
||||
*err = "BUILD EDIT malformed";
|
||||
return false;
|
||||
}
|
||||
uint32 buildId = static_cast<uint32>(std::strtoul(payload.substr(0, t1).c_str(), nullptr, 10));
|
||||
std::string name = SanitizeBuildName(payload.substr(t1 + 1, t2 - t1 - 1));
|
||||
std::string icon = SanitizeBuildIcon(payload.substr(t2 + 1));
|
||||
if (!buildId)
|
||||
{
|
||||
*err = "BUILD EDIT bad id";
|
||||
return false;
|
||||
}
|
||||
if (name.empty())
|
||||
{
|
||||
*err = "build name is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
if (!BuildBelongsToPlayer(lowGuid, buildId))
|
||||
{
|
||||
*err = "build does not belong to player";
|
||||
return false;
|
||||
}
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_paragon_builds SET name = '{}', icon = '{}' WHERE build_id = {}",
|
||||
name, icon, buildId);
|
||||
|
||||
PushBuildCatalog(pl);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Permanently delete a parked pet (as if the player abandoned it at the
|
||||
// stable master). This is intentionally destructive -- the client warns
|
||||
// the user before reaching this code path. Mirrors the engine's
|
||||
// PET_SAVE_AS_DELETED behavior in DeleteFromDB but scoped to the rows
|
||||
// we know about (the pet itself is unsummoned and not present in
|
||||
// PetStable.CurrentPet, so this is purely a DB cleanup).
|
||||
void DeleteParkedPet(uint32 lowGuid, uint32 petNumber)
|
||||
{
|
||||
if (!petNumber)
|
||||
return;
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_pet WHERE owner = {} AND id = {}", lowGuid, petNumber);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM pet_aura WHERE guid = {}", petNumber);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM pet_spell WHERE guid = {}", petNumber);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM pet_spell_cooldown WHERE guid = {}", petNumber);
|
||||
}
|
||||
|
||||
bool HandleBuildDelete(Player* pl, std::string const& payload, std::string* err)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
*err = "not a Paragon";
|
||||
return false;
|
||||
}
|
||||
uint32 const buildId = static_cast<uint32>(std::strtoul(payload.c_str(), nullptr, 10));
|
||||
if (!buildId)
|
||||
{
|
||||
*err = "BUILD DELETE bad id";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
|
||||
// Look up parked pet (and verify ownership) in a single query.
|
||||
QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT pet_number FROM character_paragon_builds WHERE build_id = {} AND guid = {}",
|
||||
buildId, lowGuid);
|
||||
if (!r)
|
||||
{
|
||||
*err = "build not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
Field const* f = r->Fetch();
|
||||
uint32 petNumber = f[0].IsNull() ? 0 : f[0].Get<uint32>();
|
||||
|
||||
// If the build being deleted is currently active, clear the
|
||||
// active pointer first so the player ends up in the "no active
|
||||
// build" state. Their currently-learned spells/talents are
|
||||
// preserved (the client warns them about this -- they keep the
|
||||
// loadout but lose the named slot and the parked pet).
|
||||
uint32 const active = GetActiveBuildId(lowGuid);
|
||||
if (active == buildId)
|
||||
SetActiveBuildId(lowGuid, 0);
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_build_spells WHERE build_id = {}", buildId);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_build_talents WHERE build_id = {}", buildId);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_builds WHERE build_id = {} AND guid = {}",
|
||||
buildId, lowGuid);
|
||||
|
||||
if (petNumber)
|
||||
DeleteParkedPet(lowGuid, petNumber);
|
||||
|
||||
PushBuildCatalog(pl);
|
||||
LOG_INFO("module", "Paragon build: {} deleted build {} (parked pet {})",
|
||||
pl->GetName(), buildId, petNumber);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HandleBuildFavorite(Player* pl, std::string const& payload, std::string* err)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
*err = "not a Paragon";
|
||||
return false;
|
||||
}
|
||||
auto sp = payload.find(' ');
|
||||
if (sp == std::string::npos)
|
||||
{
|
||||
*err = "BUILD FAVORITE malformed";
|
||||
return false;
|
||||
}
|
||||
uint32 buildId = static_cast<uint32>(std::strtoul(payload.substr(0, sp).c_str(), nullptr, 10));
|
||||
int flag = std::atoi(payload.substr(sp + 1).c_str());
|
||||
if (!buildId)
|
||||
{
|
||||
*err = "BUILD FAVORITE bad id";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
if (!BuildBelongsToPlayer(lowGuid, buildId))
|
||||
{
|
||||
*err = "build does not belong to player";
|
||||
return false;
|
||||
}
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_paragon_builds SET is_favorite = {} WHERE build_id = {}",
|
||||
flag ? 1 : 0, buildId);
|
||||
|
||||
PushBuildCatalog(pl);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Snapshot the player's current panel-purchased state into a build's
|
||||
// recipe rows. Wipes the build's existing recipe rows first so this is
|
||||
// idempotent. Reads `character_paragon_panel_spells` (already authoritative
|
||||
// for purchased spells) and walks `Player::m_talents` per spec for
|
||||
// purchased talents that ALSO appear in `character_paragon_panel_talents`
|
||||
// (so we don't accidentally capture talents the player learned via some
|
||||
// non-panel mechanism -- e.g. trainer dual-spec gift).
|
||||
void SnapshotBuildFromCurrent(Player* pl, uint32 buildId)
|
||||
{
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_build_spells WHERE build_id = {}", buildId);
|
||||
CharacterDatabase.DirectExecute(
|
||||
"DELETE FROM character_paragon_build_talents WHERE build_id = {}", buildId);
|
||||
|
||||
if (QueryResult sp = CharacterDatabase.Query(
|
||||
"SELECT spell_id FROM character_paragon_panel_spells WHERE guid = {}", lowGuid))
|
||||
{
|
||||
do
|
||||
{
|
||||
uint32 sid = sp->Fetch()[0].Get<uint32>();
|
||||
CharacterDatabase.DirectExecute(
|
||||
"INSERT IGNORE INTO character_paragon_build_spells (build_id, spell_id) VALUES ({}, {})",
|
||||
buildId, sid);
|
||||
} while (sp->NextRow());
|
||||
}
|
||||
|
||||
// Per-spec talents: query the engine's per-spec talent state via
|
||||
// ActivateSpec round-trips, intersected with the panel-bought set
|
||||
// so we only record talents the player actually paid for.
|
||||
std::unordered_set<uint32> panelTalents;
|
||||
if (QueryResult pt = CharacterDatabase.Query(
|
||||
"SELECT talent_id FROM character_paragon_panel_talents WHERE guid = {}", lowGuid))
|
||||
{
|
||||
do { panelTalents.insert(pt->Fetch()[0].Get<uint32>()); } while (pt->NextRow());
|
||||
}
|
||||
|
||||
if (panelTalents.empty())
|
||||
return;
|
||||
|
||||
uint8 const origSpec = pl->GetActiveSpec();
|
||||
for (uint8 s = 0; s < pl->GetSpecsCount(); ++s)
|
||||
{
|
||||
if (s != origSpec)
|
||||
pl->ActivateSpec(s);
|
||||
// Walk PlayerTalentMap and record the rank of each panel-known
|
||||
// talent. The engine stores individual rank ids in the talent
|
||||
// map; we resolve back to the (talentId, rank) pair by walking
|
||||
// sTalentStore.
|
||||
for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i)
|
||||
{
|
||||
TalentEntry const* te = sTalentStore.LookupEntry(i);
|
||||
if (!te)
|
||||
continue;
|
||||
if (!panelTalents.count(te->TalentID))
|
||||
continue;
|
||||
uint8 rank = 0;
|
||||
for (int8 r = MAX_TALENT_RANK - 1; r >= 0; --r)
|
||||
{
|
||||
if (te->RankID[r] && pl->HasTalent(te->RankID[r], s))
|
||||
{
|
||||
rank = static_cast<uint8>(r + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!rank)
|
||||
continue;
|
||||
CharacterDatabase.DirectExecute(
|
||||
"REPLACE INTO character_paragon_build_talents "
|
||||
"(build_id, spec, talent_id, `rank`) VALUES ({}, {}, {}, {})",
|
||||
buildId, s, te->TalentID, rank);
|
||||
}
|
||||
}
|
||||
if (pl->GetActiveSpec() != origSpec)
|
||||
pl->ActivateSpec(origSpec);
|
||||
}
|
||||
|
||||
// Park the currently-summoned hunter pet (if any) into PET_SAVE_NOT_IN_SLOT
|
||||
// and bind the resulting pet_number to `buildId` so that on swap-back the
|
||||
// same pet (with its name, talents, exp) can be re-summoned. Non-hunter
|
||||
// pets (warlock demon, DK ghoul, mage water elemental) are NOT parked
|
||||
// because the engine re-summons those from a fresh template each cast,
|
||||
// so there's nothing to preserve.
|
||||
void ParkActivePetForBuild(Player* pl, uint32 buildId)
|
||||
{
|
||||
if (!pl || !buildId)
|
||||
return;
|
||||
Pet* pet = pl->GetPet();
|
||||
if (!pet || pet->getPetType() != HUNTER_PET)
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
uint32 const petNumber = pet->GetCharmInfo() ? pet->GetCharmInfo()->GetPetNumber() : 0;
|
||||
|
||||
pl->RemovePet(pet, PET_SAVE_NOT_IN_SLOT);
|
||||
|
||||
if (!petNumber)
|
||||
return;
|
||||
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_paragon_builds SET pet_number = {} WHERE build_id = {}",
|
||||
petNumber, buildId);
|
||||
|
||||
LOG_INFO("module", "Paragon build: parked pet #{} for build {} (player {})",
|
||||
petNumber, buildId, lowGuid);
|
||||
}
|
||||
|
||||
// Reverse of ParkActivePetForBuild: if `buildId` has a parked pet,
|
||||
// move that pet from PET_SAVE_NOT_IN_SLOT back to PET_SAVE_AS_CURRENT
|
||||
// and re-summon it next to the player. Mirrors the engine's
|
||||
// HandleStableSwapPet flow (NPCHandler.cpp:576).
|
||||
void RestoreParkedPetForBuild(Player* pl, uint32 buildId)
|
||||
{
|
||||
if (!pl || !buildId)
|
||||
return;
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
|
||||
QueryResult r = CharacterDatabase.Query(
|
||||
"SELECT pet_number FROM character_paragon_builds WHERE build_id = {} AND guid = {}",
|
||||
buildId, lowGuid);
|
||||
if (!r)
|
||||
return;
|
||||
uint32 petNumber = r->Fetch()[0].IsNull() ? 0 : r->Fetch()[0].Get<uint32>();
|
||||
if (!petNumber)
|
||||
return;
|
||||
|
||||
// Refuse mid-combat / mid-instance restores -- these would race with
|
||||
// the worldserver's spawn replication and can leave a "ghost pet"
|
||||
// whose guid the client never receives. The build-load path is
|
||||
// already gated on combat upstream; this is a defense-in-depth
|
||||
// check.
|
||||
if (pl->IsInCombat() || pl->GetMap()->IsBattlegroundOrArena())
|
||||
{
|
||||
LOG_INFO("module", "Paragon build: skipping pet restore for build {} (combat/arena)",
|
||||
buildId);
|
||||
return;
|
||||
}
|
||||
|
||||
// If a pet is already current (shouldn't happen because the swap
|
||||
// path parks first, but defensively handle), park it to NOT_IN_SLOT
|
||||
// so we don't create two CurrentPet rows.
|
||||
if (Pet* existing = pl->GetPet())
|
||||
pl->RemovePet(existing, PET_SAVE_NOT_IN_SLOT);
|
||||
|
||||
// DB: flip the parked pet's slot to AS_CURRENT.
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_pet SET slot = {} WHERE owner = {} AND id = {}",
|
||||
static_cast<int>(PET_SAVE_AS_CURRENT), lowGuid, petNumber);
|
||||
|
||||
// In-memory PetStable: move the matching UnslottedPets entry into
|
||||
// CurrentPet so subsequent SummonPet() calls resolve correctly.
|
||||
PetStable* ps = pl->GetPetStable();
|
||||
if (ps)
|
||||
{
|
||||
// First, if there's a stale CurrentPet from the existing-park
|
||||
// step above, push it back to UnslottedPets in memory.
|
||||
if (ps->CurrentPet)
|
||||
{
|
||||
ps->UnslottedPets.push_back(std::move(*ps->CurrentPet));
|
||||
ps->CurrentPet.reset();
|
||||
}
|
||||
for (auto it = ps->UnslottedPets.begin(); it != ps->UnslottedPets.end(); ++it)
|
||||
{
|
||||
if (it->PetNumber == petNumber)
|
||||
{
|
||||
ps->CurrentPet = std::move(*it);
|
||||
ps->UnslottedPets.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match HandleStableSwapPet (NPCHandler.cpp:576): when a petnumber is
|
||||
// specified, the `current` flag is ignored by GetLoadPetInfo, so use
|
||||
// false to mirror the engine convention.
|
||||
Pet* newPet = new Pet(pl, HUNTER_PET);
|
||||
if (!newPet->LoadPetFromDB(pl, 0, petNumber, false))
|
||||
{
|
||||
delete newPet;
|
||||
// Revert DB on failure so we don't strand the pet in CURRENT.
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_pet SET slot = {} WHERE owner = {} AND id = {}",
|
||||
static_cast<int>(PET_SAVE_NOT_IN_SLOT), lowGuid, petNumber);
|
||||
LOG_INFO("module", "Paragon build: pet restore failed for build {} pet #{}",
|
||||
buildId, petNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// pet_number column on the build row is now stale (the pet is
|
||||
// current, not parked). Clear it so a subsequent park can rewrite.
|
||||
CharacterDatabase.DirectExecute(
|
||||
"UPDATE character_paragon_builds SET pet_number = NULL WHERE build_id = {}",
|
||||
buildId);
|
||||
}
|
||||
|
||||
bool HandleBuildLoad(Player* pl, std::string const& payload, std::string* err)
|
||||
{
|
||||
if (!pl || pl->getClass() != CLASS_PARAGON)
|
||||
{
|
||||
*err = "not a Paragon";
|
||||
return false;
|
||||
}
|
||||
uint32 const targetId = static_cast<uint32>(std::strtoul(payload.c_str(), nullptr, 10));
|
||||
if (!targetId)
|
||||
{
|
||||
*err = "BUILD LOAD bad id";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const lowGuid = pl->GetGUID().GetCounter();
|
||||
if (!BuildBelongsToPlayer(lowGuid, targetId))
|
||||
{
|
||||
*err = "build does not belong to player";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pl->IsInCombat())
|
||||
{
|
||||
*err = "cannot swap builds while in combat";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 const activeId = GetActiveBuildId(lowGuid);
|
||||
|
||||
// No-op swap: target is already active. Refresh catalog so client
|
||||
// UI re-syncs and bail.
|
||||
if (activeId == targetId)
|
||||
{
|
||||
PushBuildCatalog(pl);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Phase 1: snapshot + park the current build's state, if any.
|
||||
// -------------------------------------------------------------
|
||||
if (activeId)
|
||||
{
|
||||
SnapshotBuildFromCurrent(pl, activeId);
|
||||
ParkActivePetForBuild(pl, activeId);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Phase 2: reset all panel-bought spells/talents (refunds AE/TE
|
||||
// through the existing reset path).
|
||||
// -------------------------------------------------------------
|
||||
std::string sub;
|
||||
if (!HandleParagonResetTalents(pl, &sub))
|
||||
{
|
||||
*err = sub;
|
||||
return false;
|
||||
}
|
||||
if (!HandleParagonResetAbilities(pl, &sub))
|
||||
{
|
||||
*err = sub;
|
||||
return false;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Phase 3: re-spend AE/TE on the target build's recipe.
|
||||
// ParagonResetAbilities/Talents above already set the active
|
||||
// build pointer to "wiped" by removing all panel rows, but it
|
||||
// hasn't touched character_paragon_active_build. We update it
|
||||
// last (after success) so a partial failure leaves the player
|
||||
// in "no active build" state with refunded currency.
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// Recipe spells: reuse PanelLearnSpellChain to drive the same
|
||||
// commit path as the panel UI -- it inserts character_paragon_panel_spells
|
||||
// / panel_spell_children / panel_spell_revoked rows internally. We
|
||||
// explicitly TrySpendAE before each chain since PanelLearnSpellChain
|
||||
// does NOT debit currency on its own (matches HandleCommit's pattern).
|
||||
std::vector<uint32> recipeSpells;
|
||||
if (QueryResult sp = CharacterDatabase.Query(
|
||||
"SELECT spell_id FROM character_paragon_build_spells WHERE build_id = {}",
|
||||
targetId))
|
||||
{
|
||||
do { recipeSpells.push_back(sp->Fetch()[0].Get<uint32>()); }
|
||||
while (sp->NextRow());
|
||||
}
|
||||
|
||||
// Recipe talents: collected up front so we can pre-flight AE+TE.
|
||||
uint32 const tePerRank = sConfigMgr->GetOption<uint32>("Paragon.Currency.TE.TalentLearnCost", 1);
|
||||
uint32 const aePerRank = sConfigMgr->GetOption<uint32>("Paragon.Currency.AE.TalentLearnCost", 1);
|
||||
|
||||
struct RecipeTalent { uint8 spec; uint32 tid; uint8 rank; };
|
||||
std::vector<RecipeTalent> recipeTalents;
|
||||
uint32 talentTeCost = 0;
|
||||
uint32 talentAeCost = 0;
|
||||
if (QueryResult pt = CharacterDatabase.Query(
|
||||
"SELECT spec, talent_id, `rank` FROM character_paragon_build_talents "
|
||||
"WHERE build_id = {} ORDER BY spec ASC, talent_id ASC", targetId))
|
||||
{
|
||||
do
|
||||
{
|
||||
Field const* f = pt->Fetch();
|
||||
RecipeTalent rt{ f[0].Get<uint8>(), f[1].Get<uint32>(), f[2].Get<uint8>() };
|
||||
TalentEntry const* te = sTalentStore.LookupEntry(rt.tid);
|
||||
if (!te || !rt.rank)
|
||||
continue;
|
||||
recipeTalents.push_back(rt);
|
||||
talentTeCost += static_cast<uint32>(rt.rank) * tePerRank;
|
||||
if (te->addToSpellBook)
|
||||
talentAeCost += static_cast<uint32>(rt.rank) * aePerRank;
|
||||
} while (pt->NextRow());
|
||||
}
|
||||
|
||||
uint32 spellAeCost = 0;
|
||||
for (uint32 sid : recipeSpells)
|
||||
spellAeCost += LookupSpellAECost(sid);
|
||||
|
||||
uint32 const totalAe = spellAeCost + talentAeCost;
|
||||
if (GetAE(pl) < totalAe)
|
||||
{
|
||||
*err = fmt::format("not enough AE to load build (need {} have {})",
|
||||
totalAe, GetAE(pl));
|
||||
SetActiveBuildId(lowGuid, 0);
|
||||
SaveCurrencyToDb(pl);
|
||||
PushCurrency(pl);
|
||||
PushSnapshot(pl);
|
||||
PushBuildCatalog(pl);
|
||||
return false;
|
||||
}
|
||||
if (GetTE(pl) < talentTeCost)
|
||||
{
|
||||
*err = fmt::format("not enough TE to load build (need {} have {})",
|
||||
talentTeCost, GetTE(pl));
|
||||
SetActiveBuildId(lowGuid, 0);
|
||||
SaveCurrencyToDb(pl);
|
||||
PushCurrency(pl);
|
||||
PushSnapshot(pl);
|
||||
PushBuildCatalog(pl);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply spells (TrySpendAE per chain mirrors HandleCommit; the
|
||||
// PanelLearnSpellChain call records DB rows for us).
|
||||
for (uint32 sid : recipeSpells)
|
||||
{
|
||||
uint32 cost = LookupSpellAECost(sid);
|
||||
if (!TrySpendAE(pl, cost))
|
||||
break;
|
||||
PanelLearnSpellChain(pl, sid);
|
||||
}
|
||||
|
||||
// Apply talents per spec. OnPlayerLearnTalents (this same script)
|
||||
// debits TE per rank and AE+TE for addToSpellBook talents -- so we
|
||||
// do NOT pre-deduct currency here, only invoke LearnTalent.
|
||||
if (!recipeTalents.empty())
|
||||
{
|
||||
uint8 const origSpec = pl->GetActiveSpec();
|
||||
uint8 lastSpec = 0xFF;
|
||||
for (RecipeTalent const& rt : recipeTalents)
|
||||
{
|
||||
if (rt.spec != lastSpec)
|
||||
{
|
||||
pl->ActivateSpec(rt.spec);
|
||||
lastSpec = rt.spec;
|
||||
}
|
||||
for (uint8 r = 0; r < rt.rank; ++r)
|
||||
pl->LearnTalent(rt.tid, r, /*command=*/true);
|
||||
// Mirror HandleCommit's panel-talent persistence (the
|
||||
// OnPlayerLearnTalents hook spends currency but does NOT
|
||||
// write to character_paragon_panel_talents).
|
||||
DbUpsertPanelTalent(lowGuid, rt.tid, rt.rank);
|
||||
}
|
||||
if (pl->GetActiveSpec() != origSpec)
|
||||
pl->ActivateSpec(origSpec);
|
||||
}
|
||||
|
||||
SetActiveBuildId(lowGuid, targetId);
|
||||
SaveCurrencyToDb(pl);
|
||||
PushCurrency(pl);
|
||||
PushSnapshot(pl);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Phase 4: restore the target build's parked pet (if any).
|
||||
// -------------------------------------------------------------
|
||||
RestoreParkedPetForBuild(pl, targetId);
|
||||
|
||||
PushBuildCatalog(pl);
|
||||
LOG_INFO("module", "Paragon build: {} loaded build {}", pl->GetName(), targetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
class Paragon_Essence_PlayerScript : public PlayerScript
|
||||
{
|
||||
public:
|
||||
@@ -1558,6 +2298,7 @@ public:
|
||||
LoadCurrencyFromDb(player);
|
||||
PushCurrency(player);
|
||||
PushSnapshot(player);
|
||||
PushBuildCatalog(player);
|
||||
|
||||
// AC's character load sequence runs _LoadSkills (which fires
|
||||
// learnSkillRewardedSpells) and _LoadSpells before this hook,
|
||||
@@ -1689,6 +2430,10 @@ public:
|
||||
// "Q SNAPSHOT" -- push R SPELLS and R TALENTS for Overview
|
||||
// "C COMMIT s:... t:..." -- apply pending learns from the panel
|
||||
// "C RESET ABILITIES" / "C RESET TALENTS" / "C RESET ALL" / "C RESET EVERYTHING"
|
||||
// "C RESET PET TALENTS" -- free + instant pet talent reset (no popup,
|
||||
// no gold). Routes to Player::ResetPetTalents
|
||||
// which itself calls Pet::resetTalents and
|
||||
// refreshes the talent points.
|
||||
void OnPlayerBeforeSendChatMessage(Player* player, uint32& /*type*/, uint32& lang, std::string& msg) override
|
||||
{
|
||||
if (!player || player->getClass() != CLASS_PARAGON)
|
||||
@@ -1765,6 +2510,75 @@ public:
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
// ---------------------------------------------------------------
|
||||
// Build catalog (saved Character Advancement loadouts).
|
||||
// See PushBuildCatalog / HandleBuild* near the top of this file
|
||||
// for wire format and behavior.
|
||||
// ---------------------------------------------------------------
|
||||
if (body == "Q BUILDS")
|
||||
{
|
||||
PushBuildCatalog(player);
|
||||
return;
|
||||
}
|
||||
if (body.compare(0, 12, "C BUILD NEW ") == 0)
|
||||
{
|
||||
std::string err;
|
||||
if (!HandleBuildNew(player, body.substr(12), &err))
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
if (body.compare(0, 13, "C BUILD EDIT ") == 0)
|
||||
{
|
||||
std::string err;
|
||||
if (!HandleBuildEdit(player, body.substr(13), &err))
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
if (body.compare(0, 15, "C BUILD DELETE ") == 0)
|
||||
{
|
||||
std::string err;
|
||||
if (!HandleBuildDelete(player, body.substr(15), &err))
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
if (body.compare(0, 17, "C BUILD FAVORITE ") == 0)
|
||||
{
|
||||
std::string err;
|
||||
if (!HandleBuildFavorite(player, body.substr(17), &err))
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
if (body.compare(0, 13, "C BUILD LOAD ") == 0)
|
||||
{
|
||||
std::string err;
|
||||
if (!HandleBuildLoad(player, body.substr(13), &err))
|
||||
SendAddonMessage(player, "R ERR " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body == "C RESET PET TALENTS")
|
||||
{
|
||||
// Pet talent reset: deliberately bypasses the engine's
|
||||
// gold-cost confirmation flow. Player::ResetPetTalents
|
||||
// wraps Pet::resetTalents (which only refunds and unlearns;
|
||||
// does NOT charge gold or dismiss the pet) and re-sends the
|
||||
// talent UI to the client. Pre-conditions:
|
||||
// - the player must own a HUNTER_PET (the only pet kind
|
||||
// with a talent tree in 3.3.5)
|
||||
// - the pet must have spent at least 1 talent point
|
||||
// If either fails Player::ResetPetTalents returns silently;
|
||||
// we ack with R OK so the client UI can refresh either way.
|
||||
Pet* pet = player->GetPet();
|
||||
if (!pet || pet->getPetType() != HUNTER_PET)
|
||||
{
|
||||
SendAddonMessage(player,
|
||||
"R ERR No active hunter pet to reset.");
|
||||
return;
|
||||
}
|
||||
player->ResetPetTalents();
|
||||
SendAddonMessage(player, "R OK PET TALENTS RESET");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void OnPlayerLearnTalents(Player* player, uint32 talentId, uint32 /*talentRank*/, uint32 /*spellid*/) override
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
|
||||
#include "Chat.h"
|
||||
#include "Config.h"
|
||||
#include "Creature.h"
|
||||
#include "CreatureData.h"
|
||||
#include "GameTime.h"
|
||||
#include "Log.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "Pet.h"
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "SpellScript.h"
|
||||
#include "SpellScriptLoader.h"
|
||||
#include "UnitDefines.h"
|
||||
#include "WorldPacket.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
@@ -45,27 +47,218 @@ public:
|
||||
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)
|
||||
// ============================================================
|
||||
// Ability stack -- claim ALL nine vanilla classes.
|
||||
// ============================================================
|
||||
// CLASS_CONTEXT_ABILITY is read by every class-specific spell
|
||||
// gate in core / scripts: DK rune mechanics (Spell.cpp,
|
||||
// SpellEffects.cpp, spell_dk.cpp, SpellAuraEffects.cpp),
|
||||
// Warrior Titan's Grip / Bladestorm (Player.cpp 3783, 15432,
|
||||
// PlayerUpdates.cpp 1547), Paladin Rebuke (Player.cpp 15441),
|
||||
// Shaman dual-wield bookkeeping (Player.cpp 5028), Hunter pet
|
||||
// / Hunter's Mark gates (spell_item.cpp 3718), Druid Insect
|
||||
// Swarm / Wild Growth (SpellAuraEffects.cpp 2153, 2232),
|
||||
// Priest Spirit of Redemption out-of-bounds check (Unit.cpp
|
||||
// 14238), Rogue pickpocketing (LootHandler.cpp 86/165/385,
|
||||
// Vehicle.cpp 80). Paragon learns abilities from every class
|
||||
// through Character Advancement, so claiming all of them lets
|
||||
// every gated spell script execute its class-specific branch
|
||||
// for our players. The only downside is double-pathed scripts
|
||||
// (e.g. a spell with both warrior and rogue branches) will
|
||||
// pick whichever the script tests first -- acceptable.
|
||||
if (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.
|
||||
// ============================================================
|
||||
// Reactive melee states.
|
||||
// ============================================================
|
||||
// Warrior dodge -> AURA_STATE_DEFENSE (Overpower window).
|
||||
// Hunter parry -> AURA_STATE_HUNTER_PARRY (Counterattack).
|
||||
// We intentionally do NOT claim CLASS_ROGUE here:
|
||||
// Unit::ProcDamageAndSpellFor (Unit.cpp 12824) skips the
|
||||
// generic AURA_STATE_DEFENSE update on dodge for rogues so
|
||||
// Riposte can take over. Claiming rogue would silently kill
|
||||
// Overpower for Paragon, and Riposte already works for us via
|
||||
// the warrior-style state we already grant.
|
||||
if (context == CLASS_CONTEXT_ABILITY_REACTIVE)
|
||||
{
|
||||
if (unitClass == CLASS_WARRIOR || unitClass == CLASS_HUNTER)
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pet ownership contexts.
|
||||
// ============================================================
|
||||
// CLASS_CONTEXT_PET is read by Pet::AddToWorld, Pet::CreateBase
|
||||
// AtCreatureInfo, Pet::InitStatsForLevel (twice -- the
|
||||
// MAX_PET_TYPE bootstrap branch and the per-class attack-time
|
||||
// scaling), Pet::IsPermanentPetFor, Player::SummonPet,
|
||||
// Player::CanResummonPet, Spell::EffectTameCreature,
|
||||
// SpellEffects.cpp (CreateTamedPet debug effects, Eyes of the
|
||||
// Beast), spell_generic.cpp 1760 (charm-as-pet conversion),
|
||||
// and PlayerGossip.cpp's hunter stable check.
|
||||
//
|
||||
// The cleanest disambiguation is by the *active pet's* shape:
|
||||
// HUNTER_PET -> hunter (beast tame)
|
||||
// SUMMON_PET + DEMON type -> warlock (Imp/VW/Succ/...)
|
||||
// SUMMON_PET + UNDEAD type -> DK ghoul / Army of Dead
|
||||
// SUMMON_PET + ELEMENTAL type -> mage water / shaman fire
|
||||
// For HUNTER specifically the no-pet case is also claimed so
|
||||
// Tame Beast's EffectTameCreature gate passes during cast.
|
||||
if (context == CLASS_CONTEXT_PET)
|
||||
{
|
||||
Pet const* activePet = const_cast<Player*>(player)->GetPet();
|
||||
|
||||
// Hunter beast: claim during taming OR when a HUNTER_PET is
|
||||
// already active. This is what makes Tame Beast / Call Pet
|
||||
// / pet stable / Counterattack pet aura feedback work.
|
||||
if (unitClass == CLASS_HUNTER)
|
||||
{
|
||||
if (!activePet || activePet->getPetType() == HUNTER_PET)
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// All other classes only claim when an active SUMMON_PET is
|
||||
// present. We then disambiguate by the creature's type
|
||||
// because warlock / DK / mage / shaman all use SUMMON_PET.
|
||||
if (!activePet || activePet->getPetType() != SUMMON_PET)
|
||||
return std::nullopt;
|
||||
|
||||
CreatureTemplate const* tmpl = activePet->GetCreatureTemplate();
|
||||
if (!tmpl)
|
||||
return std::nullopt;
|
||||
|
||||
switch (unitClass)
|
||||
{
|
||||
case CLASS_WARLOCK:
|
||||
// Drives Master Demonologist / Demonic Knowledge /
|
||||
// Demonic Pact propagation, last-pet-spell tracking
|
||||
// (Pet.cpp 112), and IsPermanentPetFor (Pet.cpp
|
||||
// 2288) so demon pets persist across logins.
|
||||
if (tmpl->type == CREATURE_TYPE_DEMON)
|
||||
return true;
|
||||
break;
|
||||
case CLASS_DEATH_KNIGHT:
|
||||
// Risen Ghoul + Army of the Dead. Player.cpp 14354
|
||||
// and Pet.cpp 243 / 1046 / 2290 read this; without
|
||||
// it the ghoul is invisible to the owner mid-load
|
||||
// and ScriptedAI hooks on the ghoul mis-route.
|
||||
if (tmpl->type == CREATURE_TYPE_UNDEAD)
|
||||
return true;
|
||||
break;
|
||||
case CLASS_MAGE:
|
||||
// Glyph-of-Eternal-Water permanent Water Elemental
|
||||
// (entry 510, 37994). Used by Pet.cpp 1047/2292.
|
||||
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
|
||||
return true;
|
||||
break;
|
||||
case CLASS_SHAMAN:
|
||||
// Fire Elemental / Earth Elemental. The base
|
||||
// engine spawns these as creatures rather than
|
||||
// proper Pet instances in most code paths, so the
|
||||
// claim mostly matters for the Pet.cpp 1045 stat
|
||||
// bootstrap when one is loaded as a SUMMON_PET.
|
||||
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Warlock pet-charm context (Enslave Demon -- Unit.cpp 14828,
|
||||
// 14894, 15025). Without this claim, charming a demon as a
|
||||
// Paragon doesn't get the warlock-flavor charm semantics
|
||||
// (faction-set-on-charm, action-bar layout, charm-break logic).
|
||||
if (unitClass == CLASS_WARLOCK && context == CLASS_CONTEXT_PET_CHARM)
|
||||
return true;
|
||||
|
||||
// ============================================================
|
||||
// Equipment contexts.
|
||||
// ============================================================
|
||||
// CLASS_CONTEXT_EQUIP_RELIC: PlayerStorage.cpp 224-240 +
|
||||
// 2475-2493. Routes Librams/Idols/Totems/Misc/Sigils into
|
||||
// EQUIPMENT_SLOT_RANGED for the matching class. Claim every
|
||||
// relic-bearing class so a Paragon can drop any of them into
|
||||
// the ranged slot.
|
||||
if (context == CLASS_CONTEXT_EQUIP_RELIC)
|
||||
{
|
||||
switch (unitClass)
|
||||
{
|
||||
case CLASS_PALADIN:
|
||||
case CLASS_DRUID:
|
||||
case CLASS_SHAMAN:
|
||||
case CLASS_WARLOCK:
|
||||
case CLASS_DEATH_KNIGHT:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// CLASS_CONTEXT_EQUIP_ARMOR_CLASS: PlayerStorage.cpp 2326,
|
||||
// 2330, 2503-2523. At level 40 each class auto-learns its
|
||||
// top armor proficiency. Paragon should pick up plate (via
|
||||
// paladin/DK), shields (paladin/warrior/shaman), mail
|
||||
// (hunter/shaman), and leather (rogue) so the level-40 train
|
||||
// event grants Paragon full proficiency and we don't have to
|
||||
// hand-curate it through the Paragon proficiency SQL.
|
||||
if (context == CLASS_CONTEXT_EQUIP_ARMOR_CLASS)
|
||||
{
|
||||
switch (unitClass)
|
||||
{
|
||||
case CLASS_PALADIN:
|
||||
case CLASS_WARRIOR:
|
||||
case CLASS_DEATH_KNIGHT:
|
||||
case CLASS_HUNTER:
|
||||
case CLASS_SHAMAN:
|
||||
case CLASS_DRUID:
|
||||
case CLASS_ROGUE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// CLASS_CONTEXT_EQUIP_SHIELDS: PlayerStorage.cpp 2467-2469.
|
||||
// Lets a Paragon equip shields without a paladin/warrior/
|
||||
// shaman skill gate.
|
||||
if (context == CLASS_CONTEXT_EQUIP_SHIELDS)
|
||||
{
|
||||
switch (unitClass)
|
||||
{
|
||||
case CLASS_PALADIN:
|
||||
case CLASS_WARRIOR:
|
||||
case CLASS_SHAMAN:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// CLASS_CONTEXT_WEAPON_SWAP: PlayerStorage.cpp 1920, 2838 --
|
||||
// rogue uses cooldown spell 6123 instead of 6119 on weapon
|
||||
// swap (Quick Draw / Combat Potency interactions). Claim
|
||||
// rogue so Paragon picks up the same cooldown spell.
|
||||
if (context == CLASS_CONTEXT_WEAPON_SWAP && unitClass == CLASS_ROGUE)
|
||||
return true;
|
||||
|
||||
// ============================================================
|
||||
// Contexts we DELIBERATELY DO NOT claim:
|
||||
// ============================================================
|
||||
// CLASS_CONTEXT_STATS -- Paragon has its own STR/AGI->AP and
|
||||
// INT/SPI->SP curves wired in StatSystem.cpp's CLASS_PARAGON
|
||||
// branch (level*2 + STR + AGI - 20 etc.). Claiming any
|
||||
// vanilla class here would override our curves with theirs.
|
||||
//
|
||||
// CLASS_CONTEXT_INIT, _TELEPORT, _QUEST, _TAXI, _SKILL,
|
||||
// _GRAVEYARD, _CLASS_TRAINER, _TALENT_POINT_CALC -- all
|
||||
// used by DK Ebon Hold / druid Moonglade starting-zone
|
||||
// scripts. Paragon doesn't go through those zones and we
|
||||
// don't want our players bound to Acherus or trapped in
|
||||
// the DK starting quest gates.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fractured / AzerothCore — native VPS rolling update (git + compile).
|
||||
#
|
||||
# Run from anywhere; resolves the repository root from this script's location.
|
||||
# Typical production layout: sources in ~/src/Fractured, install prefix in ~/azeroth-server
|
||||
# (see docs/DEPLOY_LINUX_VPS.md).
|
||||
#
|
||||
# What this does:
|
||||
# 1. git pull on the current branch (optional; can skip)
|
||||
# 2. ./acore.sh compiler build — or compiler all for a full clean rebuild
|
||||
#
|
||||
# Database migrations from data/sql/updates/ run when you next start worldserver/authserver
|
||||
# (Updates.* / SourceDirectory in *.conf). This script does not start or stop daemons unless
|
||||
# you pass --run-after or set FRACTURED_POST_UPDATE_CMD.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/vps-update-server.sh
|
||||
# bash scripts/vps-update-server.sh --full
|
||||
# bash scripts/vps-update-server.sh --no-pull
|
||||
# bash scripts/vps-update-server.sh --dry-run
|
||||
# FRACTURED_POST_UPDATE_CMD='sudo systemctl restart fractured-world' bash scripts/vps-update-server.sh --run-after
|
||||
# bash scripts/vps-update-server.sh --run-after 'sudo systemctl restart fractured-world'
|
||||
#
|
||||
# Environment:
|
||||
# FRACTURED_GIT_REMOTE — remote name (default: origin)
|
||||
# FRACTURED_POST_UPDATE_CMD — shell command run after a successful compile (if --run-after is passed without an argument, this is used)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
NO_PULL=0
|
||||
FULL_BUILD=0
|
||||
COMPILE_ONLY=0
|
||||
DRY_RUN=0
|
||||
DO_RUN_AFTER=0
|
||||
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
|
||||
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Fractured VPS update — git pull + compiler (see header in script for full notes).
|
||||
|
||||
Usage:
|
||||
bash scripts/vps-update-server.sh [options]
|
||||
|
||||
Options:
|
||||
--no-pull Skip git pull (only compile current tree).
|
||||
--full ./acore.sh compiler all (clean + configure + compile).
|
||||
--compile-only ./acore.sh compiler compile (incremental).
|
||||
--dry-run Print commands without running them.
|
||||
--run-after [CMD] Run shell command after successful compile. If CMD is omitted,
|
||||
uses FRACTURED_POST_UPDATE_CMD from the environment.
|
||||
|
||||
Environment:
|
||||
FRACTURED_GIT_REMOTE Git remote (default: origin).
|
||||
FRACTURED_POST_UPDATE_CMD Used with bare --run-after.
|
||||
EOF
|
||||
}
|
||||
|
||||
run() {
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
printf '[dry-run] '
|
||||
printf '%q ' "$@"
|
||||
printf '\n'
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--no-pull)
|
||||
NO_PULL=1
|
||||
shift
|
||||
;;
|
||||
--full)
|
||||
FULL_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--compile-only)
|
||||
COMPILE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--run-after)
|
||||
DO_RUN_AFTER=1
|
||||
shift
|
||||
if [[ $# -gt 0 && "$1" != -* ]]; then
|
||||
POST_UPDATE_CMD="$1"
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "error: unknown option: $1" >&2
|
||||
echo "Try: bash scripts/vps-update-server.sh --help" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$FULL_BUILD" -eq 1 && "$COMPILE_ONLY" -eq 1 ]]; then
|
||||
echo "error: use only one of --full or --compile-only" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT/.git" ]]; then
|
||||
echo "error: not a git clone: $ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ROOT/acore.sh" ]]; then
|
||||
echo "error: acore.sh not found under $ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ROOT/conf/config.sh" ]]; then
|
||||
echo "error: missing $ROOT/conf/config.sh — copy conf/dist/config.sh and edit (see DEPLOY_LINUX_VPS.md)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
if [[ "$DO_RUN_AFTER" -eq 1 && -z "${POST_UPDATE_CMD// }" ]]; then
|
||||
echo "error: --run-after needs a command or FRACTURED_POST_UPDATE_CMD set in the environment." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
current_branch() {
|
||||
git symbolic-ref -q --short HEAD || git rev-parse --short HEAD
|
||||
}
|
||||
|
||||
if [[ "$NO_PULL" -eq 0 ]]; then
|
||||
ref="$(current_branch)"
|
||||
if [[ "$ref" == "HEAD" ]]; then
|
||||
echo "error: detached HEAD; checkout a branch or use --no-pull." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> git pull $GIT_REMOTE $ref"
|
||||
run git pull "$GIT_REMOTE" "$ref"
|
||||
else
|
||||
echo "==> skipping git pull (--no-pull)"
|
||||
fi
|
||||
|
||||
echo "==> ensuring acore.sh and JSONPath are executable"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
run chmod +x acore.sh deps/jsonpath/JSONPath.sh
|
||||
else
|
||||
chmod +x acore.sh deps/jsonpath/JSONPath.sh 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [[ "$FULL_BUILD" -eq 1 ]]; then
|
||||
echo "==> ./acore.sh compiler all (clean, configure, compile)"
|
||||
run ./acore.sh compiler all
|
||||
elif [[ "$COMPILE_ONLY" -eq 1 ]]; then
|
||||
echo "==> ./acore.sh compiler compile (incremental; build dir must exist)"
|
||||
run ./acore.sh compiler compile
|
||||
else
|
||||
echo "==> ./acore.sh compiler build (configure + compile)"
|
||||
run ./acore.sh compiler build
|
||||
fi
|
||||
|
||||
if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
|
||||
echo "==> post-update: $POST_UPDATE_CMD"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
printf '[dry-run] eval %q\n' "$POST_UPDATE_CMD"
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
eval "$POST_UPDATE_CMD"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Done. Restart authserver/worldserver (or your service manager) when ready so new binaries and SQL updates apply."
|
||||
@@ -385,6 +385,13 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (getClass() == CLASS_PARAGON)
|
||||
{
|
||||
// Fractured class 12: same hybrid curve as requested for Paragon UI
|
||||
// (level*2 + AGI + STR - 20). Implemented in core so we do not rely
|
||||
// on PlayerScript hooks in this hot path.
|
||||
val2 = level * 2.0f + GetStat(STAT_AGILITY) + GetStat(STAT_STRENGTH) - 20.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
val2 = GetStat(STAT_AGILITY) - 10.0f;
|
||||
@@ -481,6 +488,10 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (getClass() == CLASS_PARAGON)
|
||||
{
|
||||
val2 = level * 2.0f + GetStat(STAT_STRENGTH) + GetStat(STAT_AGILITY) - 20.0f;
|
||||
}
|
||||
else if (IsClass(CLASS_MAGE, CLASS_CONTEXT_STATS) || IsClass(CLASS_PRIEST, CLASS_CONTEXT_STATS) || IsClass(CLASS_WARLOCK, CLASS_CONTEXT_STATS))
|
||||
{
|
||||
val2 = GetStat(STAT_STRENGTH) - 10.0f;
|
||||
|
||||
@@ -9046,6 +9046,21 @@ int32 Unit::SpellBaseDamageBonusDone(SpellSchoolMask schoolMask)
|
||||
DoneAdvertisedBenefit += ToPlayer()->GetBaseSpellPowerBonus();
|
||||
DoneAdvertisedBenefit += ToPlayer()->GetBaseSpellDamageBonus();
|
||||
|
||||
// Fractured class 12 (Paragon) intrinsic spell power:
|
||||
// SP = level*2 + INT + SPI - 20 (clamped at 0)
|
||||
// Read live from current stats so character-sheet refreshes (via
|
||||
// UpdateSpellDamageAndHealingBonus) and live spell casts both see the
|
||||
// up-to-date value with no script hooks or m_baseSpellPower mutation.
|
||||
if (ToPlayer()->getClass() == CLASS_PARAGON)
|
||||
{
|
||||
int32 paragonSP = int32(GetLevel()) * 2
|
||||
+ int32(GetStat(STAT_INTELLECT))
|
||||
+ int32(GetStat(STAT_SPIRIT))
|
||||
- 20;
|
||||
if (paragonSP > 0)
|
||||
DoneAdvertisedBenefit += paragonSP;
|
||||
}
|
||||
|
||||
// Damage bonus from stats
|
||||
AuraEffectList const& mDamageDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT);
|
||||
for (AuraEffectList::const_iterator i = mDamageDoneOfStatPercent.begin(); i != mDamageDoneOfStatPercent.end(); ++i)
|
||||
@@ -9803,6 +9818,20 @@ int32 Unit::SpellBaseHealingBonusDone(SpellSchoolMask schoolMask)
|
||||
AdvertisedBenefit += ToPlayer()->GetBaseSpellPowerBonus();
|
||||
AdvertisedBenefit += ToPlayer()->GetBaseSpellHealingBonus();
|
||||
|
||||
// Fractured class 12 (Paragon) intrinsic spell power: same level*2 +
|
||||
// INT + SPI - 20 floor as on the damage side (the character sheet
|
||||
// shows a single Spell Power value, so both sides must add the same
|
||||
// bonus).
|
||||
if (ToPlayer()->getClass() == CLASS_PARAGON)
|
||||
{
|
||||
int32 paragonSP = int32(GetLevel()) * 2
|
||||
+ int32(GetStat(STAT_INTELLECT))
|
||||
+ int32(GetStat(STAT_SPIRIT))
|
||||
- 20;
|
||||
if (paragonSP > 0)
|
||||
AdvertisedBenefit += paragonSP;
|
||||
}
|
||||
|
||||
// Healing bonus from stats
|
||||
AuraEffectList const& mHealingDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT);
|
||||
for (AuraEffectList::const_iterator i = mHealingDoneOfStatPercent.begin(); i != mHealingDoneOfStatPercent.end(); ++i)
|
||||
|
||||
@@ -5368,6 +5368,44 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
LockEntry* key = const_cast<LockEntry*>(sLockStore.LookupEntry(36)); // 3366 Opening, allows to open without proper key
|
||||
key->Type[2] = LOCK_KEY_NONE;
|
||||
|
||||
// Fractured: strip reagent requirements from every player-class spell at
|
||||
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
||||
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
||||
// tailoring, engineering, inscription, mining, herbalism, skinning, fishing,
|
||||
// first aid — all SpellFamilyName == SPELLFAMILY_GENERIC == 0) keep their
|
||||
// mats and only the class abilities that asked for ankhs / candles / soul
|
||||
// shards / verdant spheres / etc. cast freely. Done here in core spell
|
||||
// data rather than as a runtime bypass in Spell::CheckItems / TakeReagents
|
||||
// so the change is data-driven (the in-memory SpellInfo simply has no
|
||||
// reagents to require). The client-side preflight is mirrored by the
|
||||
// matching Spell.dbc patch shipped via patch-enUS-4.MPQ
|
||||
// (fractured-tooling/_patch_spell_dbc_reagents.py).
|
||||
{
|
||||
uint32 fixedClassSpells = 0;
|
||||
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
|
||||
{
|
||||
SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId);
|
||||
if (!info || info->SpellFamilyName == 0)
|
||||
continue;
|
||||
|
||||
bool hadAny = false;
|
||||
for (uint32 i = 0; i < MAX_SPELL_REAGENTS; ++i)
|
||||
if (info->Reagent[i] != 0 || info->ReagentCount[i] != 0)
|
||||
{ hadAny = true; break; }
|
||||
if (!hadAny)
|
||||
continue;
|
||||
|
||||
SpellInfo* mut = const_cast<SpellInfo*>(info);
|
||||
for (uint32 i = 0; i < MAX_SPELL_REAGENTS; ++i)
|
||||
{
|
||||
mut->Reagent[i] = 0;
|
||||
mut->ReagentCount[i] = 0;
|
||||
}
|
||||
++fixedClassSpells;
|
||||
}
|
||||
LOG_INFO("server.loading", ">> Fractured: cleared reagents on {} class spells", fixedClassSpells);
|
||||
}
|
||||
|
||||
LOG_INFO("server.loading", ">> Loading spell dbc data corrections in {} ms", GetMSTimeDiffToNow(oldMSTime));
|
||||
LOG_INFO("server.loading", " ");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user