Compare commits

...

20 Commits

Author SHA1 Message Date
Docker Build f2952c905a Fractured: strip class-spell reagents at load; Paragon relic ranged slot
- SpellInfoCorrections: zero Reagent/ReagentCount on spells with non-zero
  SpellFamilyName so class abilities no longer require shards, candles,
  etc., while profession crafts (SpellFamilyName 0) keep mats. Matches
  the client Spell.dbc bake in patch-enUS-4.MPQ.
- Paragon_SC: OnPlayerIsClass returns true for CLASS_CONTEXT_EQUIP_RELIC
  for paladin/druid/shaman/warlock/dk so Paragon can equip all relic types
  in the ranged slot.
- CLIENT-PATCHES: document Spell.dbc reagent pass, rune script order, and
  stock ammo slot behavior in patch-enUS-5.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 22:38:32 -04:00
Docker Build 8abd40f217 Paragon: give class 12 intrinsic AP and SP scaling from stats
Stock 3.3.5 hardcodes per-class stat -> AP/SP formulas in
Player::UpdateAttackPowerAndDamage and Unit::SpellBase{Damage,Healing}BonusDone,
so class 12 fell into the default branches and ended up with 0 AP and 0 SP
regardless of STR / AGI / INT / SPI. The character sheet, combat log, and
ability damage all reflected this, and Mental Quickness-style AP->SP plumbing
silently no-oped on Paragon characters.

Add Paragon-specific branches in core (no PlayerScript hooks - those caused
SIGSEGVs when the new mid-list enum entry shifted later hook ordinals and
broke vtable dispatch):

- StatSystem.cpp: melee and ranged AP = level*2 + STR + AGI - 20, mirroring
  the formula the UI patch already advertises in tooltips.
- Unit.cpp:       intrinsic SP    = level*2 + INT + SPI - 20 (clamped >=0),
  added symmetrically to SpellBaseDamageBonusDone and
  SpellBaseHealingBonusDone so the single advertised Spell Power value the
  character sheet renders matches what spells actually use in combat.

Drop the now-unused UnitDefines.h include in Paragon_SC.cpp - it was only
needed by the AP PlayerScript hook that was rolled back in favor of the
core change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 20:39:23 -04:00
Docker Build 34cc87a5f9 mod-paragon: fix panel cascade sweep revoking talent-granted spells
RevokeUnwantedCascadeSpellsForPlayer and RevokeBlockedSpellsForPlayer
built their allowlist only from character_paragon_panel_spells and
panel_spell_children. Many Character Advancement "abilities" (e.g.
Scourge Strike) are panel talents stored in character_paragon_panel_talents,
so learning Death Coil afterward activated DK skill lines and the sweep
removed those spells as false orphans.

Add BuildPanelOwnedSpellsAllowlist to union spell chains, talent rank spell
IDs up to the purchased rank, and passive children. Also keep the prior
fixes: clear stale panel_spell_revoked rows on purchase and skip+delete
revoke entries that now match the allowlist on login.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 17:37:08 -04:00
Docker Build f986fdcddd mod-paragon: let class 12 actually USE class-restricted glyphs / items
Two paths still rejected glyph use on Paragon characters even after the
earlier AllowableClass server bypass:

1. Spell::CheckItems (server) treated cast-from-glyph as a normal
   "equipped item required" cast and called HasItemFitToSpellRequirements,
   which only handles weapon/armor and falls through default for
   ITEM_CLASS_GLYPH -> SPELL_FAILED_EQUIPPED_ITEM_CLASS. Skip that check
   when the cast item itself is the glyph.

2. The 3.3.5 client engine pre-checks ItemTemplate.AllowableClass against
   the player's class locally and refuses the right-click before sending
   CMSG_USE_ITEM, regardless of what the server would do. Bake the
   Paragon class bit (1<<11 = 2048) into AllowableClass for every
   class-restricted item via a mod-paragon SQL migration so the engine's
   pre-check passes for class 12.

Cache caveat: clients that previously inspected an affected item have
the old AllowableClass cached in Cache/<locale>/itemcache.wdb; deleting
the Cache folder forces a re-query. The server also caches item_template
in memory at boot, so this migration only takes effect for clients after
a worldserver restart (or .reload item_template) once the SQL has been
applied -- DBUpdater handles the SQL automatically on the next start.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 16:52:37 -04:00
Docker Build a212717c37 docs: note tooltip 'Classes:' patcher in patch-enUS-5 description
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 16:08:19 -04:00
Docker Build 49cb354133 mod-paragon: ignore item AllowableClass for class 12 (gear + glyphs)
Paragon is a classless concept layered on top of WotLK class data: stock items and class glyphs gate equip / vendor visibility / loot rolls / AH 'usable' filter via ItemTemplate.AllowableClass, which never has the class-12 bit (0x800). Bypassing the gate at the five enforcement sites lets Paragon equip any class-restricted item -- including class glyphs, since EffectApplyGlyph itself has no class check beyond the item gate. Race / level / proficiency / skill / required-spell checks still apply, so Paragon can't skip baseline progression.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 16:07:01 -04:00
Docker Build 7298d89c9a mod-paragon: default HasActivePowers on for rage from white hits
Paragon OnPlayerHasActivePowerType only reported POWER_RAGE when
Paragon.MultiResource.HasActivePowers was true. Core melee rage uses
Unit::DealDamage -> HasActivePowerType(POWER_RAGE) before RewardRage;
missing module config (common on fresh clones / Docker without merged
mod_paragon.conf) fell through to GetOption(..., false) and white swings
never generated rage. Match mod_paragon.conf.dist and default the C++
fallback to true so Paragon behaves correctly out of the box. Set
Paragon.MultiResource.HasActivePowers = 0 only for intentional test builds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 15:54:39 -04:00
Docker Build 3a2ae82593 mod-paragon: combined Arcane Torrent also refunds 15 rage
Extend spell_paragon_arcane_torrent to EnergizeBySpell POWER_RAGE 150 (15
displayed; rage uses the same 10x internal scaling as runic power, see the
`-20` rage decay step in Player::Regenerate). Paragon's combined Arcane
Torrent now refunds mana, rage, energy, and runic power -- whichever pool
the character is using at the moment. ModifyPower no-ops on pools with
MaxPower == 0, so it's safe even before the Paragon picks up rage abilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 15:51:37 -04:00
Docker Build 16717acdd3 mod-paragon: combined Arcane Torrent that refunds mana, energy, and runic power
Building on the previous fix that hid the rogue and DK Arcane Torrent variants
for Paragon Blood Elves: instead of just dropping the duplicates, turn the
remaining mana variant (28730) into a single combined racial that refunds
whichever resource pool the character is using at the moment.

Add SpellScript spell_paragon_arcane_torrent in modules/mod-paragon/src/
Paragon_SC.cpp. Hooks AfterCast on 28730: when the caster is class 12 the
script EnergizeBySpell's 15 energy and 150 internal runic power (= 15 displayed,
matching stock 25046 / 50613 amounts) on top of the spell's stock mana effect.
ModifyPower no-ops on pools the player has no max for, so it is safe even
before the Paragon picks up energy- or RP-using abilities. Non-Paragon Blood
Elves are untouched and keep learning their stock racial.

Update migration 2026_05_10_03.sql to also register the script binding via
spell_script_names (28730 -> 'spell_paragon_arcane_torrent'). Idempotent
DELETE + INSERT.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 15:44:42 -04:00
Docker Build d96123e661 mod-paragon: single Arcane Torrent for Paragon Blood Elves
Blood Elf racial skill line 756 grants three different Arcane Torrent spell
IDs (28730 mana, 25046 rogue energy, 50613 DK runic power). The blanket
SkillLineAbility overlay in 2026_05_10_02 OR'd class 12 into all three, so
Paragon Blood Elves auto-learned every variant and the spellbook listed three
identical "Arcane Torrent" entries.

Add db-world migration 2026_05_10_03.sql to clear the class-12 bit on the rogue
and DK rows only (SkillLineAbility IDs 13338 and 17510), leaving 28730 as the
sole Paragon-visible racial cast. OnPlayerLogin removes 25046/50613 if still
present so existing characters self-heal without a manual unlearn.

The fractured-tooling DBC overlay generator is updated in the same workspace
to skip those two rows when regenerating SkillLineAbility SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 15:34:02 -04:00
Docker Build 8a0da95ed2 mod-paragon: bypass talent DependsOn check for Paragon class
Player::LearnTalent enforces the column-arrow prereq (talentInfo->DependsOn)
even when called with command=true, so Character Advancement's commit path
was silently dropping any talent whose Talent.dbc row points to an unrelated
sibling -- e.g. Deep Wounds (depends on Improved Heroic Strike), Bloody
Vengeance (depends on Dark Conviction), Expose Weakness (depends on Lethal
Shots). Players spent points in the panel, hit Learn All, and the talent
silently never reached addTalent / OnPlayerLearnTalents -- the snapshot came
back without it and the client repainted the points as "unspent."

The Character Advancement panel gates progression via AE/TE essence cost,
not via the spec-tree column arrows, so the DependsOn rule doesn't apply to
class 12. Skip it for Paragon, mirroring the existing class-mask bypass a
few lines above.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 15:16:30 -04:00
Docker Build 8363b1b6c8 Paragon: ship class-12 SkillLineAbility overlay so proficiency passives auto-learn
Companion to 2026_05_09_00.sql (DBC overlay for chrclasses + srci) and
2026_05_10_01.sql (proficiency skill rows in playercreateinfo_skills).
Those two grant the SKILL (Maces, Shield, Cloth, ...) to Paragon at
character creation; this one opens the SkillLineAbility rows that
CASCADE skill -> passive spell, so when a fresh Paragon is created
AC's `Player::LearnDefaultSkill` actually grants the proficiency
passives:

  Block (107), Parry (3127), Dual Wield (674), Defense, weapon Shoot,
  racial Mace/Sword Specialization, ...

Without this overlay, a class-12 Paragon spawns with the right skill
rows but a near-empty spellbook past the racials and class defaults
that come from playercreateinfo_action.

How it works
------------
AC's DBCStores.cpp::LoadDBC loads each store from the on-disk .dbc
file first, then merges <table>_dbc world-DB rows on top. Our patched
client SkillLineAbility.dbc (in patch-enUS-4.MPQ) OR's the class-12
bit (0x800) into ClassMask on 3,314 rows -- the same rows the server
needs for the cascade to fire on Paragon. Stock Docker installs use
the upstream `ac-wotlk-client-data` image which fills data/dbc/ from
a vanilla 3.3.5a extract, so without this SQL overlay the server
runs against an unmodified SkillLineAbility.dbc and the cascade
never fires.

Generation
----------
Auto-generated end-to-end by
`fractured-tooling/from-workspace-root/_gen_paragon_dbc_overlay_sql.py`,
extended in this commit to handle SkillLineAbility.dbc (14-int
WotLK layout, 56 bytes per record). The script diffs patched vs
stock by ID, keeps only rows whose stock ClassMask did NOT include
the class-12 bit but whose patched ClassMask does, and emits the
3,314 REPLACE INTO rows. Re-running with the same inputs is byte-
stable.

Verified locally
----------------
- Migration applies twice in a row at exactly 3,314 SQL overlay rows
  (idempotent: DELETE WHERE ID IN (...) before INSERT).
- ac-worldserver restart logs:
    >> Loaded 10219 SkillLineAbility MultiMap Data
  -- the same total as stock (10,219 rows), confirming our overlay
  REPLACES existing rows by ID rather than appending duplicates.
- Spot-checked spell IDs: 107 (Block, ClassMask 2115 = warrior +
  paladin + dk + Paragon), 3127 (Parry, 2063), 674 (Dual Wield,
  2157), 75 (Auto Shoot, 2052) all carry the 0x800 bit.

Existing characters
-------------------
The cascade fires inside Player::Create and Player::LearnDefaultSkill
at character spawn, so existing class-12 characters created before
this migration keep their broken state. Delete and re-roll, or hand-
grant the missing spells via .learn for individual existing chars.

CLIENT-PATCHES.md updated to add the third symptom ("proficiency
skills exist but passive spells don't auto-learn") and document
this migration as the fourth piece of the class-12 bootstrap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 14:38:55 -04:00
Docker Build 2874119c6d Paragon: ship class-12 weapon/armor proficiencies as SQL migration
Companion to 2026_05_10_00.sql. The spawn-data migration teaches the
worldserver where Paragon characters spawn and what per-level base
stats they have; this one teaches it which weapon/armor skill lines
to grant at first character login.

Without these rows a fresh Paragon character lands in their newbie
zone with no weapon or armor proficiencies (auto-attack greys out
on anything beyond a fist) -- the universal classMask=0 rows in
playercreateinfo_skills only cover Defense, Unarmed, Cloth,
languages, Mounts, and Companion Pets.

Adds 20 rows in playercreateinfo_skills with classMask=2048 (class
12 only) for every weapon and armor proficiency:
  - Weapons: Swords, Axes, Bows, Guns, Maces, 2H Swords, Dual Wield,
             Staves, 2H Maces, 2H Axes, Daggers, Thrown, Crossbows,
             Wands, Polearms, Fist Weapons.
  - Armor:   Plate Mail, Mail, Leather, Shield. (Cloth already
             granted via the classMask=0 universal row.)

Idempotent: DELETE WHERE classMask=2048 then INSERT, so it replays
cleanly on a partially-seeded DB (e.g. one where a contributor hand-
patched these rows before the migration landed).

Verified locally: applies cleanly twice in a row, worldserver restart
now logs `>> Loaded 1391 Player Create Skills` (was 1371 pre-Paragon
= +20 class-12 rows) and a freshly-rolled Draenei Paragon spawns with
the full weapon/armor kit.

CLIENT-PATCHES.md troubleshooting block updated to call out the
"Paragon spawns naked / can't equip anything" failure mode and list
all three migrations in the current rebuild recipe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 13:38:27 -04:00
Docker Build 56fa2fc7f7 docs(client): note spellbook expansion in patch-enUS-5.MPQ
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 13:24:50 -04:00
Docker Build 5deb9e3255 Paragon: ship class-12 starter spawn data so character creation works on fresh installs
Companion to 2026_05_09_00.sql (DBC overlay). The DBC overlay teaches
the world server that class 12 (Paragon) exists; this migration
teaches it WHERE class-12 characters spawn, what action bar they boot
with, and what per-level base stats Player::InitStatsForLevel uses.

Without these rows, contributors hit:
  - Player::Create -> "invalid race/class pair (R/12) - refusing"
    and the client shows "Error creating character".
  - WorldServer load -> "class-12 Level-L does not have stats data!"
    integrity warnings.

Tables touched (idempotent: DELETE WHERE class=12 then INSERT):
  - playercreateinfo         : 10 rows, every DK-eligible race spawning
                               in their racial newbie zone (Northshire,
                               Valley of Trials, Ammen Vale, ...).
                               NOT Acherus -- Paragon is from-level-1.
  - playercreateinfo_action  : 46 rows, default action bar layout
                               per race (attack 6603, eat 78, racial,
                               etc.).
  - player_class_stats       : 80 rows, per-level base HP/Mana/STR/AGI/
                               STA/INT/SPI. Curve mirrors Warrior to
                               level 60, Paladin-style HP inflation
                               past 60 to keep Paragon competitive
                               in Wrath content.

Tables intentionally untouched: playercreateinfo_item is empty for
class 12 (Paragon ships no per-class starting items, only racial
kit), and the mask-based playercreateinfo_skills/_cast_spell/
_spell_custom rows already cover class 12 via their classMask=0
"all classes" entries.

Verified locally: applies cleanly twice in a row (idempotent),
worldserver restart now logs `>> Loaded 72 Player Create Definitions`
(was 62 pre-Paragon = +10 races for class 12) and creates a Draenei
Paragon without rejection.

CLIENT-PATCHES.md troubleshooting block updated to merge the two
"Character Creation Failed" modes (DBC overlay missing + spawn data
missing) into a single fix recipe. Existing contributors with a
pre-built dbimport image need
`docker compose build ac-db-import ac-worldserver` before this
migration is visible to DBUpdater; fresh clones get it on first
`docker compose up`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 13:06:39 -04:00
Docker Build ecd8eacb1f chore(conf): revert seed defaults to stock so fresh installs auto-connect
The previous seed pinned auth/realmlist to production values
(`hsrwow.net` + RealmServerPort 47497), which silently bricked every
fresh local install: after auth login the realm hand-off pointed
clients at our public host, where their local credentials don't
exist, and they were dropped within a frame.

Seed now matches stock AzerothCore for solo dev:
- realmlist.address  = 127.0.0.1   (was hsrwow.net)
- RealmServerPort    = 3724        (was 47497)

Production owners apply both overrides post-dbimport via a one-shot
SQL UPDATE + an authserver.conf edit. Documented end-to-end in
contrib/fractured-dev-extras/BUILD-NATIVE.md (new "Production
deployment overrides" section) and the disconnect-after-login
symptom is called out in CLIENT-PATCHES.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 12:44:45 -04:00
Docker Build 1811c0ec35 Fix MariaDB dbimport: utf8mb4 collation and dbimport.conf
Use utf8mb4_unicode_ci in base SQL (MariaDB lacks utf8mb4_0900_ai_ci).
Add Updates.ExceptionShutdownDelay to dbimport.conf.dist to match
DBUpdater expectations.
2026-05-09 11:41:57 -05:00
Docker Build fae3ff5028 Paragon: ship server-side DBC overlay as SQL so fresh installs can roll class 12
Stock Docker installs fill data/dbc/ from the vanilla 3.3.5a extract
in `ac-wotlk-client-data`, which has no class 12 in ChrClasses.dbc and
no class-12 bit on SkillRaceClassInfo.dbc. CharacterHandler.cpp's
sChrClassesStore.LookupEntry(12) returns null and the create fails
with CHAR_CREATE_FAILED ("Class (12) not found in DBC ...") before the
contributor ever sees the panel. Fixing it required hand-copying the
patched DBCs onto the named volume — undocumented, fragile, and not
portable to native installs.

DBCStores.cpp::LoadDBC merges every <table>_dbc world-DB row on top of
the on-disk DBC store (storage.LoadFromDB after storage.Load). We use
that merge layer to ship Paragon's class-12 deltas as SQL:

- chrclasses_dbc: 1 row defining class 12 (Paragon, power=Mana,
  family=Warrior, expansion=2). Resolves CHAR_CREATE_FAILED.
- skillraceclassinfo_dbc: 235 rows REPLACEing stock entries with the
  patched ClassMask (class-12 bit OR'd in) so baseline skills (defense,
  weapon skills, etc.) are available to Paragon characters.

The new `modules/mod-paragon/data/sql/db-world/updates/2026_05_09_00.sql`
is applied automatically by AC's DBUpdater on every fresh `ac-db-import`
run (Docker) or first worldserver boot (native). End-to-end verified
locally: truncate -> docker compose up ac-db-import -> rows reappear
with hash 33B1A05 recorded in updates table.

The migration is auto-generated by
fractured-tooling/from-workspace-root/_gen_paragon_dbc_overlay_sql.py
(outside this repo per the repo-tidy policy). Re-run it whenever the
DBC bake changes.

CLIENT-PATCHES.md is rewritten so contributors no longer need the
manual DBC sync section as their primary install path. Manual overlay
is preserved as a labelled fallback for tools that read data/dbc/
directly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 12:19:59 -04:00
Docker Build 20a24b7935 docs(client): document worldserver DBC sync for Paragon character create
Explains why Character Creation Failed occurs when the client has
patch-enUS-4 but Docker/native data/dbc is still vanilla: ChrClasses
row 12 only exists in the patched DBC set. Adds Docker volume copy
steps, native install path, and log verification.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 12:00:16 -04:00
Docker Build 526022e2bc Add VPS clone scripts for sparse checkout without Docker
Track vps-clone-without-docker.sh and vps-sparse-checkout-no-docker.sh
so a fresh GitHub clone can run the helper end-to-end. Mark both
executable in git for ./ usage after clone.
2026-05-09 10:41:40 -05:00
27 changed files with 4615 additions and 54 deletions
+42 -6
View File
@@ -17,17 +17,53 @@ prerequisites; everything here is just the deltas you need on top of it.
## Fractured client + network defaults
Production Fractured uses a non-default **auth** port so the client realmlist can be:
Stock-friendly defaults for fresh local installs. A `git clone` ->
`docker compose up` (or native install) lets a single developer log in
from the same machine without any post-install config tweaks.
- **`authserver.conf` -> `RealmServerPort`** = **3724** (stock WoW). A
patched `Wow.exe` with `set realmlist 127.0.0.1` (no port) reaches
the auth handshake.
- **`realmlist` table -> `port`** is the **world** port (default
**8085**, matches `WorldServerPort` in `worldserver.conf.dist`).
Auth tells the client to handshake to this port for the world hand-off.
- **`realmlist` table -> `address`** defaults to **`127.0.0.1`** in the
base SQL. The auth server hands this address to clients after login,
so 127.0.0.1 means "talk to the world server on the same machine
auth is running on" -- correct for solo dev. **Override on production
deploys**, see *Production deployment overrides* below.
### Production deployment overrides
Production Fractured runs on a remote VPS at `hsrwow.net` with auth
bound to a non-stock port (47497 -- 3724 was unavailable on that host).
Apply the overrides **once per fresh dbimport** on the production box.
```sql
-- Run against acore_auth on the production database after first dbimport:
UPDATE realmlist
SET address = 'hsrwow.net',
port = 8085 -- world port; leave at 8085 unless changed
WHERE id = 1;
```
Edit the production `authserver.conf` (NOT `authserver.conf.dist`)
to bind the auth listener to the production port:
```ini
RealmServerPort = 47497
```
Restart the auth server. Production clients connect with:
```text
set realmlist hsrwow.net:47497
```
(Patched 3.3.5 clients that support `host:port`; otherwise use port forwarding to **3724**.)
- **`authserver.conf``RealmServerPort`** must be **`47497`** (matches `authserver.conf.dist` in this repo).
- **`realmlist` table → `port`** is the **world** port (default **8085**, same as `WorldServerPort` in `worldserver.conf.dist`), **not** 47497.
- **`realmlist``address`** defaults to **`hsrwow.net`** in base SQL; change if your public hostname differs.
The Fractured-patched 3.3.5 client supports the `host:port` syntax;
stock 3.3.5 clients do not, so any contributor distributing the
client bundle for production must include the patched `Wow.exe` from
the GitHub release.
---
+173 -3
View File
@@ -13,8 +13,8 @@ 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` | ~40 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards. |
| `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 nonDeath 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` | ~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, Paragon stat tooltips on the character sheet, 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). The paper-doll **ammo slot** follows stock visibility rules (shown for hunters / ranged weapons; hidden when `UnitHasRelicSlot` applies). |
| `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. |
| `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. |
@@ -52,6 +52,171 @@ worldserver image is older than commit `4d2a80d` (the
`character_paragon_panel_spell_revoked` migration). Pull both ends to
the same release tag and rebuild the worldserver image.
If the **client** shows the Paragon class on the create screen but the
server replies **Character Creation Failed** (sometimes shown as
"Error creating character") when you pick it -- **or** the character
is created but spawns with no weapon / armor proficiencies (auto-attack
greys out, can't equip anything beyond a fist), or with the proficiency
**skills** but no **passive spells** like Block, Parry, Dual Wield --
the worldserver is missing one of four pieces of class-12 data. All
ship as SQL migrations under
`modules/mod-paragon/data/sql/db-world/updates/` and are auto-applied
by AzerothCore's DBUpdater on every `ac-db-import` run, but the SQL
files are baked into the dbimport Docker image at build time -- so a
stale image won't pick up new migrations. Fix:
```bash
git pull origin main
docker compose build ac-db-import ac-worldserver
docker compose up -d ac-db-import
docker compose restart ac-worldserver
```
Existing class-12 characters created before these migrations will
keep their broken state -- the cascade only fires inside
`Player::Create` and `Player::LearnDefaultSkill` at character spawn.
Delete the old Paragon and re-roll after the rebuild.
The four migrations:
- `2026_05_09_00.sql` -- DBC overlay rows for `chrclasses_dbc` and
`skillraceclassinfo_dbc`. Without this the server can't even
resolve class 12 in `sChrClassesStore`. See **Server-side Paragon
DBC overlay** below.
- `2026_05_10_00.sql` -- `playercreateinfo`, `playercreateinfo_action`,
and `player_class_stats` rows for class 12. Without this
`Player::Create` rejects every (race, class=12) pair as an
"invalid race/class pair" and the worldserver prints
`class-N Level-L does not have stats data!` integrity warnings on
load.
- `2026_05_10_01.sql` -- 20 `playercreateinfo_skills` rows
(`classMask = 2048` = class 12) granting every weapon /
armor proficiency at level 1. Without this a Paragon spawns with
only the universal `classMask = 0` skills (Defense, Unarmed,
Cloth, languages, Mounts) -- no Swords, no Mail, no Shield, etc.
- `2026_05_10_02.sql` -- 3,314 `skilllineability_dbc` rows opening
the class-12 bit on every SkillLineAbility row our patched
`SkillLineAbility.dbc` modified. AC reads these rows in
`Player::LearnDefaultSkill` to drive the `skill -> passive spell`
cascade. Without it the proficiency *skills* from `_01.sql` exist
but the *passive spells* (Block, Parry, Dual Wield, Defense,
weapon Shoot, racial Mace/Sword Specialization, etc.) never auto-
learn, so the spellbook past the racials looks empty.
After the rebuild + restart, `ac-worldserver` should log
`>> Loaded 72 Player Create Definitions` (was 62 pre-Paragon),
`>> Loaded 1391 Player Create Skills` (was 1371),
`>> Loaded 10219 SkillLineAbility MultiMap Data` (unchanged total --
the SQL overlay replaces existing rows by ID, doesn't add new ones),
and character creation succeeds for any DK-eligible race with a full
weapon / armor kit and the matching passive spells.
If the client **logs in** successfully but **disconnects immediately**
when entering the realm: the auth server is handing your client the
wrong world-server address. On a fresh local install the seed defaults
to `127.0.0.1` (commit landing this paragraph). If your DB was
imported from an older Fractured checkout, the seed may still point at
`hsrwow.net`, which sends the client to our production world server
instead of yours. Fix:
```bash
# Docker:
docker exec ac-database mysql -uroot -ppassword \
-e "UPDATE acore_auth.realmlist SET address='127.0.0.1' WHERE id=1;"
docker compose restart ac-authserver
```
Substitute your public hostname/IP for `127.0.0.1` if remote players
will be connecting. See `BUILD-NATIVE.md` -> *Production deployment
overrides* for the full list of values to set on a production box.
---
## Server-side Paragon DBC overlay (automatic)
The Fractured **client** learns about Paragon from `patch-enUS-4.MPQ`
(DBC + GlueXML). The **worldserver** never reads your MPQs — it reads
plain `.dbc` files under its `DataDir` (`.../data/dbc/` by default).
Stock Docker installs populate `data/dbc/` from a vanilla 3.3.5a
extract (`ac-client-data-init` in `docker-compose.yml`). That tree has
no `ChrClasses` row for id **12** and no class-12 bit on
`SkillRaceClassInfo` rows, which would normally trigger:
`Class (12) not found in DBC while creating new char ... wrong DBC files or cheater?`
…and reject the create with `CHAR_CREATE_FAILED`.
To remove that gap, the repo ships
`modules/mod-paragon/data/sql/db-world/updates/2026_05_09_00.sql`,
which `INSERT`s the Paragon class-12 deltas into:
- `chrclasses_dbc` — 1 row defining class 12 ("Paragon", power=Mana,
family=Warrior, expansion=2).
- `skillraceclassinfo_dbc` — 235 rows replacing stock entries with the
patched ClassMask (class-12 bit OR'd in) so every baseline skill is
available to Paragon characters.
`AzerothCore`'s DBC loader (`DBCStores.cpp::LoadDBC` -> `LoadFromDB`)
merges these rows on top of whatever `data/dbc/` contains at every
worldserver boot. The DBUpdater in `ac-db-import` (Docker) or the
worldserver itself (native) applies the migration automatically — so
the **only** steps a fresh contributor needs are `git clone` and
`docker compose up -d`.
### Regenerating the migration
The SQL is auto-generated from the patched DBCs that already live
inside `patch-enUS-4.MPQ`. The bake script lives outside this repo
(per the repo-tidy policy) at:
`fractured-tooling/from-workspace-root/_gen_paragon_dbc_overlay_sql.py`
Re-run it whenever you change the Paragon DBC bake — for example,
adding a new race to the Paragon class mask. It diffs the patched
DBCs against a stock 3.3.5a DBC extract and emits a fresh
`2026_05_09_00.sql` (or successor migration with a new timestamp if
deltas change). Workflow:
```powershell
# Extract the patched DBCs once:
.\tools\mpq\mpqcli.exe extract `
"ChromieCraft_3.3.5a\Data\enUS\patch-enUS-4.MPQ" `
-o "$env:TEMP\paragon-dbc-extract"
# Regenerate the SQL migration:
python fractured-tooling\from-workspace-root\_gen_paragon_dbc_overlay_sql.py
```
If the regenerated SQL has new content, commit it as a **new** dated
migration filename (e.g. `2026_06_01_00.sql`) — never edit a file that
has already been applied to live databases, AC's DBUpdater will detect
the hash change and re-run the SQL, which can be fine but is best
reserved for emergencies.
### Manual DBC overlay (rare, fallback)
If you ever need the patched DBCs *on disk* — e.g. for a tool that
reads `data/dbc/` directly outside the worldserver, or to verify a
client-vs-server DBC mismatch — extract `patch-enUS-4.MPQ` and copy
its `DBFilesClient/*.dbc` into `data/dbc/`:
**Docker:**
```powershell
docker run --rm `
-v ac-client-data:/data `
-v ${PWD}\paragon-dbc-extract:/patch:ro `
alpine sh -c "cp -f /patch/*.dbc /data/dbc/"
docker compose restart ac-worldserver
```
**Native:** copy into `<CMAKE_INSTALL_PREFIX>/data/dbc/` and restart.
This is **not required** for normal operation — the SQL migration
covers everything `mod-paragon` needs at runtime. Use the manual
overlay only when you're consciously bypassing the SQL merge layer.
---
## Building the patches yourself
@@ -68,7 +233,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
+16 -5
View File
@@ -42,15 +42,26 @@ CREATE TABLE `realmlist` (
--
-- Dumping data for table `realmlist`
--
-- Fractured defaults: `address` / `port` are the WORLD server (must match
-- WorldServerPort in worldserver.conf). Client auth uses RealmServerPort from
-- authserver.conf (Fractured dist: 47497), e.g. set realmlist hsrwow.net:47497
-- Adjust `localAddress` if your LAN/internal routing differs.
-- Defaults are tuned for fresh local installs: `address` is what the auth
-- server hands clients after login as the WORLD server endpoint. Stock
-- 127.0.0.1 means "the same box auth is running on", so a fresh
-- `git clone` -> `docker compose up` works without any post-install
-- tweaks for a developer hosting on their own machine.
--
-- Production deployments must override `address` after first dbimport,
-- e.g.:
-- UPDATE realmlist SET address = 'your.public.host', port = 8085 WHERE id = 1;
-- See contrib/fractured-dev-extras/BUILD-NATIVE.md for the full deploy
-- checklist (auth/world ports, firewall, public hostnames).
--
-- `port` is the WORLD server port (must match WorldServerPort in
-- worldserver.conf). The auth-server LISTEN port is separately configured
-- via RealmServerPort in authserver.conf (stock default 3724).
LOCK TABLES `realmlist` WRITE;
/*!40000 ALTER TABLE `realmlist` DISABLE KEYS */;
INSERT INTO `realmlist` VALUES
(1,'Fractured WoW','hsrwow.net','127.0.0.1','255.255.255.0',8085,0,0,1,0,0,12340);
(1,'Fractured WoW','127.0.0.1','127.0.0.1','255.255.255.0',8085,0,0,1,0,0,12340);
/*!40000 ALTER TABLE `realmlist` ENABLE KEYS */;
UNLOCK TABLES;
+1 -1
View File
@@ -24,7 +24,7 @@ CREATE TABLE `world_state` (
`Id` int unsigned NOT NULL COMMENT 'Internal save ID',
`Data` longtext,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='WorldState save system';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='WorldState save system';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -27,7 +27,7 @@ CREATE TABLE `player_shapeshift_model` (
`GenderID` tinyint unsigned NOT NULL,
`ModelID` int unsigned NOT NULL,
PRIMARY KEY (`ShapeshiftID`,`RaceID`,`CustomizationID`,`GenderID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci PACK_KEYS=0;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci PACK_KEYS=0;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -25,7 +25,7 @@ CREATE TABLE `player_totem_model` (
`RaceID` tinyint unsigned NOT NULL,
`ModelID` int unsigned NOT NULL,
PRIMARY KEY (`TotemID`,`RaceID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci PACK_KEYS=0;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci PACK_KEYS=0;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -12,6 +12,11 @@ Paragon.StickyComboPoints = 1
# in addition to runes/runic power. Required for the patch-enUS-5.MPQ player
# frame to populate Mana/Rage/Energy bars - otherwise the server treats those
# powers as inactive and never sends max values, leaving the bars empty.
# Also required for core rage generation: Unit::DealDamage only calls
# RewardRage() when the attacker HasActivePowerType(POWER_RAGE); if this is off,
# Paragon white swings never grant rage (users without this line in any loaded
# config used to hit the C++ fallback default of false). Default is on; set 0
# only if you intentionally want a stripped-down Paragon test build.
Paragon.MultiResource.HasActivePowers = 1
# Ability / Talent Essence (AE/TE) — Ascension-inspired currency
@@ -0,0 +1,270 @@
-- mod-paragon: server-side DBC overlay for class 12 (Paragon).
-- Auto-generated by fractured-tooling/from-workspace-root/
-- _gen_paragon_dbc_overlay_sql.py
--
-- AzerothCore's DBCStores.cpp::LoadDBC merges every <table>_dbc
-- world-DB row on top of the on-disk DBC store at startup
-- (storage.LoadFromDB). We use that to ship Paragon's class-12
-- DBC deltas in SQL form so a stock data/dbc/ tree (e.g. the
-- vanilla `ac-wotlk-client-data` Docker image) still resolves
-- class 12 in sChrClassesStore and class-12 entries in
-- sSkillRaceClassInfoStore.
--
-- Without this migration, fresh installs hit:
-- CHAR_CREATE_FAILED -- "Class (12) not found in DBC ..."
-- the moment a contributor tries to roll a Paragon character.
--
-- This file is regenerated end-to-end from patch-enUS-4.MPQ;
-- do not hand-edit. Update the patched DBC source and rerun
-- the bake script.
-- chrclasses_dbc: classes added or modified by patch-enUS-4.MPQ.
-- AzerothCore merges this on top of the on-disk ChrClasses.dbc
-- so a stock data/dbc tree still gets class 12 at runtime.
DELETE FROM `chrclasses_dbc` WHERE `ID` IN (12);
INSERT INTO `chrclasses_dbc` (`ID`,`Field01`,`DisplayPower`,`PetNameToken`,`Name_Lang_enUS`,`Name_Lang_Mask`,`Name_Female_Lang_Mask`,`Name_Male_Lang_Mask`,`Filename`,`SpellClassSet`,`Flags`,`CinematicSequenceID`,`Required_Expansion`) VALUES
(12, 0, 0, 0, 'Paragon', 0, 0, 0, 'PARAGON', 4, 50, 0, 2);
-- skillraceclassinfo_dbc: rows where patch-enUS-4 OR'd the
-- class-12 bit (0x800) into ClassMask, opening every
-- baseline skill to Paragon. Replaces the stock row by ID so
-- AzerothCore picks the patched mask on the SQL merge pass.
DELETE FROM `skillraceclassinfo_dbc` WHERE `ID` IN (
57,301,107,82,75,140,328,638,872,880,881,885,886,910,117,335,628,629,630,912,126,127,133,134,635,31,39,135,325,636,637,643,644,888,889,914,125,626,884,898,901,58,60,916,59,40,41,68,48,49,44,45,42,43,50,51,131,132,883,913,105,71,70,69,925,54,25,138,139,91,882,85,84,93,88,865,87,441,94,443,92,481,89,442,123,124,624,625,702,908,6,922,33,243,899,241,122,621,622,701,907,970,129,323,631,632,633,634,641,642,142,143,639,640,28,63,282,29,284,65,97,244,940,72,128,878,879,137,144,136,915,55,79,81,76,149,112,111,106,66,26,83,74,73,108,109,110,113,38,35,36,37,61,62,64,24,34,21,906,46,47,52,53,281,104,102,101,27,95,98,96,30,145,146,147,148,151,155,158,159,271,175,178,183,186,270,189,191,193,198,200,265,266,203,204,205,268,269,246,272,330,381,403,445,446,461,501,463,464,521,522,541,544,581,601,741,742,781,841,861,862,866,867,877,934,892,896,897,951,895,900,936,938,939,947
);
INSERT INTO `skillraceclassinfo_dbc` (`ID`,`SkillID`,`RaceMask`,`ClassMask`,`Flags`,`MinLevel`,`SkillTierID`,`SkillCostIndex`) VALUES
(57,6,-1,2176,1040,0,0,0),
(301,8,-1,2176,1040,0,0,0),
(107,26,-1,2049,1040,0,0,0),
(82,38,-1,2056,1040,0,0,0),
(75,39,-1,2056,1040,0,0,0),
(140,43,1115,2049,128,0,0,0),
(328,43,3071,2052,128,0,0,0),
(638,43,164,2049,128,0,0,0),
(872,43,32767,2056,128,0,0,0),
(880,43,1024,2052,128,0,0,0),
(881,43,32767,2432,128,0,0,0),
(885,43,1029,2050,128,0,0,0),
(886,43,512,2050,128,0,0,0),
(910,43,262143,2080,128,0,0,0),
(117,44,166,2052,128,0,0,0),
(335,44,2147483647,2122,128,0,0,0),
(628,44,1544,2052,128,0,0,0),
(629,44,167,2049,128,0,0,0),
(630,44,1112,2049,128,0,0,0),
(912,44,262143,2080,128,0,0,0),
(126,45,650,2052,128,0,0,0),
(127,45,32767,2061,128,0,0,0),
(133,46,36,2052,128,0,0,0),
(134,46,32767,2057,128,0,0,0),
(635,46,1674,2052,128,0,0,0),
(31,50,-1,2052,1040,0,0,0),
(39,51,-1,2052,1040,0,0,0),
(135,54,2147483647,2128,128,0,0,0),
(325,54,-1,2056,128,0,0,0),
(636,54,1133,2049,128,0,0,0),
(637,54,658,2049,128,0,0,0),
(643,54,8,3072,128,0,0,0),
(644,54,32,3072,128,0,0,0),
(888,54,261631,2050,128,0,0,0),
(889,54,512,2050,128,0,0,0),
(914,54,262143,2080,128,0,0,0),
(125,55,262143,2052,128,0,0,0),
(626,55,163839,2049,128,0,0,0),
(884,55,512,2050,128,0,0,0),
(898,55,262143,2080,128,0,0,0),
(901,55,261631,2050,128,0,0,0),
(58,56,-1,2064,1040,0,0,0),
(60,78,-1,2064,1040,0,0,0),
(916,95,524287,2080,640,0,0,0),
(59,96,2047,3072,1168,0,0,0),
(40,98,1101,3583,128,0,0,0),
(41,98,674,3551,160,0,21,0),
(68,101,4,3583,1170,0,0,0),
(48,109,690,3583,128,0,0,0),
(49,109,1101,3551,160,0,21,0),
(44,111,4,3583,128,0,0,0),
(45,111,2043,3551,160,0,21,0),
(42,113,8,3583,128,0,0,0),
(43,113,2039,3551,160,0,21,0),
(50,115,32,3583,128,0,0,0),
(51,115,2015,3551,160,0,21,0),
(131,118,32767,2056,146,1,0,0),
(132,118,32767,2053,146,20,0,0),
(883,118,32767,2112,402,0,0,0),
(913,118,262143,2080,146,0,0,0),
(105,120,2047,2304,1170,0,0,0),
(71,124,32,3583,1170,0,0,0),
(70,125,2,3583,146,0,0,0),
(69,126,8,3583,1170,0,0,0),
(925,129,-1,2080,128,0,63,0),
(54,130,2047,2176,1168,4,0,0),
(25,134,-1,3072,1040,10,0,0),
(138,136,32767,3536,128,0,0,0),
(139,136,32767,2053,128,0,0,0),
(91,137,1535,3551,160,0,21,0),
(882,137,512,3583,128,0,0,0),
(85,138,2047,3583,128,0,0,0),
(84,139,2047,3583,160,0,21,0),
(93,140,2047,3583,128,0,0,0),
(88,141,2047,3583,160,0,21,0),
(865,142,2047,3583,0,0,0,0),
(87,148,1,3551,1170,0,181,0),
(441,148,222,3583,1170,0,182,0),
(94,149,2,3551,1170,0,181,0),
(443,149,509,3583,1170,0,182,0),
(92,150,8,3551,1170,0,181,0),
(481,150,215,3583,1170,0,182,0),
(89,152,4,3551,1170,0,181,0),
(442,152,219,3583,1170,0,182,0),
(123,160,262143,2050,128,0,0,0),
(124,160,-1,3072,128,0,0,0),
(624,160,32,2049,128,0,0,0),
(625,160,262111,2049,128,0,0,0),
(702,160,-1,2112,128,0,0,0),
(908,160,262143,2080,128,0,0,0),
(6,162,2147483647,3551,128,0,0,0),
(922,162,262143,2080,128,0,0,0),
(33,163,-1,2052,1040,0,0,0),
(243,164,2047,3583,160,0,41,0),
(899,165,2047,3583,160,0,41,0),
(241,171,2047,3583,160,0,41,0),
(122,172,163839,2050,128,0,0,0),
(621,172,6,2049,128,0,0,0),
(622,172,1529,2049,128,0,0,0),
(701,172,163839,2112,128,0,0,0),
(907,172,524287,2080,128,0,0,0),
(970,172,163839,2052,128,0,0,0),
(129,173,32767,2312,128,0,0,0),
(323,173,32767,2256,128,0,0,0),
(631,173,520,2052,128,0,0,0),
(632,173,1190,2052,128,0,0,0),
(633,173,216,2049,128,0,0,0),
(634,173,1063,2049,128,0,0,0),
(641,173,32,3072,128,0,0,0),
(642,173,8,3072,128,0,0,0),
(142,176,-1,2056,128,0,0,0),
(143,176,-1,2052,128,0,0,0),
(639,176,128,2049,128,0,0,0),
(640,176,262015,2049,128,0,0,0),
(28,182,2047,3583,160,0,2,0),
(63,184,-1,2050,1040,0,0,0),
(282,185,2047,3583,128,0,61,0),
(29,186,2047,3583,160,0,2,0),
(284,197,2047,3583,160,0,62,0),
(65,198,2047,2050,1168,0,0,0),
(97,199,2047,2112,1168,0,0,0),
(244,202,2047,3583,160,0,41,0),
(940,205,524287,2176,2048,0,0,0),
(72,220,16,3583,1170,0,0,0),
(128,226,32767,2057,128,0,0,0),
(878,226,1024,2052,128,0,0,0),
(879,226,31743,2052,128,0,0,0),
(137,227,2047,3077,128,0,0,0),
(144,228,-1,2448,128,0,0,0),
(136,229,32767,3079,128,20,0,0),
(915,229,262143,2080,128,0,0,0),
(55,237,-1,2176,1040,0,0,0),
(79,238,2047,2056,1168,4,0,0),
(81,239,2047,2056,1168,0,0,0),
(76,241,2047,2056,128,40,0,0),
(149,242,2047,2056,1168,16,0,0),
(112,243,2047,2049,1170,0,0,0),
(111,244,2047,2049,1168,0,0,0),
(106,245,2047,2049,1168,0,0,0),
(66,246,2047,2050,1168,0,0,0),
(26,247,2047,3072,1168,20,0,0),
(83,252,2047,2057,128,0,0,0),
(74,253,-1,2056,1040,0,0,0),
(73,254,2047,2056,1168,10,0,0),
(108,255,2047,2049,1168,0,0,0),
(109,256,-1,2049,1040,0,0,0),
(110,257,-1,2049,1040,0,0,0),
(113,258,2047,2049,1168,10,0,0),
(38,260,2047,2052,128,0,0,0),
(35,262,2047,2052,128,0,0,0),
(36,263,2047,2052,128,0,0,0),
(37,264,2047,2052,128,0,0,0),
(61,267,-1,2050,1040,0,0,0),
(62,268,2047,2050,1170,0,0,0),
(64,269,2047,2050,1168,0,0,0),
(24,272,2047,3072,1168,10,0,0),
(34,273,2047,2052,128,0,0,0),
(21,293,2047,2051,128,40,0,0),
(906,293,262143,2080,128,0,0,0),
(46,313,64,3583,128,0,0,0),
(47,313,1983,3551,160,0,21,0),
(52,315,128,3583,128,0,0,0),
(53,315,1919,3551,160,0,21,0),
(281,333,2047,3583,160,0,62,0),
(104,353,2047,2304,1170,0,0,0),
(102,354,-1,2304,1040,0,0,0),
(101,355,-1,2304,1040,0,0,0),
(27,356,2047,3583,128,0,23,0),
(95,373,-1,2112,1040,0,0,0),
(98,374,262143,2112,1040,0,0,0),
(96,375,262143,2112,1040,0,0,0),
(30,393,2047,3583,160,0,161,0),
(145,413,2047,2116,128,40,0,0),
(146,413,2047,2083,128,0,0,0),
(147,414,2047,3183,128,0,0,0),
(148,415,2047,3583,128,0,0,0),
(151,416,2047,2049,192,0,0,0),
(155,416,2047,2050,192,0,0,1),
(158,416,2047,3136,192,0,0,1),
(159,416,2047,2060,192,0,0,1),
(271,416,2047,2448,192,0,0,2),
(175,418,2047,2049,384,0,0,0),
(178,418,2047,2050,384,0,0,0),
(183,418,2047,3332,384,0,0,1),
(186,418,2047,2192,384,0,0,1),
(270,418,2047,2120,384,0,0,1),
(189,419,2047,2060,640,0,0,2),
(191,419,2047,3072,640,0,0,1),
(193,419,2047,2192,640,0,0,0),
(198,419,2047,2050,640,0,0,1),
(200,419,2047,2049,640,0,0,2),
(265,419,2047,2304,640,0,0,0),
(266,419,2047,2112,640,0,0,1),
(203,420,2047,2061,1152,0,0,2),
(204,420,2047,3074,1152,0,0,1),
(205,420,2047,2320,1152,0,0,0),
(268,420,2047,2176,1152,0,0,0),
(269,420,2047,2112,1152,0,0,1),
(246,433,2047,2115,128,0,0,0),
(272,453,2047,2051,128,0,0,0),
(330,473,4095,3149,130,0,0,0),
(381,493,8,3583,164,0,0,0),
(403,515,2047,3551,128,0,0,0),
(445,533,128,3583,1170,0,181,0),
(446,533,95,3551,1170,0,182,0),
(461,553,64,3551,1170,0,181,0),
(501,553,4,3583,1170,0,182,0),
(463,554,16,3551,1170,0,181,0),
(464,554,207,3583,1170,0,182,0),
(521,573,-1,3072,1040,0,0,0),
(522,574,-1,3072,1040,0,0,0),
(541,593,-1,2304,1040,0,0,0),
(544,594,-1,2050,1040,0,0,0),
(581,613,-1,2064,1040,0,0,0),
(601,633,-1,2056,128,0,0,0),
(741,673,16,3583,128,0,0,0),
(742,673,2031,3551,160,0,21,0),
(781,713,255,3583,1170,0,181,0),
(841,733,128,3583,1170,0,0,0),
(861,753,64,3583,1170,0,0,0),
(862,754,1,3583,1170,0,0,0),
(866,755,2047,3583,160,0,41,0),
(867,756,512,3583,146,0,0,0),
(877,760,1024,3583,146,0,0,0),
(934,762,524287,2080,144,0,223,0),
(892,769,32767,3583,1040,0,0,0),
(896,770,-1,2080,1040,0,0,0),
(897,771,262143,2080,1040,0,0,0),
(951,771,2097151,3583,0,0,0,0),
(895,772,-1,2080,1040,0,0,0),
(900,773,262143,3583,160,0,41,0),
(936,776,262143,2080,128,0,0,0),
(938,777,524287,3583,2,0,0,0),
(939,778,524287,3583,2,0,0,0),
(947,778,2097151,3583,0,0,0,0);
@@ -0,0 +1,179 @@
-- mod-paragon: starter spawn data for class 12 (Paragon).
--
-- Companion to 2026_05_09_00.sql. The DBC overlay teaches the world
-- server that class 12 exists; this migration teaches it WHERE
-- characters of that class spawn, what action bar they boot with,
-- and what per-level base stats to integrity-check against.
--
-- Without these rows, character creation fails inside Player::Create:
--
-- PlayerInfo const* info = sObjectMgr->GetPlayerInfo(race, class);
-- if (!info) {
-- LOG_ERROR("entities.player",
-- "Player::Create: ... invalid race/class pair ({}/{})"
-- " - refusing to do so.", ..., race, class);
-- return false; // -> client sees "Error creating character"
-- }
--
-- and on world load the player_class_stats integrity check trips:
--
-- "Class N Level L does not have stats data!"
--
-- Tables touched:
-- - playercreateinfo : (race, class=12) -> map/zone/x/y/z
-- Race-specific starting zones (Paragon
-- spawns in each race's standard newbie
-- area, NOT Acherus, since it is a
-- from-level-1 class).
-- - playercreateinfo_action : (race, class=12, button) -> action,type
-- Default action bar layout per race.
-- - player_class_stats : (class=12, level 1..80) -> base stats
-- Per-level HP/Mana/STR/AGI/STA/INT/SPI
-- used by Player::InitStatsForLevel.
--
-- Tables intentionally NOT touched here:
-- - playercreateinfo_item : Paragon ships no per-class starting
-- items; gear comes from the racial
-- kit only.
-- - playercreateinfo_skills / _cast_spell / _spell_custom :
-- These are mask-based. Class-12 baseline
-- weapon/defense skills come through
-- classMask=0 ("all classes") rows that
-- already cover Paragon. The DBC overlay
-- in 2026_05_09_00.sql opens
-- SkillRaceClassInfo for class 12.
-- Idempotent: blow away any pre-existing class-12 rows first so this
-- migration can be replayed cleanly on a partially-seeded DB (e.g.
-- after a contributor manually patched their local DB before this
-- migration landed).
DELETE FROM `playercreateinfo` WHERE `class` = 12;
DELETE FROM `playercreateinfo_action` WHERE `class` = 12;
DELETE FROM `player_class_stats` WHERE `Class` = 12;
-- ---------------------------------------------------------------
-- playercreateinfo (10 rows: every DK-eligible race, racial start)
-- ---------------------------------------------------------------
INSERT INTO `playercreateinfo` (`race`, `class`, `map`, `zone`, `position_x`, `position_y`, `position_z`, `orientation`) VALUES
( 1, 12, 0, 12, -8949.95, -132.493, 83.5312, 0 ), -- Human -> Northshire, Elwynn Forest
( 2, 12, 1, 14, -618.518, -4251.67, 38.718, 0 ), -- Orc -> Valley of Trials, Durotar
( 3, 12, 0, 1, -6240.32, 331.033, 382.758, 6.17716 ), -- Dwarf -> Coldridge Valley, Dun Morogh
( 4, 12, 1, 141, 10311.3, 832.463, 1326.41, 5.69632 ), -- Night Elf -> Shadowglen, Teldrassil
( 5, 12, 0, 85, 1676.71, 1678.31, 121.67, 2.70526 ), -- Undead -> Deathknell, Tirisfal
( 6, 12, 1, 215, -2917.58, -257.98, 52.9968, 0 ), -- Tauren -> Camp Narache, Mulgore
( 7, 12, 0, 1, -6240.32, 331.033, 382.758, 0 ), -- Gnome -> Coldridge Valley (shared)
( 8, 12, 1, 14, -618.518, -4251.67, 38.718, 0 ), -- Troll -> Valley of Trials (shared)
(10, 12, 530, 3431, 10349.6, -6357.29, 33.4026, 5.31605 ), -- Blood Elf -> Sunstrider Isle, Eversong
(11, 12, 530, 3526, -3961.64,-13931.2, 100.615, 2.08364 ); -- Draenei -> Ammen Vale, Azuremyst Isle
-- ---------------------------------------------------------------
-- playercreateinfo_action (46 rows)
-- Buttons: 72=Attack(6603), 73=Eat(78), 74=racial, 75=race-extra,
-- 82=Skinning(59752, Tauren only), 84=Attack, 96=Attack
-- ---------------------------------------------------------------
INSERT INTO `playercreateinfo_action` (`race`, `class`, `button`, `action`, `type`) VALUES
( 1, 12, 72, 6603, 0), ( 1, 12, 73, 78, 0), ( 1, 12, 82, 59752, 0),
( 1, 12, 84, 6603, 0), ( 1, 12, 96, 6603, 0),
( 2, 12, 72, 6603, 0), ( 2, 12, 73, 78, 0), ( 2, 12, 74, 20572, 0),
( 2, 12, 84, 6603, 0), ( 2, 12, 96, 6603, 0),
( 3, 12, 72, 6603, 0), ( 3, 12, 73, 78, 0), ( 3, 12, 74, 20594, 0),
( 3, 12, 75, 2481, 0), ( 3, 12, 84, 6603, 0), ( 3, 12, 96, 6603, 0),
( 4, 12, 72, 6603, 0), ( 4, 12, 73, 78, 0), ( 4, 12, 74, 58984, 0),
( 4, 12, 84, 6603, 0), ( 4, 12, 96, 6603, 0),
( 5, 12, 72, 6603, 0), ( 5, 12, 73, 78, 0), ( 5, 12, 74, 20577, 0),
( 5, 12, 84, 6603, 0), ( 5, 12, 96, 6603, 0),
( 6, 12, 72, 6603, 0), ( 6, 12, 73, 78, 0), ( 6, 12, 74, 20549, 0),
( 6, 12, 84, 6603, 0), ( 6, 12, 96, 6603, 0),
( 7, 12, 72, 6603, 0), ( 7, 12, 73, 78, 0), ( 7, 12, 84, 6603, 0),
( 7, 12, 96, 6603, 0),
( 8, 12, 72, 6603, 0), ( 8, 12, 73, 78, 0), ( 8, 12, 74, 2764, 0),
( 8, 12, 75, 26297, 0), ( 8, 12, 84, 6603, 0), ( 8, 12, 96, 6603, 0),
(11, 12, 72, 6603, 0), (11, 12, 73, 78, 0), (11, 12, 74, 28880, 0),
(11, 12, 84, 6603, 0), (11, 12, 96, 6603, 0);
-- ---------------------------------------------------------------
-- player_class_stats (80 rows: levels 1..80 per-class base stats)
-- Curve mirrors Warrior baseline -> Paladin past 60 (vehicle-style HP
-- inflation past 60 to keep Paragon competitive in Wrath content).
-- ---------------------------------------------------------------
INSERT INTO `player_class_stats` (`Class`, `Level`, `BaseHP`, `BaseMana`, `Strength`, `Agility`, `Stamina`, `Intellect`, `Spirit`) VALUES
(12, 1, 20, 60, 23, 20, 22, 20, 20),
(12, 2, 29, 66, 24, 21, 23, 20, 20),
(12, 3, 38, 73, 25, 21, 24, 20, 21),
(12, 4, 47, 81, 26, 22, 25, 20, 21),
(12, 5, 56, 90, 28, 23, 26, 20, 21),
(12, 6, 65, 100, 29, 24, 27, 21, 21),
(12, 7, 74, 111, 30, 24, 28, 21, 22),
(12, 8, 83, 123, 31, 25, 29, 21, 22),
(12, 9, 92, 136, 32, 26, 30, 21, 22),
(12, 10, 97, 150, 33, 26, 31, 21, 23),
(12, 11, 103, 165, 35, 27, 33, 21, 23),
(12, 12, 109, 182, 36, 28, 34, 21, 23),
(12, 13, 118, 200, 37, 29, 35, 21, 24),
(12, 14, 128, 219, 39, 30, 36, 22, 24),
(12, 15, 139, 239, 40, 30, 37, 22, 24),
(12, 16, 151, 260, 41, 31, 38, 22, 25),
(12, 17, 154, 282, 42, 32, 40, 22, 25),
(12, 18, 168, 305, 44, 33, 41, 22, 25),
(12, 19, 183, 329, 45, 34, 42, 22, 26),
(12, 20, 199, 354, 47, 35, 43, 22, 26),
(12, 21, 206, 380, 48, 35, 45, 23, 26),
(12, 22, 224, 392, 49, 36, 46, 23, 27),
(12, 23, 243, 420, 51, 37, 47, 23, 27),
(12, 24, 253, 449, 52, 38, 49, 23, 28),
(12, 25, 274, 479, 54, 39, 50, 23, 28),
(12, 26, 296, 509, 55, 40, 51, 23, 28),
(12, 27, 309, 524, 57, 41, 53, 23, 29),
(12, 28, 333, 554, 58, 42, 54, 24, 29),
(12, 29, 348, 584, 60, 43, 56, 24, 30),
(12, 30, 374, 614, 62, 44, 57, 24, 30),
(12, 31, 401, 629, 63, 45, 58, 24, 30),
(12, 32, 419, 659, 65, 46, 60, 24, 31),
(12, 33, 448, 689, 66, 47, 61, 24, 31),
(12, 34, 468, 704, 68, 48, 63, 25, 32),
(12, 35, 499, 734, 70, 49, 64, 25, 32),
(12, 36, 521, 749, 72, 50, 66, 25, 33),
(12, 37, 545, 779, 73, 51, 68, 25, 33),
(12, 38, 581, 809, 75, 52, 69, 25, 33),
(12, 39, 609, 824, 77, 53, 71, 26, 34),
(12, 40, 649, 854, 79, 54, 72, 26, 34),
(12, 41, 681, 869, 80, 56, 74, 26, 35),
(12, 42, 715, 899, 82, 57, 76, 26, 35),
(12, 43, 761, 914, 84, 58, 77, 26, 36),
(12, 44, 799, 944, 86, 59, 79, 26, 36),
(12, 45, 839, 959, 88, 60, 81, 27, 37),
(12, 46, 881, 989, 90, 61, 83, 27, 37),
(12, 47, 935, 1004, 92, 63, 84, 27, 38),
(12, 48, 981, 1019, 94, 64, 86, 27, 38),
(12, 49, 1029, 1049, 96, 65, 88, 28, 39),
(12, 50, 1079, 1064, 98, 66, 90, 28, 39),
(12, 51, 1131, 1079, 100, 68, 92, 28, 40),
(12, 52, 1185, 1109, 102, 69, 94, 28, 40),
(12, 53, 1241, 1124, 104, 70, 96, 28, 41),
(12, 54, 1299, 1139, 106, 72, 98, 29, 42),
(12, 55, 1359, 1154, 109, 73, 100, 29, 42),
(12, 56, 1421, 1169, 111, 74, 102, 29, 43),
(12, 57, 1485, 1199, 113, 76, 104, 29, 43),
(12, 58, 1551, 1214, 115, 77, 106, 30, 44),
(12, 59, 1619, 1229, 118, 79, 108, 30, 44),
(12, 60, 1689, 1244, 120, 80, 110, 30, 45),
(12, 61, 1902, 1357, 122, 81, 112, 30, 46),
(12, 62, 2129, 1469, 125, 83, 114, 30, 46),
(12, 63, 2357, 1582, 127, 84, 117, 31, 47),
(12, 64, 2612, 1694, 130, 86, 119, 31, 47),
(12, 65, 2883, 1807, 132, 88, 121, 31, 48),
(12, 66, 3169, 1919, 135, 89, 123, 32, 49),
(12, 67, 3455, 2032, 137, 91, 126, 32, 49),
(12, 68, 3774, 2145, 140, 92, 128, 32, 50),
(12, 69, 4109, 2257, 142, 94, 130, 32, 51),
(12, 70, 4444, 2370, 145, 96, 133, 33, 51),
(12, 71, 4720, 2482, 148, 97, 135, 33, 52),
(12, 72, 5013, 2595, 150, 99, 138, 33, 53),
(12, 73, 5325, 2708, 153, 101, 140, 33, 54),
(12, 74, 5656, 2820, 156, 102, 143, 34, 54),
(12, 75, 6008, 2933, 159, 104, 145, 34, 55),
(12, 76, 6381, 3045, 162, 106, 148, 34, 56),
(12, 77, 6778, 3158, 165, 108, 151, 35, 57),
(12, 78, 7198, 3270, 168, 109, 153, 35, 57),
(12, 79, 7646, 3383, 171, 111, 156, 35, 58),
(12, 80, 8121, 3496, 174, 113, 159, 36, 59);
@@ -0,0 +1,50 @@
-- mod-paragon: starter weapon / armor skills for class 12 (Paragon).
--
-- Companion to 2026_05_10_00.sql. The spawn-data migration teaches
-- Player::Create *that* class 12 exists at a given race; this one
-- teaches it which weapon and armor skill lines to grant on first
-- character login.
--
-- Without these rows a fresh Paragon character lands in their newbie
-- zone with **no** weapon or armor proficiencies (auto-attack greys
-- out the moment they equip anything beyond a fist). The classMask=0
-- "all classes" rows in playercreateinfo_skills only cover Defense,
-- Unarmed, Cloth, the racial / language skills, Mounts and
-- Companion Pets -- which is exactly what bare-fisted, naked
-- characters look like.
--
-- Paragon plays every class, so it grants every weapon / armor
-- proficiency at level 1. The skillline rows themselves are still
-- gated by skillraceclassinfo_dbc (handled in 2026_05_09_00.sql),
-- so the client/server agree on what's allowed.
--
-- Idempotent: deletes any pre-existing classMask=2048 rows first
-- (class 12 owns this bitmask on Fractured) so the migration can
-- replay cleanly on a partially-seeded DB.
DELETE FROM `playercreateinfo_skills` WHERE `classMask` = 2048;
INSERT INTO `playercreateinfo_skills`
(`raceMask`, `classMask`, `skill`, `rank`, `comment`) VALUES
-- Weapon proficiencies
(0, 2048, 43, 0, 'Paragon - Swords'),
(0, 2048, 44, 0, 'Paragon - Axes'),
(0, 2048, 45, 0, 'Paragon - Bows'),
(0, 2048, 46, 0, 'Paragon - Guns'),
(0, 2048, 54, 0, 'Paragon - Maces'),
(0, 2048, 55, 0, 'Paragon - Two-Handed Swords'),
(0, 2048, 118, 0, 'Paragon - Dual Wield'),
(0, 2048, 136, 0, 'Paragon - Staves'),
(0, 2048, 160, 0, 'Paragon - Two-Handed Maces'),
(0, 2048, 172, 0, 'Paragon - Two-Handed Axes'),
(0, 2048, 173, 0, 'Paragon - Daggers'),
(0, 2048, 176, 0, 'Paragon - Thrown'),
(0, 2048, 226, 0, 'Paragon - Crossbows'),
(0, 2048, 228, 0, 'Paragon - Wands'),
(0, 2048, 229, 0, 'Paragon - Polearms'),
(0, 2048, 473, 0, 'Paragon - Fist Weapons'),
-- Armor proficiencies (Cloth is in a classMask=0 row already)
(0, 2048, 293, 0, 'Paragon - Plate Mail'),
(0, 2048, 413, 0, 'Paragon - Mail'),
(0, 2048, 414, 0, 'Paragon - Leather'),
(0, 2048, 433, 0, 'Paragon - Shield');
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
-- mod-paragon: Blood Elf "Arcane Torrent" uses three spell IDs in WotLK
-- (28730 mana/casters, 25046 rogue energy, 50613 death knight runic power),
-- all on racial skill line 756. Migration 2026_05_10_02 OR'd class 12 into
-- every SkillLineAbility delta from patch-enUS-4, so Paragon Blood Elves
-- auto-learned all three and the spellbook showed three identical entries.
--
-- Paragon should learn a single combined Arcane Torrent that refunds mana,
-- energy, AND runic power -- whichever pool the character is using at the
-- moment. We keep spell 28730 as the in-book entry for class 12 and attach
-- the SpellScript spell_paragon_arcane_torrent (modules/mod-paragon/src/
-- Paragon_SC.cpp) so casts by a Paragon also EnergizeBySpell energy + RP on
-- top of the stock mana effect. Other classes' Blood Elves are unaffected.
--
-- IDs 13338 / 17510 match stock WotLK SkillLineAbility rows for spells 25046
-- / 50613 on skill line 756.
UPDATE `skilllineability_dbc`
SET `ClassMask` = `ClassMask` & ~2048
WHERE `ID` IN (13338, 17510);
-- Bind spell_paragon_arcane_torrent (defined in Paragon_SC.cpp) to spell
-- 28730. AC's `spell_script_names` is the standard mapping: script name on
-- the right, spell id on the left. Idempotent via DELETE + INSERT.
DELETE FROM `spell_script_names`
WHERE `spell_id` = 28730 AND `ScriptName` = 'spell_paragon_arcane_torrent';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(28730, 'spell_paragon_arcane_torrent');
@@ -0,0 +1,30 @@
-- mod-paragon: extend ItemTemplate::AllowableClass to include class 12
-- (Paragon, bit 1<<11 = 2048) for every class-restricted item.
--
-- Server-side, Player::CanUseItem (PlayerStorage.cpp) already short-
-- circuits the AllowableClass check for class 12. That's enough for any
-- code path the server controls (vendor list filter, AH "usable" filter,
-- CanRollForItemInLFG, CanBuyItem). It is NOT enough on the 3.3.5 client:
-- the WoW.exe binary independently pre-checks AllowableClass against the
-- player's class on right-click of a bag item and refuses *locally* with
-- the red "You can't use that item." text in UIErrorsFrame, never sending
-- CMSG_USE_ITEM at all. Server logs stay silent; only client knows it
-- refused.
--
-- Fix: OR class 12's bit into AllowableClass on every class-restricted
-- row so the client engine's pre-check passes for Paragon. Other
-- classes' bits are unchanged, so e.g. a warrior-only item is still
-- warrior-only for everyone except Paragon. Items with AllowableClass
-- == -1 ("all classes") or 0 ("no restriction recorded") already pass
-- the client engine's check and are not touched.
--
-- After applying this migration the *client* still caches item info in
-- Cache/<locale>/itemcache.wdb. Players who already inspected the item
-- before the change must delete that file (or the whole Cache folder)
-- and reconnect to repopulate it from the worldserver, otherwise the
-- stale cached AllowableClass keeps the engine pre-check failing.
UPDATE `item_template`
SET `AllowableClass` = `AllowableClass` | 2048
WHERE `AllowableClass` > 0
AND (`AllowableClass` & 2048) = 0;
+177 -21
View File
@@ -294,6 +294,98 @@ void DbInsertPanelSpellRevoked(uint32 lowGuid, uint32 parentSpellId, uint32 revo
lowGuid, parentSpellId, revokedSpellId);
}
// Drop any panel_spell_revoked rows whose `revoked_spell_id` falls inside
// `chainIds`. Called from `PanelLearnSpellChain` right after the panel
// purchase row is committed so a freshly bought spell can't be unlearned
// on the next login by a stale (pre-purchase) revoke entry. Without this,
// the very first login after the purchase would walk the revoke table,
// hit the ghost row, `removeSpell` the freshly-paid-for ability, and
// then `PushSpellSnapshot` (which deletes panel_spells rows whose spell
// the player no longer has) would erase the purchase from the panel
// record entirely -- losing both the spell and the AE refund hook.
void DbDeletePanelSpellRevokedForChain(uint32 lowGuid,
std::unordered_set<uint32> const& chainIds)
{
if (chainIds.empty())
return;
std::string in;
in.reserve(chainIds.size() * 8);
bool first = true;
for (uint32 sid : chainIds)
{
if (!first)
in += ",";
in += std::to_string(sid);
first = false;
}
CharacterDatabase.DirectExecute(
"DELETE FROM character_paragon_panel_spell_revoked "
"WHERE guid = {} AND revoked_spell_id IN ({})",
lowGuid, in);
}
// Build the allowlist of spell IDs the player legitimately owns through
// Character Advancement: every chain rank of every spell in panel_spells,
// every rank-spell-id up to the purchased rank of every talent in
// panel_talents, and every recorded panel_spell_children id. Both
// `RevokeUnwantedCascadeSpellsForPlayer` and `RevokeBlockedSpellsForPlayer`
// need exactly this set; without the talent contribution, buying a spell
// after a talent (e.g., DK Death Coil after Scourge Strike) caused the
// post-commit sweep to revoke the talent-granted spell because it didn't
// appear in panel_spells. See ParagonAdvancement_TalentData.lua: many
// "abilities" the player perceives as spells (Scourge Strike id=2216,
// Bladestorm, Starfall, ...) are panel TALENTS that grant a spell rank
// via Player::LearnTalent.
void BuildPanelOwnedSpellsAllowlist(uint32 lowGuid, std::unordered_set<uint32>& allowed)
{
if (!lowGuid)
return;
if (QueryResult r = CharacterDatabase.Query(
"SELECT spell_id FROM character_paragon_panel_spells WHERE guid = {}",
lowGuid))
{
do
{
CollectSpellChainIds(r->Fetch()[0].Get<uint32>(), allowed);
} while (r->NextRow());
}
if (QueryResult r = CharacterDatabase.Query(
"SELECT talent_id, `rank` FROM character_paragon_panel_talents WHERE guid = {}",
lowGuid))
{
do
{
Field const* f = r->Fetch();
uint32 const tid = f[0].Get<uint32>();
uint32 const rank = f[1].Get<uint32>();
TalentEntry const* te = sTalentStore.LookupEntry(tid);
if (!te || !rank)
continue;
// panel_talents.rank is 1-based ("1 means rank-1 owned"). Allow
// every rank id from 0..rank-1 so a partial-rank purchase still
// protects all lower ranks the player rolled through.
uint32 const cap = std::min<uint32>(rank, MAX_TALENT_RANK);
for (uint32 i = 0; i < cap; ++i)
if (te->RankID[i])
allowed.insert(te->RankID[i]);
} while (r->NextRow());
}
if (QueryResult r = CharacterDatabase.Query(
"SELECT child_spell_id FROM character_paragon_panel_spell_children WHERE guid = {}",
lowGuid))
{
do
{
allowed.insert(r->Fetch()[0].Get<uint32>());
} while (r->NextRow());
}
}
// Walk every (guid, *, revoked_spell_id) row and `removeSpell` it if the
// player still has it. Call sites:
// * `OnPlayerLogin` -- because `_LoadSkills` -> `learnSkillRewardedSpells`
@@ -301,12 +393,30 @@ void DbInsertPanelSpellRevoked(uint32 lowGuid, uint32 parentSpellId, uint32 revo
// Death Coil / Death Grip / etc. before any of our hooks fired.
// * `HandleParagonResetAbilities` is NOT a caller; reset clears the
// table outright so the revoke list starts fresh on next purchase.
//
// Allowlist-aware: a revoked row whose `revoked_spell_id` is now part of
// a panel_spells chain (or recorded as a passive child) is *stale* -- it
// was inserted before the player legitimately purchased that spell, so
// re-running `removeSpell` on it would zap a paid-for ability. Such rows
// are skipped and dropped from the table so they can't fire again. This
// is the self-heal path for the pre-fix bug where buying a spell that
// had previously been caught by the login sweep left the (0, sid) ghost
// row in place; on every subsequent login that ghost would unlearn the
// freshly bought spell, and `PushSpellSnapshot`'s !HasSpell branch would
// then delete the panel_spells row, vanishing the purchase entirely.
void RevokeBlockedSpellsForPlayer(Player* pl)
{
if (!pl)
return;
uint32 const lowGuid = pl->GetGUID().GetCounter();
// Allowlist: chain ranks of panel_spells + rank IDs of panel_talents
// + panel_spell_children. Talents matter here because many Wrath
// "abilities" (Scourge Strike, Bladestorm, ...) are talent-granted.
std::unordered_set<uint32> allowed;
BuildPanelOwnedSpellsAllowlist(lowGuid, allowed);
QueryResult r = CharacterDatabase.Query(
"SELECT revoked_spell_id FROM character_paragon_panel_spell_revoked WHERE guid = {}",
lowGuid);
@@ -314,9 +424,15 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
return;
uint32 removed = 0;
std::vector<uint32> stale; // allowlisted -> drop the row
do
{
uint32 const sid = r->Fetch()[0].Get<uint32>();
if (allowed.count(sid))
{
stale.push_back(sid);
continue;
}
if (pl->HasSpell(sid))
{
pl->removeSpell(sid, SPEC_MASK_ALL, false);
@@ -324,6 +440,34 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
}
} while (r->NextRow());
if (!stale.empty())
{
// Build IN-list. `stale` is bounded by the player's revoked rows.
std::sort(stale.begin(), stale.end());
stale.erase(std::unique(stale.begin(), stale.end()), stale.end());
std::string in;
in.reserve(stale.size() * 8);
bool first = true;
for (uint32 sid : stale)
{
if (!first)
in += ",";
in += std::to_string(sid);
first = false;
}
CharacterDatabase.DirectExecute(
"DELETE FROM character_paragon_panel_spell_revoked "
"WHERE guid = {} AND revoked_spell_id IN ({})",
lowGuid, in);
LOG_INFO("module",
"Paragon panel: dropped {} stale revoke rows for {} "
"(spell now owned via panel purchase)",
stale.size(), pl->GetName());
}
if (removed)
LOG_INFO("module",
"Paragon panel: re-revoked {} skill-cascade dependents for {} on login",
@@ -459,28 +603,13 @@ void RevokeUnwantedCascadeSpellsForPlayer(Player* pl)
PruneSkillLineCascadeChildrenFromDb(pl, lowGuid);
// Build the allowlist: every chain rank of every panel-purchased spell,
// plus every recorded passive child.
// Allowlist: chain ranks of panel_spells + rank IDs of panel_talents
// + panel_spell_children. Talents matter here because many Wrath
// "abilities" (Scourge Strike, Bladestorm, ...) are talent-granted:
// a Death Coil purchase otherwise activates the DK skill line and
// sweeps Scourge Strike (55090) out from under the talent.
std::unordered_set<uint32> allowed;
if (QueryResult r = CharacterDatabase.Query(
"SELECT spell_id FROM character_paragon_panel_spells WHERE guid = {}",
lowGuid))
{
do
{
uint32 const base = r->Fetch()[0].Get<uint32>();
CollectSpellChainIds(base, allowed);
} while (r->NextRow());
}
if (QueryResult r = CharacterDatabase.Query(
"SELECT child_spell_id FROM character_paragon_panel_spell_children WHERE guid = {}",
lowGuid))
{
do
{
allowed.insert(r->Fetch()[0].Get<uint32>());
} while (r->NextRow());
}
BuildPanelOwnedSpellsAllowlist(lowGuid, allowed);
if (allowed.empty())
return;
@@ -559,6 +688,23 @@ void RevokeUnwantedCascadeSpellsForPlayer(Player* pl)
toRevoke.size(), pl->GetName(), ourSkillLines.size(), allowed.size());
}
// Blood Elf racial "Arcane Torrent" is three different spell IDs in WotLK
// (28730 mana, 25046 rogue energy, 50613 DK runic power), all on skill line
// 756. The blanket SkillLineAbility overlay opened all three to class 12;
// Paragon should keep only the mana version (matches primary power display).
void RevokeDuplicateBloodElfArcaneTorrent(Player* pl)
{
if (!pl || pl->getRace() != RACE_BLOODELF)
return;
constexpr uint32 SPELL_ARCANE_TORRENT_ROGUE = 25046;
constexpr uint32 SPELL_ARCANE_TORRENT_DK = 50613;
for (uint32 sid : { SPELL_ARCANE_TORRENT_ROGUE, SPELL_ARCANE_TORRENT_DK })
if (pl->HasSpell(sid))
pl->removeSpell(sid, SPEC_MASK_ALL, false);
}
// Snapshot of currently-known spell IDs (excluding entries marked removed).
// Used by PanelLearnSpellChain to detect spells that AzerothCore's
// addSpell machinery auto-learns alongside the spell we asked for.
@@ -751,6 +897,13 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
DbInsertPanelSpell(lowGuid, trackId);
// Clear any stale revoke rows that targeted a rank in this chain. A
// prior login sweep (before the purchase) or an earlier commit-time
// diff (e.g., this chain was revoked as a cascade dependent of a
// *different* purchase the user has since reset/refunded) may have
// left rows that would otherwise re-fire `removeSpell` next login.
DbDeletePanelSpellRevokedForChain(lowGuid, chainIds);
if (diag)
LOG_INFO("module",
"[paragon-diag] PanelLearnSpellChain end: trackId={}", trackId);
@@ -1441,6 +1594,9 @@ public:
// cascade re-fire on relog. Only walks our own SkillLines, so
// racials / weapon skills / Defense rewards are never touched.
RevokeUnwantedCascadeSpellsForPlayer(player);
// Step 3: Blood Elf only -- strip rogue/DK Arcane Torrent clones
// (skill-line overlay taught all three; see 2026_05_10_03.sql).
RevokeDuplicateBloodElfArcaneTorrent(player);
// Intentionally NOT calling SendSilenceClose here -- the chat
// frame buffers system messages during the loading screen and
+87 -3
View File
@@ -13,7 +13,8 @@
#include "Player.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include "UnitDefines.h"
#include "SpellScript.h"
#include "SpellScriptLoader.h"
#include "WorldPacket.h"
#include "WorldSession.h"
@@ -35,7 +36,7 @@ public:
{
LOG_INFO("module", "[paragon] Paragon_PlayerScript registered "
"(MultiResource.HasActivePowers={})",
sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", false));
sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", true));
}
[[nodiscard]] Optional<bool> OnPlayerIsClass(Player const* player, Classes unitClass, ClassContext context) override
@@ -64,6 +65,37 @@ public:
return true;
}
// Unified relic / ranged slot for class 12.
// ----------------------------------------------------------------
// CLASS_CONTEXT_EQUIP_RELIC is read in exactly two places in core
// (PlayerStorage.cpp): FindEquipSlot's INVTYPE_RELIC switch, which
// routes Librams/Idols/Totems/Misc/Sigils into EQUIPMENT_SLOT_RANGED
// for the matching class only, and CanEquipUniqueItem's per-subclass
// proficiency gate. By claiming this context for paladin/druid/
// shaman/warlock/dk we let Paragon drop any of those relics into the
// ranged slot exactly the same way each native class does, with no
// core patch and no other side effects (the constant is not read
// anywhere else in the codebase).
//
// Bows/guns/crossbows already equip via the regular
// INVTYPE_RANGED/RANGEDRIGHT routing -- weapon proficiencies for
// class 12 are seeded by the Paragon proficiency SQL migrations, so
// they pass the GetSkillValue check in CanEquipUniqueItem.
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;
}
}
return std::nullopt;
}
@@ -75,7 +107,7 @@ public:
if (power == POWER_RUNIC_POWER || power == POWER_RUNE)
return true;
if (sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", false))
if (sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", true))
{
switch (power)
{
@@ -268,7 +300,59 @@ private:
std::unordered_map<ObjectGuid, Paragon_PlayerScript::ParagonRuneSyncState> Paragon_PlayerScript::runeSyncByGuid;
// Arcane Torrent (28730) for Paragon: Blood Elf racial skill line 756 has
// three Arcane Torrent variants in stock WotLK (28730 mana, 25046 rogue
// energy, 50613 DK runic power). For Paragon Blood Elves we keep only 28730
// (see migration 2026_05_10_03.sql) and turn it into a "combined" version:
// the stock spell already silences nearby enemies and energizes mana via its
// own effects; this script adds energy, rage, and runic power energize on
// top when the caster is class 12, so a single button refunds whichever
// resource pool the player is actually using. Non-Paragon casters are
// untouched and keep learning their stock racial variant.
class spell_paragon_arcane_torrent : public SpellScript
{
PrepareSpellScript(spell_paragon_arcane_torrent);
void HandleAfterCast()
{
Unit* caster = GetCaster();
if (!caster || !caster->IsPlayer())
return;
Player* player = caster->ToPlayer();
if (player->getClass() != CLASS_PARAGON)
return;
// Stock energize amounts from spell_dbc:
// 25046 Arcane Torrent (Energy) -> 15 energy
// 50613 Arcane Torrent (Runic Power) -> 15 displayed RP (= 150
// internal; AC stores RP scaled 10x, see Player::SetMaxPower
// POWER_RUNIC_POWER, 1000).
// Rage uses the same 10x internal scaling as runic power (see
// Player.cpp:Regenerate where rage decay is `-20` for "2 rage by
// tick"), so 15 displayed rage = 150 internal.
// ModifyPower no-ops on pools the player has no max for, so this is
// safe even before the Paragon picks up energy/rage/RP abilities.
constexpr int32 kEnergyGain = 15;
constexpr int32 kRageGain = 150;
constexpr int32 kRunicPowerGain = 150;
SpellInfo const* spellInfo = GetSpellInfo();
uint32 const spellId = spellInfo ? spellInfo->Id : 28730u;
caster->EnergizeBySpell(player, spellId, kEnergyGain, POWER_ENERGY);
caster->EnergizeBySpell(player, spellId, kRageGain, POWER_RAGE);
caster->EnergizeBySpell(player, spellId, kRunicPowerGain, POWER_RUNIC_POWER);
}
void Register() override
{
AfterCast += SpellCastFn(spell_paragon_arcane_torrent::HandleAfterCast);
}
};
void AddSC_paragon()
{
new Paragon_PlayerScript();
RegisterSpellScript(spell_paragon_arcane_torrent);
}
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Clone Dawnforger/Fractured and omit Docker-only paths. Use when this script is
# already on disk (e.g. scp). Otherwise: git clone … && cd Fractured && bash scripts/vps-sparse-checkout-no-docker.sh
#
# Usage:
# bash scripts/vps-clone-without-docker.sh /path/to/Fractured git@github.com:Dawnforger/Fractured.git
set -euo pipefail
TARGET="${1:?usage: $0 /path/to/Fractured <git-remote-url>}"
REMOTE="${2:?usage: $0 /path/to/Fractured <git-remote-url>}"
if [[ -e "$TARGET" ]]; then
echo "error: $TARGET already exists; remove it or pick another path." >&2
exit 1
fi
mkdir -p "$(dirname "$TARGET")"
git clone "$REMOTE" "$TARGET"
cd "$TARGET"
if [[ ! -f scripts/vps-sparse-checkout-no-docker.sh ]]; then
echo "error: clone missing scripts/vps-sparse-checkout-no-docker.sh — pull latest main." >&2
exit 1
fi
bash scripts/vps-sparse-checkout-no-docker.sh
echo "Done. Next: docs/DEPLOY_LINUX_VPS.md"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Omit Docker-only paths from the working tree (native VPS / production clones).
# Repository root is the AzerothCore tree (flat layout).
#
# Run from repository root (directory that contains acore.sh and apps/).
#
# Usage:
# bash scripts/vps-sparse-checkout-no-docker.sh
#
# Restore full tree: git sparse-checkout disable
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if [[ ! -d .git ]]; then
echo "error: run from a git clone (no .git in $ROOT)." >&2
exit 1
fi
git sparse-checkout init --no-cone
cat >.git/info/sparse-checkout <<'EOF'
/*
!/docker-compose.yml
!/docker-compose.override.yml
!/apps/docker/
!/env/docker-focal-build/
!/.devcontainer/
EOF
if git sparse-checkout reapply 2>/dev/null; then
:
else
git read-tree -mu HEAD
fi
echo "Sparse checkout applied (Docker-only paths omitted)."
echo "To restore full tree locally: git sparse-checkout disable"
@@ -53,12 +53,14 @@ MaxPingTime = 30
#
# RealmServerPort
# Description: TCP port the auth server listens on (login handshake).
# Fractured production: match your client realmlist host:port, e.g.
# set realmlist hsrwow.net:47497
# requires RealmServerPort = 47497 and firewall/NAT to this process.
# Default: 3724 (stock WoW); Fractured dist default: 47497
# 3724 is the stock WoW default; clients with `set realmlist <host>`
# (no port) connect here. Production deployments that cannot bind
# 3724 (NAT, conflicting service, etc.) can set this to e.g. 47497
# and have clients use `set realmlist <host>:47497` -- the
# Fractured-patched Wow.exe supports the host:port syntax.
# Default: 3724
RealmServerPort = 47497
RealmServerPort = 3724
#
#
@@ -669,7 +669,12 @@ bool AuctionHouseUsablePlayerInfo::PlayerCanUseItem(ItemTemplate const* proto) c
return false;
}
if ((proto->AllowableClass & classMask) == 0 || (proto->AllowableRace & raceMask) == 0)
// mod-paragon: class 12 (Paragon) ignores AllowableClass for AH "Usable"
// filter. classMask here is the searching player's mask; PARAGON_BIT 0x800
// = (1 << (12 - 1)). Race restriction still applies.
bool const searcherIsParagon = (classMask & 0x800u) != 0;
if ((!searcherIsParagon && (proto->AllowableClass & classMask) == 0)
|| (proto->AllowableRace & raceMask) == 0)
return false;
if (proto->RequiredSkill != 0)
+12 -2
View File
@@ -10687,7 +10687,12 @@ bool Player::BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uin
return false;
}
if (!(pProto->AllowableClass & getClassMask()) && pProto->Bonding == BIND_WHEN_PICKED_UP && !IsGameMaster())
// mod-paragon: class 12 ignores BoP buy-side AllowableClass gate, so
// class-restricted vendor items (e.g. class glyphs) can be purchased.
if (getClass() != CLASS_PARAGON
&& !(pProto->AllowableClass & getClassMask())
&& pProto->Bonding == BIND_WHEN_PICKED_UP
&& !IsGameMaster())
{
SendBuyError(BUY_ERR_CANT_FIND_ITEM, nullptr, item, 0);
return false;
@@ -14084,7 +14089,12 @@ void Player::LearnTalent(uint32 talentId, uint32 talentRank, bool command /*= fa
}
// xinef: check if talent deponds on another talent
if (talentInfo->DependsOn > 0)
// mod-paragon: Character Advancement gates talents by AE/TE essence cost,
// not by the column-arrow prereq from Blizzard's spec UI. For class 12
// (Paragon) we skip the DependsOn check so e.g. Deep Wounds, Bloody
// Vengeance and Expose Weakness can be picked without first speccing into
// their unrelated prereq sibling.
if (talentInfo->DependsOn > 0 && getClass() != CLASS_PARAGON)
if (TalentEntry const* depTalentInfo = sTalentStore.LookupEntry(talentInfo->DependsOn))
{
bool hasEnoughRank = false;
@@ -2364,7 +2364,16 @@ InventoryResult Player::CanUseItem(ItemTemplate const* proto) const
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
}
if ((proto->AllowableClass & getClassMask()) == 0 || (proto->AllowableRace & getRaceMask()) == 0)
// mod-paragon: class 12 (Paragon) ignores AllowableClass entirely, so any
// class-restricted item (including class glyphs) can be equipped/used.
// Race restriction still applies; proficiency/level/skill checks below
// still gate it sensibly via the standard skill cascade.
if (getClass() != CLASS_PARAGON
&& (proto->AllowableClass & getClassMask()) == 0)
{
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
}
if ((proto->AllowableRace & getRaceMask()) == 0)
{
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
}
@@ -2430,7 +2439,11 @@ InventoryResult Player::CanRollForItemInLFG(ItemTemplate const* proto, WorldObje
SKILL_FISHING
}; //Copy from function Item::GetSkill()
if ((proto->AllowableClass & getClassMask()) == 0 || (proto->AllowableRace & getRaceMask()) == 0)
// mod-paragon: class 12 ignores AllowableClass for LFG roll eligibility.
if (getClass() != CLASS_PARAGON
&& (proto->AllowableClass & getClassMask()) == 0)
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
if ((proto->AllowableRace & getRaceMask()) == 0)
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
if (proto->RequiredSpell != 0 && !HasSpell(proto->RequiredSpell))
@@ -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;
+29
View File
@@ -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)
+6 -1
View File
@@ -908,7 +908,12 @@ void WorldSession::SendListInventory(ObjectGuid vendorGuid, uint32 vendorEntry)
{
if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item->item))
{
if (!(itemTemplate->AllowableClass & _player->getClassMask()) && itemTemplate->Bonding == BIND_WHEN_PICKED_UP && !_player->IsGameMaster())
// mod-paragon: class 12 sees every BoP class-restricted item
// in vendor lists (class glyphs, class tier sets, ...).
if (_player->getClass() != CLASS_PARAGON
&& !(itemTemplate->AllowableClass & _player->getClassMask())
&& itemTemplate->Bonding == BIND_WHEN_PICKED_UP
&& !_player->IsGameMaster())
{
continue;
}
+10 -2
View File
@@ -7296,8 +7296,16 @@ SpellCastResult Spell::CheckItems(uint32* param1, uint32* param2)
{
// Xinef: this is not true in my opinion, in eg bladestorm will not be canceled after disarm
//if (!HasTriggeredCastFlag(TRIGGERED_IGNORE_EQUIPPED_ITEM_REQUIREMENT))
if (m_caster->IsPlayer() && !m_caster->ToPlayer()->HasItemFitToSpellRequirements(m_spellInfo))
return SPELL_FAILED_EQUIPPED_ITEM_CLASS;
if (m_caster->IsPlayer())
{
// Cast-from-glyph: many glyph on-use spells set EquippedItemClass to ITEM_CLASS_GLYPH.
// HasItemFitToSpellRequirements only implements weapon/armor, so it would always fail here
// even though the glyph item in the bag is the valid spell source.
bool const castFromGlyphScroll = m_CastItem && m_CastItem->GetTemplate() &&
m_CastItem->GetTemplate()->Class == ITEM_CLASS_GLYPH;
if (!castFromGlyphScroll && !m_caster->ToPlayer()->HasItemFitToSpellRequirements(m_spellInfo))
return SPELL_FAILED_EQUIPPED_ITEM_CLASS;
}
}
// do not take reagents for these item casts
@@ -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", " ");
}
+8
View File
@@ -215,6 +215,14 @@ Updates.AllowRehash = 1
# -1 - (Enabled - unlimited)
Updates.CleanDeadRefMaxCount = 3
#
# Updates.ExceptionShutdownDelay
# Description: Time (in milliseconds) to wait before shutting down after a fatal exception (e.g. failed SQL update).
# Default: 10000 - 10 seconds
# 0 - Disabled (immediate shutdown)
Updates.ExceptionShutdownDelay = 10000
###################################################################################################
###################################################################################################