Compare commits

...

7 Commits

Author SHA1 Message Date
Docker Build 6a1f8eec89 Paragon tester hunter BiS, mount cast QoL, learn all mounts RBAC, trade cap 11
- mod-paragon: .paragon tester bis hunter (Sanctified Ahn'Kahar Blood Hunter + Windrunner's Heartseeker), bis gems kits, AGI bow vs ranged/gun/crossbow, ranged for spi/hybrid weapons.
- .learn all mounts: RBAC 916 + db_auth migration 2026_05_12_00.sql.
- Cast-time mount spells: allow start/complete while moving; block in combat; interrupt mount cast on combat enter; relax movement prevention for NPCs/units.
- MaxPrimaryTradeSkill default 11 (all WotLK primary professions) in WorldConfig + worldserver.conf.dist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 23:02:30 -04:00
Docker Build 0bb6b0ef84 feat(launcher): script to replace launcher-only on legacy Gitea release
- gitea-replace-launcher-only.sh: swap Fractured-Launcher* + yml without wiping MPQs
- Only remove latest.yml / latest-linux.yml if a replacement exists in dist/
- README: bridge rollout steps and checklist item

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:44:13 -05:00
Docker Build 295cb6df52 chore(launcher): point baked Gitea to git.hisora.dev, bump 1.0.13
Players on the old DDNS host can fetch this build from there once, then
patches and launcher updates use https://git.hisora.dev (Dawnsorrow/Fractured-Distro).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:39:33 -05:00
Docker Build fbd6ea47f2 Switch server scripts to tmux for panel console access
Rewrite start-azeroth-servers.sh to launch auth/worldserver in named
tmux sessions instead of nohup/disown. Add kill-azeroth-servers.sh to
tear down sessions and stray processes. Update vps-update-server.sh
with a --restart flag that stops servers before compile and restarts
them in tmux after.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:39:33 -05:00
Docker Build a64279ed7e fix(player): restore missing additional save timer and reduce autosave interval
The m_additionalSaveTimer was never processed in the update loop, so quick
partial saves after important events (rare+ item pickups, quest completions)
never fired. This caused players to lose progress on disconnect/crash since
only the 15-minute full autosave protected them.

- Add m_additionalSaveTimer tick logic to Player::Update
- Reduce default PlayerSaveInterval from 900000 (15 min) to 300000 (5 min)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:39:33 -05:00
Docker Build 87219cb4eb Paragon: multidot Devouring Plague, stance/presence clones, advancement SLA
- Allow multiple Devouring Plague DoTs on different targets (core + DK script).

- Warrior stance and DK presence clone spells for Character Advancement; spellbook SkillLineAbility rows and aura/shapeshift attribute fixes.

- World SQL updates 2026_05_12_02 through 07 (mod-paragon db-world).

Client patch-enUS-4/5/6 and Wow.exe ship on the matching GitHub Release (not in repo).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 19:20:13 -04:00
Docker Build da17074a63 Paragon: Runeforging support (panel-purchasable, no anvil required)
Lets the Paragon class buy Runeforging from the Character Advancement
panel and apply rune enchants from anywhere in the world without needing
to be near a runeforge GameObject. Three carve-outs work together:

* Spell.cpp: bypass the SpellFocusObject GO proximity check when the
  caster is a Paragon and the spell belongs to SKILL_RUNEFORGING (776).
  Stock DK behaviour is unchanged -- the bypass is gated on
  getClass() == CLASS_PARAGON, not on the IsClass() context hook.

* Player.cpp: skip the Paragon class-skill cascade block for skill 776
  so the rune-enchant SLA cascade actually fires. Without this the
  player gets the Runeforging skill but no rune options at the anvil.

* Paragon_Essence.cpp:
  - Treat SKILL_RUNEFORGING children as a meta-skill cluster: cascade
    them like passives even though they're active casts, so they stick
    as panel_spell_children and get cleaned up via the standard refund
    path.
  - Whitelist the 8 basic rune-enchants in PruneSkillLineCascadeChildren
    so they don't get evicted as "active in children = legacy garbage".
  - Force-attach them in PanelLearnSpellChain (the SLA rows ship with
    AcquireMethod=0, so the engine cascade alone won't grant them).
  - Add an OnPlayerLogin fixup so existing Paragons who bought
    Runeforging before this change get the 8 runes retro-granted.
  - Stop filtering SPELL_ATTR0_DO_NOT_DISPLAY in PushSpellSnapshot --
    Runeforging itself is hidden in the DBC but is a real panel
    purchase that must show in the Overview tab.

The two advanced runes (Stoneskin Gargoyle, Nerubian Carapace) are
intentionally excluded from the auto-grant -- retail gates them behind
heroic dungeon / raid item drops and the SLA AcquireMethod=0 honours
that gating.

No SQL migration needed; works against existing DBC + SLA data.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 04:49:38 -04:00
30 changed files with 1606 additions and 87 deletions
@@ -0,0 +1,10 @@
-- DB update 2026_05_03_00 -> 2026_05_12_00
-- RBAC permission for .learn all mounts (Admin 196, Gamemaster 197).
DELETE FROM `rbac_permissions` WHERE `id` = 916;
INSERT INTO `rbac_permissions` (`id`, `name`) VALUES
(916, 'Command: learn all mounts');
DELETE FROM `rbac_linked_permissions` WHERE `linkedId` = 916;
INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES
(196, 916),
(197, 916);
@@ -0,0 +1,21 @@
-- Fractured / Paragon: multidot Devouring Plague clone (spell IDs 951000-951008).
-- Spell rows live in the patched client Spell.dbc (see fractured-tooling
-- from-workspace-root/_patch_spell_dbc_paragon_multidot_devouring_plague.py).
-- Deploy the same Spell.dbc into the worldserver `data/dbc/` folder OR import
-- equivalent `spell_dbc` rows from a full exporter; stock SQL cannot express
-- the SpellEntryfmt NA padding columns safely in one INSERT here.
DELETE FROM `spell_ranks` WHERE `first_spell_id` = 951000;
INSERT INTO `spell_ranks` (`first_spell_id`,`spell_id`,`rank`) VALUES
(951000,951000,1),
(951000,951001,2),
(951000,951002,3),
(951000,951003,4),
(951000,951004,5),
(951000,951005,6),
(951000,951006,7),
(951000,951007,8),
(951000,951008,9);
DELETE FROM `paragon_spell_ae_cost` WHERE `spell_id` IN (2944,951000);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`,`ae_cost`) VALUES (951000, 1);
@@ -0,0 +1,15 @@
-- Fractured / Paragon: spellbook tab for multidot Devouring Plague (951000 chain).
-- Shadow priest skill line (78); ClassMask 2064 matches mod-paragon SLA overlay.
-- Client: patched SkillLineAbility.dbc in patch-enUS-4 from the same script.
DELETE FROM `skilllineability_dbc` WHERE `ID` IN (1951000, 1951001, 1951002, 1951003, 1951004, 1951005, 1951006, 1951007, 1951008);
INSERT INTO `skilllineability_dbc` (`ID`,`SkillLine`,`Spell`,`RaceMask`,`ClassMask`,`ExcludeRace`,`ExcludeClass`,`MinSkillLineRank`,`SupercededBySpell`,`AcquireMethod`,`TrivialSkillLineRankHigh`,`TrivialSkillLineRankLow`,`CharacterPoints_1`,`CharacterPoints_2`) VALUES
(1951000,78,951000,0,2064,0,0,1,0,0,0,0,0,0),
(1951001,78,951001,0,2064,0,0,1,0,0,0,0,0,0),
(1951002,78,951002,0,2064,0,0,1,0,0,0,0,0,0),
(1951003,78,951003,0,2064,0,0,1,0,0,0,0,0,0),
(1951004,78,951004,0,2064,0,0,1,0,0,0,0,0,0),
(1951005,78,951005,0,2064,0,0,1,0,0,0,0,0,0),
(1951006,78,951006,0,2064,0,0,1,0,0,0,0,0,0),
(1951007,78,951007,0,2064,0,0,1,0,0,0,0,0,0),
(1951008,78,951008,0,2064,0,0,1,0,0,0,0,0,0);
@@ -0,0 +1,21 @@
-- Fractured / Paragon: Character Advancement stance/presence clones (951010-951015).
-- Client: patched Spell.dbc + SpellShapeshiftForm.dbc + SkillLineAbility.dbc in patch-enUS-4.MPQ.
-- Server: copy Spell.dbc + SpellShapeshiftForm.dbc into `data/dbc/` (SpellShapeshiftForm is not in stock MPQ); SkillLineAbility is DB-driven on server.
DELETE FROM `paragon_spell_ae_cost` WHERE `spell_id` IN (951010,951011,951012,951013,951014,951015);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`,`ae_cost`) VALUES
(951010, 1),
(951011, 1),
(951012, 1),
(951013, 1),
(951014, 1),
(951015, 1);
DELETE FROM `skilllineability_dbc` WHERE `ID` IN (1951020,1951021,1951022,1951023,1951024,1951025);
INSERT INTO `skilllineability_dbc` (`ID`,`SkillLine`,`Spell`,`RaceMask`,`ClassMask`,`ExcludeRace`,`ExcludeClass`,`MinSkillLineRank`,`SupercededBySpell`,`AcquireMethod`,`TrivialSkillLineRankHigh`,`TrivialSkillLineRankLow`,`CharacterPoints_1`,`CharacterPoints_2`) VALUES
(1951020,26,951010,0,2049,0,0,1,0,2,0,0,0,0),
(1951021,257,951011,0,2049,0,0,1,0,0,0,0,0,0),
(1951022,256,951012,0,2049,0,0,1,0,0,0,0,0,0),
(1951023,770,951013,0,2080,0,0,1,0,2,0,0,0,0),
(1951024,771,951014,0,2080,0,0,1,0,0,0,0,0,0),
(1951025,772,951015,0,2080,0,0,1,0,0,0,0,0,0);
@@ -0,0 +1,9 @@
-- Fractured / Paragon: run spell_dk_presence on Character Advancement DK presence clones (951013-951015).
-- Spell.dbc sets SpellFamilyName=0 on these rows (see fractured-tooling/_patch_spell_dbc_paragon_stance_presence_clones.py)
-- so the stock client does not map them onto DK stance buttons; core still needs the aura script for Improved Presence.
DELETE FROM `spell_script_names` WHERE `spell_id` IN (951013, 951014, 951015);
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(951013, 'spell_dk_presence'),
(951014, 'spell_dk_presence'),
(951015, 'spell_dk_presence');
@@ -0,0 +1,8 @@
-- Fractured / Paragon: Character Advancement stance/presence clones — spellbook + client bits.
-- 1) SkillLineAbility: DK presence clones belong on 770/771/772 (Blood/Frost/Unholy tabs), not 760 (General).
-- (760 was an experiment; stance bar visibility is driven by Spell.dbc AttributesEx2 USE_SHAPESHIFT_BAR.)
-- 2) Idempotent if rows already match.
UPDATE `skilllineability_dbc` SET `SkillLine` = 770 WHERE `ID` = 1951023 AND `Spell` = 951013;
UPDATE `skilllineability_dbc` SET `SkillLine` = 771 WHERE `ID` = 1951024 AND `Spell` = 951014;
UPDATE `skilllineability_dbc` SET `SkillLine` = 772 WHERE `ID` = 1951025 AND `Spell` = 951015;
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Kill AzerothCore authserver + worldserver tmux sessions and any stray processes.
#
# Usage:
# bash scripts/kill-azeroth-servers.sh
set -euo pipefail
AUTH_SESSION="authserver"
WORLD_SESSION="worldserver"
echo "Stopping servers..."
# Kill tmux sessions
if tmux has-session -t "$WORLD_SESSION" 2>/dev/null; then
tmux kill-session -t "$WORLD_SESSION"
echo " killed tmux session: $WORLD_SESSION"
else
echo " no tmux session: $WORLD_SESSION"
fi
if tmux has-session -t "$AUTH_SESSION" 2>/dev/null; then
tmux kill-session -t "$AUTH_SESSION"
echo " killed tmux session: $AUTH_SESSION"
else
echo " no tmux session: $AUTH_SESSION"
fi
# Clean up any stray processes not managed by tmux
if pkill -x worldserver 2>/dev/null; then
echo " killed stray worldserver process"
fi
if pkill -x authserver 2>/dev/null; then
echo " killed stray authserver process"
fi
echo "Done."
+32 -13
View File
@@ -1,14 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Start AzerothCore authserver + worldserver detached from the SSH session (nohup + disown). # Start AzerothCore authserver + worldserver in named tmux sessions.
# Stops any already-running authserver/worldserver processes first. # Kills any already-running sessions first (acts as a restart).
# #
# Usage: # Usage:
# sudo bash scripts/start-azeroth-servers.sh # bash scripts/start-azeroth-servers.sh
# AZEROTH_BIN=/path/to/azeroth-server/bin bash scripts/start-azeroth-servers.sh # AZEROTH_BIN=/path/to/bin bash scripts/start-azeroth-servers.sh
# #
# Environment: # Environment:
# AZEROTH_BIN — directory with authserver and worldserver (default: /home/fractured-panel/azeroth-server/bin) # AZEROTH_BIN — directory with authserver and worldserver (default: /home/fractured-panel/azeroth-server/bin)
# AZEROTH_LOG_DIR — log directory (default: <parent of bin>/logs) # AZEROTH_LOG_DIR — log directory (default: <parent of bin>/logs)
#
# tmux sessions:
# authserver — authserver console
# worldserver — worldserver console
set -euo pipefail set -euo pipefail
@@ -20,6 +24,14 @@ CONF_DIR="${BASE_DIR}/etc"
AUTH_BIN="${BIN_DIR}/authserver" AUTH_BIN="${BIN_DIR}/authserver"
WORLD_BIN="${BIN_DIR}/worldserver" WORLD_BIN="${BIN_DIR}/worldserver"
AUTH_SESSION="authserver"
WORLD_SESSION="worldserver"
if ! command -v tmux &>/dev/null; then
echo "error: tmux is not installed" >&2
exit 1
fi
if [[ ! -x "$AUTH_BIN" ]]; then if [[ ! -x "$AUTH_BIN" ]]; then
echo "error: not found or not executable: $AUTH_BIN" >&2 echo "error: not found or not executable: $AUTH_BIN" >&2
exit 1 exit 1
@@ -29,23 +41,30 @@ if [[ ! -x "$WORLD_BIN" ]]; then
exit 1 exit 1
fi fi
mkdir -p "$LOG_DIR"
# Tear down existing sessions (ignore errors if they don't exist)
tmux kill-session -t "$AUTH_SESSION" 2>/dev/null || true
tmux kill-session -t "$WORLD_SESSION" 2>/dev/null || true
# Also kill any stray processes not managed by tmux
pkill -x authserver 2>/dev/null || true pkill -x authserver 2>/dev/null || true
pkill -x worldserver 2>/dev/null || true pkill -x worldserver 2>/dev/null || true
sleep 1 sleep 1
mkdir -p "$LOG_DIR" # Launch authserver in a tmux session
tmux new-session -d -s "$AUTH_SESSION" -c "$BIN_DIR" \
cd "$BIN_DIR" "$AUTH_BIN -c ${CONF_DIR}/authserver.conf 2>&1 | tee -a ${LOG_DIR}/authserver.log"
nohup "$AUTH_BIN" -c "${CONF_DIR}/authserver.conf" >>"$LOG_DIR/authserver.log" 2>&1 &
disown || true
sleep 2 sleep 2
nohup "$WORLD_BIN" -c "${CONF_DIR}/worldserver.conf" >>"$LOG_DIR/worldserver.log" 2>&1 & # Launch worldserver in a tmux session
disown || true tmux new-session -d -s "$WORLD_SESSION" -c "$BIN_DIR" \
"$WORLD_BIN -c ${CONF_DIR}/worldserver.conf 2>&1 | tee -a ${LOG_DIR}/worldserver.log"
echo "Started authserver and worldserver (survives SSH disconnect)." echo "Started servers in tmux sessions."
echo " tmux attach -t $AUTH_SESSION — authserver console"
echo " tmux attach -t $WORLD_SESSION — worldserver console"
echo "Bin: $BIN_DIR" echo "Bin: $BIN_DIR"
echo "Config: $CONF_DIR" echo "Config: $CONF_DIR"
echo "Logs: $LOG_DIR/authserver.log" echo "Logs: $LOG_DIR/authserver.log"
+42 -14
View File
@@ -6,24 +6,25 @@
# (see docs/DEPLOY_LINUX_VPS.md). # (see docs/DEPLOY_LINUX_VPS.md).
# #
# What this does: # What this does:
# 1. git pull on the current branch (optional; can skip) # 1. Optionally kill running servers (tmux sessions)
# 2. ./acore.sh compiler build — or compiler all for a full clean rebuild # 2. git pull on the current branch (optional; can skip)
# 3. ./acore.sh compiler build — or compiler all for a full clean rebuild
# 4. Optionally restart servers in tmux sessions
# #
# Database migrations from data/sql/updates/ run when you next start worldserver/authserver # 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 # (Updates.* / SourceDirectory in *.conf).
# you pass --run-after or set FRACTURED_POST_UPDATE_CMD.
# #
# Usage: # Usage:
# bash scripts/vps-update-server.sh # bash scripts/vps-update-server.sh # pull + compile only
# bash scripts/vps-update-server.sh --full # bash scripts/vps-update-server.sh --restart # pull + compile + restart servers in tmux
# bash scripts/vps-update-server.sh --no-pull # bash scripts/vps-update-server.sh --full --restart # clean rebuild + restart
# bash scripts/vps-update-server.sh --no-pull --restart # compile current tree + restart
# bash scripts/vps-update-server.sh --dry-run # 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 'custom command here'
# bash scripts/vps-update-server.sh --run-after 'sudo systemctl restart fractured-world'
# #
# Environment: # Environment:
# FRACTURED_GIT_REMOTE — remote name (default: origin) # 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) # FRACTURED_POST_UPDATE_CMD — shell command run after compile (used by bare --run-after)
set -euo pipefail set -euo pipefail
@@ -35,6 +36,7 @@ FULL_BUILD=0
COMPILE_ONLY=0 COMPILE_ONLY=0
DRY_RUN=0 DRY_RUN=0
DO_RUN_AFTER=0 DO_RUN_AFTER=0
DO_RESTART=0
INSTALL_PREFIX="" INSTALL_PREFIX=""
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}" POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}" GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
@@ -52,8 +54,9 @@ Options:
--compile-only ./acore.sh compiler compile (incremental). --compile-only ./acore.sh compiler compile (incremental).
--prefix PATH Override CMAKE_INSTALL_PREFIX (updates conf/config.sh BINPATH). --prefix PATH Override CMAKE_INSTALL_PREFIX (updates conf/config.sh BINPATH).
--dry-run Print commands without running them. --dry-run Print commands without running them.
--run-after [CMD] Run shell command after successful compile. If CMD is omitted, --restart Kill servers before compile, restart in tmux after.
uses FRACTURED_POST_UPDATE_CMD from the environment. --run-after [CMD] Run a custom shell command after successful compile.
If CMD is omitted, uses FRACTURED_POST_UPDATE_CMD.
Environment: Environment:
FRACTURED_GIT_REMOTE Git remote (default: origin). FRACTURED_GIT_REMOTE Git remote (default: origin).
@@ -102,6 +105,10 @@ while [[ $# -gt 0 ]]; do
DRY_RUN=1 DRY_RUN=1
shift shift
;; ;;
--restart)
DO_RESTART=1
shift
;;
--run-after) --run-after)
DO_RUN_AFTER=1 DO_RUN_AFTER=1
shift shift
@@ -140,6 +147,18 @@ fi
cd "$ROOT" cd "$ROOT"
KILL_SCRIPT="${SCRIPT_DIR}/kill-azeroth-servers.sh"
START_SCRIPT="${SCRIPT_DIR}/start-azeroth-servers.sh"
if [[ "$DO_RESTART" -eq 1 ]]; then
if [[ ! -f "$KILL_SCRIPT" || ! -f "$START_SCRIPT" ]]; then
echo "error: --restart requires kill-azeroth-servers.sh and start-azeroth-servers.sh in scripts/" >&2
exit 1
fi
echo "==> stopping servers before compile"
run bash "$KILL_SCRIPT"
fi
if [[ -n "$INSTALL_PREFIX" ]]; then if [[ -n "$INSTALL_PREFIX" ]]; then
echo "==> updating conf/config.sh BINPATH to: $INSTALL_PREFIX" echo "==> updating conf/config.sh BINPATH to: $INSTALL_PREFIX"
if grep -q '^BINPATH=' conf/config.sh; then if grep -q '^BINPATH=' conf/config.sh; then
@@ -189,6 +208,11 @@ else
run ./acore.sh compiler build run ./acore.sh compiler build
fi fi
if [[ "$DO_RESTART" -eq 1 ]]; then
echo "==> restarting servers in tmux sessions"
run bash "$START_SCRIPT"
fi
if [[ "$DO_RUN_AFTER" -eq 1 ]]; then if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
echo "==> post-update: $POST_UPDATE_CMD" echo "==> post-update: $POST_UPDATE_CMD"
if [[ "$DRY_RUN" -eq 1 ]]; then if [[ "$DRY_RUN" -eq 1 ]]; then
@@ -199,4 +223,8 @@ if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
fi fi
fi fi
echo "Done. Restart authserver/worldserver (or your service manager) when ready so new binaries and SQL updates apply." if [[ "$DO_RESTART" -eq 0 && "$DO_RUN_AFTER" -eq 0 ]]; then
echo "Done. Run 'bash scripts/start-azeroth-servers.sh' to (re)start servers in tmux."
else
echo "Done."
fi
@@ -1753,9 +1753,9 @@ InstantLogout = 1
# #
# PlayerSaveInterval # PlayerSaveInterval
# Description: Time (in milliseconds) for player save interval. # Description: Time (in milliseconds) for player save interval.
# Default: 900000 - (15 min) # Default: 300000 - (5 min)
PlayerSaveInterval = 900000 PlayerSaveInterval = 300000
# #
# PlayerSave.Stats.MinLevel # PlayerSave.Stats.MinLevel
@@ -2260,9 +2260,9 @@ Achievement.RealmFirstKillWindow = 60
# MaxPrimaryTradeSkill # MaxPrimaryTradeSkill
# Description: Maximum number of primary professions a character can learn. # Description: Maximum number of primary professions a character can learn.
# Range: 0-11 # Range: 0-11
# Default: 2 # Default: 11 - (All WotLK primary professions; set 2 for retail-like two-slot cap.)
MaxPrimaryTradeSkill = 2 MaxPrimaryTradeSkill = 11
# #
# SkillChance.Prospecting # SkillChance.Prospecting
+1
View File
@@ -678,6 +678,7 @@ enum RBACPermissions
RBAC_PERM_COMMAND_BF_QUEUE = 913, RBAC_PERM_COMMAND_BF_QUEUE = 913,
RBAC_PERM_COMMAND_PET_LIST = 914, RBAC_PERM_COMMAND_PET_LIST = 914,
RBAC_PERM_COMMAND_PET_DELETE = 915, RBAC_PERM_COMMAND_PET_DELETE = 915,
RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS = 916,
// custom permissions 1000+ // custom permissions 1000+
RBAC_PERM_MAX RBAC_PERM_MAX
}; };
@@ -3640,6 +3640,13 @@ bool Creature::IsMovementPreventedByCasting() const
return false; return false;
} }
// Fractured: cast-time mount summon (player-style mount spells on NPCs are rare but supported).
if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL])
{
if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell())
return false;
}
if (HasSpellFocus()) if (HasSpellFocus())
{ {
return true; return true;
+14 -1
View File
@@ -12048,7 +12048,20 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
// weapon, language, and racial skill cascades stay enabled so things // weapon, language, and racial skill cascades stay enabled so things
// like recipe auto-learn, weapon proficiencies, and racial perks // like recipe auto-learn, weapon proficiencies, and racial perks
// still work. // still work.
if (getClass() == CLASS_PARAGON) //
// Carve-out: SKILL_RUNEFORGING (776) is a CLASS-category skill but
// behaves like a profession in this context — the player buys ONE
// panel ability (Runeforging, spell 53428) and the rune-enchant
// spells (Rune of the Fallen Crusader, Razorice, Cinderglacier, ...)
// are supposed to come along for the ride via the standard SLA
// cascade, exactly the same way they do for a stock DK. Without
// this carve-out, the early-return below blocks the cascade and a
// Paragon who buys Runeforging gets the skill but no actual rune
// options at the runeforge anvil. The cascade only fires once per
// skill-grant for 776 (it's not on UpdateSkillsForLevel) so the
// "leaking back into the spellbook" concern that motivates the
// early-return doesn't apply to this skill.
if (getClass() == CLASS_PARAGON && skill_id != SKILL_RUNEFORGING)
{ {
if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id)) if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
if (sl->categoryId == SKILL_CATEGORY_CLASS) if (sl->categoryId == SKILL_CATEGORY_CLASS)
+1
View File
@@ -1828,6 +1828,7 @@ public:
uint32 GetLastPotionId() { return m_lastPotionId; } uint32 GetLastPotionId() { return m_lastPotionId; }
void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; } void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; }
void UpdatePotionCooldown(Spell* spell = nullptr); void UpdatePotionCooldown(Spell* spell = nullptr);
void AtEnterCombat() override;
void AtExitCombat() override; void AtExitCombat() override;
void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana) void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana)
@@ -32,6 +32,7 @@
#include "Player.h" #include "Player.h"
#include "ScriptMgr.h" #include "ScriptMgr.h"
#include "SkillDiscovery.h" #include "SkillDiscovery.h"
#include "Spell.h"
#include "SpellAuraEffects.h" #include "SpellAuraEffects.h"
#include "SpellMgr.h" #include "SpellMgr.h"
#include "UpdateFieldFlags.h" #include "UpdateFieldFlags.h"
@@ -332,6 +333,28 @@ void Player::Update(uint32 p_time)
} }
} }
if (m_additionalSaveTimer)
{
if (p_time >= m_additionalSaveTimer)
{
m_additionalSaveTimer = 0;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
if (m_additionalSaveMask & ADDITIONAL_SAVING_INVENTORY_AND_GOLD)
SaveInventoryAndGoldToDB(trans);
if (m_additionalSaveMask & ADDITIONAL_SAVING_QUEST_STATUS)
_SaveQuestStatus(trans);
CharacterDatabase.CommitTransaction(trans);
m_additionalSaveMask = 0;
}
else
{
m_additionalSaveTimer -= p_time;
}
}
// Handle Water/drowning // Handle Water/drowning
HandleDrowning(p_time); HandleDrowning(p_time);
@@ -1539,6 +1562,27 @@ void Player::UpdatePvP(bool state, bool _override)
sScriptMgr->OnPlayerPVPFlagChange(this, state); sScriptMgr->OnPlayerPVPFlagChange(this, state);
} }
void Player::AtEnterCombat()
{
Unit::AtEnterCombat();
// Fractured: cancel cast-time mount summon if combat starts mid-cast.
for (uint32 spellType = CURRENT_FIRST_NON_MELEE_SPELL; spellType < CURRENT_MAX_SPELL; ++spellType)
{
if (Spell* spell = GetCurrentSpell(CurrentSpellTypes(spellType)))
{
if (SpellInfo const* info = spell->GetSpellInfo())
{
if (info->IsCastTimeRidingMountSpell())
{
InterruptSpell(CurrentSpellTypes(spellType), false, false);
break;
}
}
}
}
}
void Player::AtExitCombat() void Player::AtExitCombat()
{ {
Unit::AtExitCombat(); Unit::AtExitCombat();
+17
View File
@@ -151,6 +151,11 @@ bool IsFracturedExclusiveStanceSpell(uint32 spellId)
case 71: // Defensive Stance case 71: // Defensive Stance
case 2458: // Berserker Stance case 2458: // Berserker Stance
// -- Paragon Advancement warrior stance clones (951010-951012).
case 951010:
case 951011:
case 951012:
// -- Druid combat forms (engine-shapeshifts). // -- Druid combat forms (engine-shapeshifts).
case 5487: // Bear Form case 5487: // Bear Form
case 9634: // Dire Bear Form case 9634: // Dire Bear Form
@@ -189,6 +194,11 @@ bool IsFracturedExclusiveStanceSpell(uint32 spellId)
case 48263: // Frost Presence case 48263: // Frost Presence
case 48265: // Unholy Presence case 48265: // Unholy Presence
// -- Paragon Advancement DK presence clones (951013-951015).
case 951013:
case 951014:
case 951015:
// -- Hunter Aspects (combat). Like presences, these are regular // -- Hunter Aspects (combat). Like presences, these are regular
// auras stock AC, not engine-shapeshifts; rank-1 ids cover all // auras stock AC, not engine-shapeshifts; rank-1 ids cover all
// ranks via GetFirstRankSpell. Cheetah / Pack are the utility // ranks via GetFirstRankSpell. Cheetah / Pack are the utility
@@ -4493,6 +4503,13 @@ bool Unit::IsMovementPreventedByCasting() const
return false; return false;
} }
// Fractured: cast-time mount summon may be completed while moving.
if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL])
{
if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell())
return false;
}
// channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute // channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute
if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL])
{ {
+7 -1
View File
@@ -99,7 +99,13 @@ enum ShapeshiftForm
FORM_FLIGHT = 0x1D, FORM_FLIGHT = 0x1D,
FORM_STEALTH = 0x1E, FORM_STEALTH = 0x1E,
FORM_MOONKIN = 0x1F, FORM_MOONKIN = 0x1F,
FORM_SPIRITOFREDEMPTION = 0x20 FORM_SPIRITOFREDEMPTION = 0x20,
// Fractured / Paragon: Character Advancement warrior stance clones (Spell.dbc
// MOD_SHAPESHIFT -> SpellShapeshiftForm 33-35, BonusActionBar=0, no client bar swap).
FORM_PARAGON_BATTLE_STANCE = 33,
FORM_PARAGON_DEFENSIVE_STANCE = 34,
FORM_PARAGON_BERSERKER_STANCE = 35,
}; };
enum ShapeshiftFlags enum ShapeshiftFlags
@@ -1373,12 +1373,15 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
HotWSpellId = 24899; HotWSpellId = 24899;
break; break;
case FORM_BATTLESTANCE: case FORM_BATTLESTANCE:
case FORM_PARAGON_BATTLE_STANCE:
spellId = 21156; spellId = 21156;
break; break;
case FORM_DEFENSIVESTANCE: case FORM_DEFENSIVESTANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
spellId = 7376; spellId = 7376;
break; break;
case FORM_BERSERKERSTANCE: case FORM_BERSERKERSTANCE:
case FORM_PARAGON_BERSERKER_STANCE:
spellId = 7381; spellId = 7381;
break; break;
case FORM_MOONKIN: case FORM_MOONKIN:
@@ -1995,7 +1998,7 @@ void AuraEffect::HandlePhase(AuraApplication const* aurApp, uint8 mode, bool app
/*** UNIT MODEL ***/ /*** UNIT MODEL ***/
/**********************/ /**********************/
void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mode, bool apply) const static void Fractured_ApplyShapeshiftFormFromAuraEffect(AuraEffect const* aurEff, AuraApplication const* aurApp, uint8 mode, bool apply, ShapeshiftForm form)
{ {
if (!(mode & AURA_EFFECT_HANDLE_REAL_OR_REAPPLY_MASK)) if (!(mode & AURA_EFFECT_HANDLE_REAL_OR_REAPPLY_MASK))
return; return;
@@ -2004,8 +2007,6 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
uint32 modelid = 0; uint32 modelid = 0;
Powers PowerType = POWER_MANA; Powers PowerType = POWER_MANA;
ShapeshiftForm form = ShapeshiftForm(GetMiscValue());
switch (form) switch (form)
{ {
case FORM_CAT: // 0x01 case FORM_CAT: // 0x01
@@ -2019,6 +2020,9 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_BATTLESTANCE: // 0x11 case FORM_BATTLESTANCE: // 0x11
case FORM_DEFENSIVESTANCE: // 0x12 case FORM_DEFENSIVESTANCE: // 0x12
case FORM_BERSERKERSTANCE: // 0x13 case FORM_BERSERKERSTANCE: // 0x13
case FORM_PARAGON_BATTLE_STANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
case FORM_PARAGON_BERSERKER_STANCE:
PowerType = POWER_RAGE; PowerType = POWER_RAGE;
break; break;
@@ -2049,10 +2053,10 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_SPIRITOFREDEMPTION: // 0x20 case FORM_SPIRITOFREDEMPTION: // 0x20
break; break;
default: default:
LOG_ERROR("spells.aura.effect", "Auras: Unknown Shapeshift Type: {}", GetMiscValue()); LOG_ERROR("spells.aura.effect", "Auras: Unknown Shapeshift Type: {}", aurEff->GetMiscValue());
} }
modelid = target->GetModelForForm(form, GetId()); modelid = target->GetModelForForm(form, aurEff->GetId());
if (apply) if (apply)
{ {
@@ -2087,8 +2091,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
// remove other shapeshift before applying a new one // remove other shapeshift before applying a new one
// xinef: rogue shouldnt be wrapped by this check (shadow dance vs stealth) // xinef: rogue shouldnt be wrapped by this check (shadow dance vs stealth)
if (GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE) if (aurEff->GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE)
target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, GetBase()); target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, aurEff->GetBase());
// stop handling the effect if it was removed by linked event // stop handling the effect if it was removed by linked event
if (aurApp->GetRemoveMode()) if (aurApp->GetRemoveMode())
@@ -2112,13 +2116,13 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (AuraEffect const* dummy = target->GetDummyAuraEffect(SPELLFAMILY_DRUID, 238, 0)) if (AuraEffect const* dummy = target->GetDummyAuraEffect(SPELLFAMILY_DRUID, 238, 0))
FurorChance = std::max(dummy->GetAmount(), 0); FurorChance = std::max(dummy->GetAmount(), 0);
switch (GetMiscValue()) switch (aurEff->GetMiscValue())
{ {
case FORM_CAT: case FORM_CAT:
{ {
int32 basePoints = int32(std::min(oldPower, FurorChance)); int32 basePoints = int32(std::min(oldPower, FurorChance));
target->SetPower(POWER_ENERGY, 0); target->SetPower(POWER_ENERGY, 0);
target->CastCustomSpell(target, 17099, &basePoints, nullptr, nullptr, true, nullptr, this); target->CastCustomSpell(target, 17099, &basePoints, nullptr, nullptr, true, nullptr, aurEff);
break; break;
} }
case FORM_BEAR: case FORM_BEAR:
@@ -2192,13 +2196,16 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_BATTLESTANCE: case FORM_BATTLESTANCE:
case FORM_DEFENSIVESTANCE: case FORM_DEFENSIVESTANCE:
case FORM_BERSERKERSTANCE: case FORM_BERSERKERSTANCE:
case FORM_PARAGON_BATTLE_STANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
case FORM_PARAGON_BERSERKER_STANCE:
{ {
uint32 Rage_val = 0; uint32 Rage_val = 0;
// Defensive Tactics // Defensive Tactics
if (form == FORM_DEFENSIVESTANCE) if (form == FORM_DEFENSIVESTANCE || form == FORM_PARAGON_DEFENSIVE_STANCE)
{ {
if (AuraEffect const* aurEff = target->IsScriptOverriden(m_spellInfo, 831)) if (AuraEffect const* scriptEff = target->IsScriptOverriden(aurEff->GetSpellInfo(), 831))
Rage_val += aurEff->GetAmount() * 10; Rage_val += scriptEff->GetAmount() * 10;
} }
// Stance mastery + Tactical mastery (both passive, and last have aura only in defense stance, but need apply at any stance switch) // Stance mastery + Tactical mastery (both passive, and last have aura only in defense stance, but need apply at any stance switch)
if (target->IsPlayer()) if (target->IsPlayer())
@@ -2238,7 +2245,7 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
// adding/removing linked auras // adding/removing linked auras
// add/remove the shapeshift aura's boosts // add/remove the shapeshift aura's boosts
HandleShapeshiftBoosts(target, apply); aurEff->HandleShapeshiftBoosts(target, apply);
if (target->IsPlayer()) if (target->IsPlayer())
target->ToPlayer()->InitDataForForm(); target->ToPlayer()->InitDataForForm();
@@ -2246,8 +2253,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (target->IsClass(CLASS_DRUID, CLASS_CONTEXT_ABILITY)) if (target->IsClass(CLASS_DRUID, CLASS_CONTEXT_ABILITY))
{ {
// Dash // Dash
if (AuraEffect* aurEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8)) if (AuraEffect* dashSpeedEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8))
aurEff->RecalculateAmount(); dashSpeedEff->RecalculateAmount();
// Disarm handling // Disarm handling
// If druid shifts while being disarmed we need to deal with that since forms aren't affected by disarm // If druid shifts while being disarmed we need to deal with that since forms aren't affected by disarm
@@ -2281,6 +2288,11 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (target->IsPlayer()) if (target->IsPlayer())
{ {
SpellShapeshiftFormEntry const* shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form); SpellShapeshiftFormEntry const* shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form);
if (!shapeInfo)
{
LOG_ERROR("spells.aura.effect", "Fractured_ApplyShapeshiftFormFromAuraEffect: missing SpellShapeshiftForm {}", uint32(form));
return;
}
// Learn spells for shapeshift form - no need to send action bars or add spells to spellbook // Learn spells for shapeshift form - no need to send action bars or add spells to spellbook
for (uint8 i = 0; i < MAX_SHAPESHIFT_SPELLS; ++i) for (uint8 i = 0; i < MAX_SHAPESHIFT_SPELLS; ++i)
{ {
@@ -2294,6 +2306,11 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
} }
} }
void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mode, bool apply) const
{
Fractured_ApplyShapeshiftFormFromAuraEffect(this, aurApp, mode, apply, ShapeshiftForm(GetMiscValue()));
}
void AuraEffect::HandleAuraTransform(AuraApplication const* aurApp, uint8 mode, bool apply) const void AuraEffect::HandleAuraTransform(AuraApplication const* aurApp, uint8 mode, bool apply) const
{ {
if (!(mode & AURA_EFFECT_HANDLE_SEND_FOR_CLIENT_MASK)) if (!(mode & AURA_EFFECT_HANDLE_SEND_FOR_CLIENT_MASK))
@@ -5174,6 +5191,20 @@ void AuraEffect::HandleAuraDummy(AuraApplication const* aurApp, uint8 mode, bool
Unit* caster = GetCaster(); Unit* caster = GetCaster();
// Fractured: Paragon warrior stance clones (951010-951012) use SPELL_AURA_DUMMY on Spell.dbc **effect2**
// (misc = Paragon SpellShapeshiftForm 33-35). Effect1 is a separate DUMMY for the buff strip; passive stats
// (e.g. armor pen / threat) live on Effect3. **AttributesEx** clears SPELL_ATTR1_NO_AURA_ICON (DBC + SpellInfoCorrections)
// so the client shows an aura icon — stock Warrior stances keep that bit set on purpose.
if (GetAuraType() == SPELL_AURA_DUMMY && m_effIndex == 1)
{
uint32 const sid = GetSpellInfo()->Id;
if (sid == 951010 || sid == 951011 || sid == 951012)
{
Fractured_ApplyShapeshiftFormFromAuraEffect(this, aurApp, mode, apply, ShapeshiftForm(GetMiscValue()));
return;
}
}
if (mode & AURA_EFFECT_HANDLE_REAL) if (mode & AURA_EFFECT_HANDLE_REAL)
{ {
// pet auras // pet auras
+54 -10
View File
@@ -3589,14 +3589,18 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const
// don't allow channeled spells / spells with cast time to be casted while moving // don't allow channeled spells / spells with cast time to be casted while moving
// (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in) // (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in)
// Fractured: cast-time mount summons (SPELL_AURA_MOUNTED + non-zero base cast) may be started while moving.
if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered()) if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered())
{ {
// 1. Has casttime, 2. Or doesn't have flag to allow action during channel if (!m_spellInfo->IsCastTimeRidingMountSpell())
if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
{ {
SendCastResult(SPELL_FAILED_MOVING); // 1. Has casttime, 2. Or doesn't have flag to allow action during channel
finish(false); if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
return SPELL_FAILED_MOVING; {
SendCastResult(SPELL_FAILED_MOVING);
finish(false);
return SPELL_FAILED_MOVING;
}
} }
} }
@@ -4436,9 +4440,11 @@ void Spell::update(uint32 difftime)
// check if the player caster has moved before the spell finished // check if the player caster has moved before the spell finished
// xinef: added preparing state (real cast, skip channels as they have other flags for this) // xinef: added preparing state (real cast, skip channels as they have other flags for this)
// Fractured: cast-time mount summons are allowed to complete while moving.
if ((m_caster->IsPlayer() && m_timer != 0) && if ((m_caster->IsPlayer() && m_timer != 0) &&
m_caster->isMoving() && (m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT) && m_spellState == SPELL_STATE_PREPARING && m_caster->isMoving() && (m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT) && m_spellState == SPELL_STATE_PREPARING &&
(m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR))) (m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR)) &&
!m_spellInfo->IsCastTimeRidingMountSpell())
{ {
// don't cancel for melee, autorepeat, triggered and instant spells // don't cancel for melee, autorepeat, triggered and instant spells
if (!IsNextMeleeSwingSpell() && !IsAutoRepeat() && !IsTriggered()) if (!IsNextMeleeSwingSpell() && !IsAutoRepeat() && !IsTriggered())
@@ -5815,6 +5821,10 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
if (reqCombat && m_caster->IsInCombat() && !m_spellInfo->CanBeUsedInCombat()) if (reqCombat && m_caster->IsInCombat() && !m_spellInfo->CanBeUsedInCombat())
return SPELL_FAILED_AFFECTING_COMBAT; return SPELL_FAILED_AFFECTING_COMBAT;
// Fractured: cast-time mount summons cannot be used while in combat (even if DBC omits NOT_IN_COMBAT_ONLY_PEACEFUL).
if (reqCombat && m_caster->IsInCombat() && m_spellInfo->IsCastTimeRidingMountSpell())
return SPELL_FAILED_AFFECTING_COMBAT;
} }
// Xinef: exploit protection // Xinef: exploit protection
@@ -5842,10 +5852,14 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
// (not wand currently autorepeat cast delayed to moving stop anyway in spell update code) // (not wand currently autorepeat cast delayed to moving stop anyway in spell update code)
if (m_caster->IsPlayer() && m_caster->ToPlayer()->isMoving() && !IsTriggered()) if (m_caster->IsPlayer() && m_caster->ToPlayer()->isMoving() && !IsTriggered())
{ {
// skip stuck spell to allow use it in falling case and apply spell limitations at movement // Fractured: cast-time mount summons may be started while moving.
if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) && if (!m_spellInfo->IsCastTimeRidingMountSpell())
(IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0)) {
return SPELL_FAILED_MOVING; // skip stuck spell to allow use it in falling case and apply spell limitations at movement
if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) &&
(IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0))
return SPELL_FAILED_MOVING;
}
} }
Vehicle* vehicle = m_caster->GetVehicle(); Vehicle* vehicle = m_caster->GetVehicle();
@@ -7860,6 +7874,36 @@ SpellCastResult Spell::CheckSpellFocus()
// check spell focus object // check spell focus object
if (m_spellInfo->RequiresSpellFocus) if (m_spellInfo->RequiresSpellFocus)
{ {
// Fractured / Paragon: skip the GO proximity check for Paragon
// casters when the spell is a runeforge enchant (skill line 776).
// Paragons get a Runeforge tab in the Character Advancement
// panel that lets them apply rune-enchants from anywhere in the
// world -- no need to fly back to Acherus or find the nearest
// Eastern Plaguelands anvil. The skill-line gate keeps the
// bypass tightly scoped: only the 10 SkillLineAbility-tagged
// rune-enchant spells qualify, every other spell that uses
// SpellFocusObject (Enchanting bench, Cooking fire, Lockpicking
// anvil, etc.) keeps its requirement intact.
//
// DK / other class casters are unchanged -- this carve-out
// intentionally checks getClass() == CLASS_PARAGON rather than
// IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY) (which
// would also return true for Paragons via mod-paragon's hook).
// Stock DK behavior must stay vanilla; the QoL bypass is a
// class-12 feature only.
if (m_caster && m_caster->IsPlayer()
&& m_caster->ToPlayer()->getClass() == CLASS_PARAGON)
{
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(m_spellInfo->Id);
for (auto it = bounds.first; it != bounds.second; ++it)
{
if (it->second->SkillLine == SKILL_RUNEFORGING)
{
return SPELL_CAST_OK;
}
}
}
CellCoord p(Acore::ComputeCellCoord(m_caster->GetPositionX(), m_caster->GetPositionY())); CellCoord p(Acore::ComputeCellCoord(m_caster->GetPositionX(), m_caster->GetPositionY()));
Cell cell(p); Cell cell(p);
+13
View File
@@ -909,6 +909,15 @@ bool SpellInfo::HasAura(AuraType aura) const
return false; return false;
} }
bool SpellInfo::IsCastTimeRidingMountSpell() const
{
if (IsChanneled())
return false;
if (!HasAura(SPELL_AURA_MOUNTED))
return false;
return CalcCastTime(nullptr, nullptr) > 0;
}
bool SpellInfo::HasAnyAura() const bool SpellInfo::HasAnyAura() const
{ {
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
@@ -2147,6 +2156,10 @@ SpellSpecificType SpellInfo::LoadSpellSpecific() const
{ {
case SPELLFAMILY_GENERIC: case SPELLFAMILY_GENERIC:
{ {
// Fractured / Paragon: DK presence advancement clones (SpellFamilyName=0 in Spell.dbc for client UX).
if (Id == 951013 || Id == 951014 || Id == 951015)
return SPELL_SPECIFIC_PRESENCE;
// Food / Drinks (mostly) // Food / Drinks (mostly)
if (AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) if (AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED)
{ {
+3
View File
@@ -434,6 +434,9 @@ public:
bool HasEffect(SpellEffects effect) const; bool HasEffect(SpellEffects effect) const;
bool HasEffectMechanic(Mechanics mechanic) const; bool HasEffectMechanic(Mechanics mechanic) const;
bool HasAura(AuraType aura) const; bool HasAura(AuraType aura) const;
/// Summon mount aura (SPELL_AURA_MOUNTED) with a non-zero base cast time from SpellCastTimes.dbc.
/// Used by Fractured mount rules: castable while moving, never in combat, interrupted on combat enter.
bool IsCastTimeRidingMountSpell() const;
bool HasAnyAura() const; bool HasAnyAura() const;
bool HasAreaAuraEffect() const; bool HasAreaAuraEffect() const;
bool HasOnlyDamageEffects() const; bool HasOnlyDamageEffects() const;
@@ -5427,11 +5427,13 @@ void SpellMgr::LoadSpellInfoCorrections()
2457, // Battle Stance 2457, // Battle Stance
71, // Defensive Stance 71, // Defensive Stance
2458, // Berserker Stance 2458, // Berserker Stance
951010, 951011, 951012, // Paragon advancement warrior stance clones
// Death Knight Presences. // Death Knight Presences.
48266, // Blood Presence 48266, // Blood Presence
48263, // Frost Presence 48263, // Frost Presence
48265, // Unholy Presence 48265, // Unholy Presence
951013, 951014, 951015, // Paragon advancement DK presence clones (SpellFamily GENERIC in Spell.dbc)
// Hunter Aspects -- every rank, since AC stores the per-rank // Hunter Aspects -- every rank, since AC stores the per-rank
// SpellInfo as separate objects and `Category` lives on each. // SpellInfo as separate objects and `Category` lives on each.
@@ -5485,14 +5487,33 @@ void SpellMgr::LoadSpellInfoCorrections()
2457, // Battle Stance 2457, // Battle Stance
71, // Defensive Stance 71, // Defensive Stance
2458, // Berserker Stance 2458, // Berserker Stance
951010, 951011, 951012, // Paragon advancement warrior stance clones
48266, // Blood Presence 48266, // Blood Presence
48263, // Frost Presence 48263, // Frost Presence
48265, // Unholy Presence 48265, // Unholy Presence
951013, 951014, 951015, // Paragon advancement DK presence clones (SpellFamily GENERIC in Spell.dbc)
}, [](SpellInfo* spellInfo) }, [](SpellInfo* spellInfo)
{ {
spellInfo->AttributesEx6 &= ~SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE; spellInfo->AttributesEx6 &= ~SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE;
}); });
// Fractured / Paragon: advancement warrior stance clones — strip SPELL_ATTR1_NO_AURA_ICON
// (copied from stock 2457/71/2458). Stock Warrior stances intentionally hide from the default aura bar;
// these clones are meant to show a cancellable buff icon instead. Client Spell.dbc is patched in tandem via
// fractured-tooling/_patch_spell_dbc_paragon_stance_presence_clones.py.
ApplySpellFix({ 951010, 951011, 951012 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx &= ~SPELL_ATTR1_NO_AURA_ICON;
});
// Fractured / Paragon: advancement DK presence clones — strip SPELL_ATTR2_USE_SHAPESHIFT_BAR (0x10) copied
// from 48266/48263/48265. That client-only bit is what parks a spell on the secondary stance bar above the
// action bar; SkillLine / SpellFamily alone do not remove it. Spellbook tabs still come from SkillLines 770/771/772.
ApplySpellFix({ 951013, 951014, 951015 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx2 &= ~SPELL_ATTR2_USE_SHAPESHIFT_BAR;
});
// Fractured: strip reagent requirements from every player-class spell at // Fractured: strip reagent requirements from every player-class spell at
// load time. Filtered by SpellFamilyName != 0 so that profession spells // load time. Filtered by SpellFamilyName != 0 so that profession spells
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking, // (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
+3 -2
View File
@@ -163,7 +163,7 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<bool>(CONFIG_ALLOW_PLAYER_COMMANDS, "AllowPlayerCommands", 1); SetConfigValue<bool>(CONFIG_ALLOW_PLAYER_COMMANDS, "AllowPlayerCommands", 1);
SetConfigValue<bool>(CONFIG_PRESERVE_CUSTOM_CHANNELS, "PreserveCustomChannels", false); SetConfigValue<bool>(CONFIG_PRESERVE_CUSTOM_CHANNELS, "PreserveCustomChannels", false);
SetConfigValue<uint32>(CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION, "PreserveCustomChannelDuration", 14); SetConfigValue<uint32>(CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION, "PreserveCustomChannelDuration", 14);
SetConfigValue<uint32>(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 900000); SetConfigValue<uint32>(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 300000);
SetConfigValue<uint32>(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0); SetConfigValue<uint32>(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0);
SetConfigValue<bool>(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true); SetConfigValue<bool>(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true);
SetConfigValue<bool>(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true); SetConfigValue<bool>(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true);
@@ -270,7 +270,8 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<uint32>(CONFIG_INSTANCE_RESET_TIME_RELATIVE_TIMESTAMP, "Instance.ResetTimeRelativeTimestamp", 1135814400); SetConfigValue<uint32>(CONFIG_INSTANCE_RESET_TIME_RELATIVE_TIMESTAMP, "Instance.ResetTimeRelativeTimestamp", 1135814400);
SetConfigValue<uint32>(CONFIG_INSTANCE_UNLOAD_DELAY, "Instance.UnloadDelay", 1800000); SetConfigValue<uint32>(CONFIG_INSTANCE_UNLOAD_DELAY, "Instance.UnloadDelay", 1800000);
SetConfigValue<uint32>(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 2); // WotLK has 11 primary profession skill lines (gathering + crafting); secondary (Cooking, Fishing, First Aid) are not limited here.
SetConfigValue<uint32>(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 11);
SetConfigValue<uint32>(CONFIG_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value <= 9; }, "<= 9"); SetConfigValue<uint32>(CONFIG_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value <= 9; }, "<= 9");
SetConfigValue<uint32>(CONFIG_GM_LOGIN_STATE, "GM.LoginState", 2); SetConfigValue<uint32>(CONFIG_GM_LOGIN_STATE, "GM.LoginState", 2);
+62
View File
@@ -16,6 +16,7 @@
*/ */
#include "CommandScript.h" #include "CommandScript.h"
#include "DBCStores.h"
#include "Language.h" #include "Language.h"
#include "ObjectMgr.h" #include "ObjectMgr.h"
#include "Pet.h" #include "Pet.h"
@@ -51,6 +52,7 @@ public:
{ "default", HandleLearnAllDefaultCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_DEFAULT, Console::No }, { "default", HandleLearnAllDefaultCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_DEFAULT, Console::No },
{ "lang", HandleLearnAllLangCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_LANG, Console::No }, { "lang", HandleLearnAllLangCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_LANG, Console::No },
{ "recipes", HandleLearnAllRecipesCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_RECIPES, Console::No }, { "recipes", HandleLearnAllRecipesCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_RECIPES, Console::No },
{ "mounts", HandleLearnAllMountsCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS, Console::No },
}; };
static ChatCommandTable learnCommandTable = static ChatCommandTable learnCommandTable =
@@ -386,6 +388,66 @@ public:
return true; return true;
} }
static void SetRidingSkillToMaxForPlayer(Player* player)
{
SkillRaceClassInfoEntry const* rcInfo = GetSkillRaceClassInfo(SKILL_RIDING, player->getRace(), player->getClass());
if (!rcInfo || GetSkillRangeType(rcInfo) != SKILL_RANGE_RANK)
return;
SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID);
if (!tier)
return;
uint8 rank = 0;
uint16 maxValue = 0;
for (uint8 i = 0; i < MAX_SKILL_STEP; ++i)
{
if (tier->Value[i] == 0)
continue;
rank = i + 1;
maxValue = tier->Value[i];
}
if (!rank || !maxValue)
return;
player->SetSkill(SKILL_RIDING, rank, maxValue, maxValue);
}
static bool HandleLearnAllMountsCommand(ChatHandler* handler)
{
Player* target = handler->getSelectedPlayer();
if (!target)
{
handler->SendSysMessage(LANG_PLAYER_NOT_FOUND);
return false;
}
SetRidingSkillToMaxForPlayer(target);
handler->PSendSysMessage("Set Riding skill to maximum for {}.", handler->GetNameLink(target));
uint32 learned = 0;
for (uint32 i = 0; i < sSpellMgr->GetSpellInfoStoreSize(); ++i)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(i);
if (!spellInfo || !SpellMgr::IsSpellValid(spellInfo))
continue;
if (!spellInfo->HasAura(SPELL_AURA_MOUNTED))
continue;
if (target->HasSpell(i))
continue;
target->learnSpell(i, false);
if (target->HasSpell(i))
++learned;
}
handler->PSendSysMessage("Learned {} mount spell(s) for {}.", learned, handler->GetNameLink(target));
return true;
}
static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId) static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId)
{ {
uint32 classmask = player->getClassMask(); uint32 classmask = player->getClassMask();
+62 -17
View File
@@ -66,6 +66,9 @@ enum DeathKnightSpells
SPELL_DK_ITEM_T8_MELEE_4P_BONUS = 64736, SPELL_DK_ITEM_T8_MELEE_4P_BONUS = 64736,
SPELL_DK_MASTER_OF_GHOULS = 52143, SPELL_DK_MASTER_OF_GHOULS = 52143,
SPELL_DK_BLOOD_PLAGUE = 55078, SPELL_DK_BLOOD_PLAGUE = 55078,
// Fractured / Paragon: stock Priest Devouring Plague vs Character Advancement multidot clone
SPELL_PRIEST_DEVOURING_PLAGUE_R1 = 2944,
SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1 = 951000,
SPELL_DK_RAISE_DEAD_USE_REAGENT = 48289, SPELL_DK_RAISE_DEAD_USE_REAGENT = 48289,
SPELL_DK_RUNIC_POWER_ENERGIZE = 49088, SPELL_DK_RUNIC_POWER_ENERGIZE = 49088,
SPELL_DK_SCENT_OF_BLOOD = 50422, SPELL_DK_SCENT_OF_BLOOD = 50422,
@@ -107,6 +110,10 @@ enum DeathKnightSpells
SPELL_DK_RUNE_STRIKE_OFF_HAND_R1 = 66217, SPELL_DK_RUNE_STRIKE_OFF_HAND_R1 = 66217,
SPELL_DK_BLOOD_STRIKE_OFF_HAND_R1 = 66215, SPELL_DK_BLOOD_STRIKE_OFF_HAND_R1 = 66215,
SPELL_DK_KILLING_MACHINE = 51124, SPELL_DK_KILLING_MACHINE = 51124,
// Fractured / Paragon: Character Advancement DK presence clones (SpellFamily GENERIC in Spell.dbc).
SPELL_PARAGON_ADV_BLOOD_PRESENCE = 951013,
SPELL_PARAGON_ADV_FROST_PRESENCE = 951014,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE = 951015,
}; };
enum DeathKnightSpellIcons enum DeathKnightSpellIcons
@@ -126,6 +133,21 @@ enum Misc
NPC_RISEN_ALLY = 30230 NPC_RISEN_ALLY = 30230
}; };
inline bool Fractured_UnitHasBloodPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_BLOOD_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_BLOOD_PRESENCE);
}
inline bool Fractured_UnitHasFrostPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_FROST_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_FROST_PRESENCE);
}
inline bool Fractured_UnitHasUnholyPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_UNHOLY_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_UNHOLY_PRESENCE);
}
// 50526 - Wandering Plague // 50526 - Wandering Plague
class spell_dk_wandering_plague : public SpellScript class spell_dk_wandering_plague : public SpellScript
{ {
@@ -1797,8 +1819,11 @@ class spell_dk_improved_blood_presence : public AuraScript
return ValidateSpellInfo( return ValidateSpellInfo(
{ {
SPELL_DK_BLOOD_PRESENCE, SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE, SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE, SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED
}); });
} }
@@ -1806,14 +1831,14 @@ class spell_dk_improved_blood_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (target->HasAnyAuras(SPELL_DK_FROST_PRESENCE, SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED)) if (target->HasAnyAuras(SPELL_DK_FROST_PRESENCE, SPELL_DK_UNHOLY_PRESENCE, SPELL_PARAGON_ADV_FROST_PRESENCE, SPELL_PARAGON_ADV_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT1, aurEff->GetAmount(), target, true, nullptr, aurEff); target->CastCustomSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT1, aurEff->GetAmount(), target, true, nullptr, aurEff);
} }
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target->HasAura(SPELL_DK_BLOOD_PRESENCE)) if (!Fractured_UnitHasBloodPresenceAura(target))
target->RemoveAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED); target->RemoveAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED);
} }
@@ -1834,8 +1859,11 @@ class spell_dk_improved_frost_presence : public AuraScript
return ValidateSpellInfo( return ValidateSpellInfo(
{ {
SPELL_DK_BLOOD_PRESENCE, SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE, SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE, SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_FROST_PRESENCE_TRIGGERED SPELL_DK_FROST_PRESENCE_TRIGGERED
}); });
} }
@@ -1843,14 +1871,14 @@ class spell_dk_improved_frost_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED)) if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_UNHOLY_PRESENCE, SPELL_PARAGON_ADV_BLOOD_PRESENCE, SPELL_PARAGON_ADV_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_FROST_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff); target->CastCustomSpell(SPELL_DK_FROST_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff);
} }
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target->HasAura(SPELL_DK_FROST_PRESENCE)) if (!Fractured_UnitHasFrostPresenceAura(target))
target->RemoveAura(SPELL_DK_FROST_PRESENCE_TRIGGERED); target->RemoveAura(SPELL_DK_FROST_PRESENCE_TRIGGERED);
} }
@@ -1871,8 +1899,11 @@ class spell_dk_improved_unholy_presence : public AuraScript
return ValidateSpellInfo( return ValidateSpellInfo(
{ {
SPELL_DK_BLOOD_PRESENCE, SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE, SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE, SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED,
SPELL_DK_UNHOLY_PRESENCE_TRIGGERED SPELL_DK_UNHOLY_PRESENCE_TRIGGERED
}); });
@@ -1881,14 +1912,14 @@ class spell_dk_improved_unholy_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (target->HasAura(SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED)) if (Fractured_UnitHasUnholyPresenceAura(target) && !target->HasAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED))
{ {
// Not listed as any effect, only base points set in dbc // Not listed as any effect, only base points set in dbc
int32 basePoints = GetSpellInfo()->Effects[EFFECT_1].CalcValue(); int32 basePoints = GetSpellInfo()->Effects[EFFECT_1].CalcValue();
target->CastCustomSpell(target, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED, &basePoints, &basePoints, &basePoints, true, nullptr, aurEff); target->CastCustomSpell(target, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED, &basePoints, &basePoints, &basePoints, true, nullptr, aurEff);
} }
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_FROST_PRESENCE) && !target->HasAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED)) if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_FROST_PRESENCE, SPELL_PARAGON_ADV_BLOOD_PRESENCE, SPELL_PARAGON_ADV_FROST_PRESENCE) && !target->HasAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff); target->CastCustomSpell(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff);
} }
@@ -1898,7 +1929,7 @@ class spell_dk_improved_unholy_presence : public AuraScript
target->RemoveAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED); target->RemoveAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED);
if (!target->HasAura(SPELL_DK_UNHOLY_PRESENCE)) if (!Fractured_UnitHasUnholyPresenceAura(target))
target->RemoveAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED); target->RemoveAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED);
} }
@@ -1939,12 +1970,12 @@ class spell_dk_pestilence : public SpellScript
// and Unit::GetDiseasesByCaster already counts it for Paragon callers // and Unit::GetDiseasesByCaster already counts it for Paragon callers
// (see Unit.cpp), so it is conceptually a disease; stock Pestilence // (see Unit.cpp), so it is conceptually a disease; stock Pestilence
// just hard-codes Blood Plague + Frost Fever and so silently drops it. // just hard-codes Blood Plague + Frost Fever and so silently drops it.
// GetAuraOfRankedSpell with the rank-1 id (2944) covers every rank of // GetAuraOfRankedSpell with the rank-1 id (2944 / 951000) covers every rank of
// Devouring Plague the player has on the target -- we re-cast that // Devouring Plague the player has on the target -- we re-cast that
// exact same rank so the spread copy carries the caster's actual // exact same rank so the spread copy carries the caster's actual
// damage tier rather than always rank 1. Stock DKs cannot cast // damage tier rather than always rank 1. Stock DKs cannot cast
// Devouring Plague at all, so the GetAuraOfRankedSpell will return // Devouring Plague at all, so both lookups return null for them and
// null for them and this branch is a no-op there. // this branch is a no-op there.
bool const paragonSpread = IsParagonWildcardCaller(caster); bool const paragonSpread = IsParagonWildcardCaller(caster);
// Spread on others // Spread on others
@@ -1958,10 +1989,16 @@ class spell_dk_pestilence : public SpellScript
if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID())) if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID()))
caster->CastSpell(hitUnit, SPELL_DK_FROST_FEVER, true); caster->CastSpell(hitUnit, SPELL_DK_FROST_FEVER, true);
// Fractured / Paragon: Devouring Plague spread. // Fractured / Paragon: Devouring Plague spread (stock 2944 chain or
// Character Advancement multidot clone 951000 chain).
if (paragonSpread) if (paragonSpread)
if (Aura const* dp = target->GetAuraOfRankedSpell(2944 /* Devouring Plague r1 */, caster->GetGUID())) {
Aura const* dp = target->GetAuraOfRankedSpell(SPELL_PRIEST_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (!dp)
dp = target->GetAuraOfRankedSpell(SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (dp)
caster->CastSpell(hitUnit, dp->GetId(), true); caster->CastSpell(hitUnit, dp->GetId(), true);
}
} }
// Refresh on target // Refresh on target
else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE)) else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE))
@@ -1985,8 +2022,13 @@ class spell_dk_pestilence : public SpellScript
// Fractured / Paragon: Devouring Plague Glyph-of-Disease refresh. // Fractured / Paragon: Devouring Plague Glyph-of-Disease refresh.
if (paragonSpread) if (paragonSpread)
if (Aura* dp = target->GetAuraOfRankedSpell(2944 /* Devouring Plague r1 */, caster->GetGUID())) {
Aura* dp = target->GetAuraOfRankedSpell(SPELL_PRIEST_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (!dp)
dp = target->GetAuraOfRankedSpell(SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (dp)
dp->RefreshDuration(); dp->RefreshDuration();
}
} }
} }
@@ -2010,6 +2052,9 @@ class spell_dk_presence : public AuraScript
SPELL_DK_BLOOD_PRESENCE, SPELL_DK_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE, SPELL_DK_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE, SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1, SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1,
SPELL_DK_IMPROVED_FROST_PRESENCE_R1, SPELL_DK_IMPROVED_FROST_PRESENCE_R1,
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1,
@@ -2024,7 +2069,7 @@ class spell_dk_presence : public AuraScript
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (GetId() == SPELL_DK_BLOOD_PRESENCE) if (GetId() == SPELL_DK_BLOOD_PRESENCE || GetId() == SPELL_PARAGON_ADV_BLOOD_PRESENCE)
target->CastSpell(target, SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, true); target->CastSpell(target, SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, true);
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1, EFFECT_0)) else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1, EFFECT_0))
if (!target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED)) if (!target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
@@ -2035,7 +2080,7 @@ class spell_dk_presence : public AuraScript
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (GetId() == SPELL_DK_FROST_PRESENCE) if (GetId() == SPELL_DK_FROST_PRESENCE || GetId() == SPELL_PARAGON_ADV_FROST_PRESENCE)
target->CastSpell(target, SPELL_DK_FROST_PRESENCE_TRIGGERED, true); target->CastSpell(target, SPELL_DK_FROST_PRESENCE_TRIGGERED, true);
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_FROST_PRESENCE_R1, EFFECT_0)) else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_FROST_PRESENCE_R1, EFFECT_0))
if (!target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED)) if (!target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
@@ -2046,12 +2091,12 @@ class spell_dk_presence : public AuraScript
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (GetId() == SPELL_DK_UNHOLY_PRESENCE) if (GetId() == SPELL_DK_UNHOLY_PRESENCE || GetId() == SPELL_PARAGON_ADV_UNHOLY_PRESENCE)
target->CastSpell(target, SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, true); target->CastSpell(target, SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, true);
if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1, EFFECT_0)) if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1, EFFECT_0))
{ {
if (GetId() == SPELL_DK_UNHOLY_PRESENCE) if (GetId() == SPELL_DK_UNHOLY_PRESENCE || GetId() == SPELL_PARAGON_ADV_UNHOLY_PRESENCE)
{ {
// Not listed as any effect, only base points set // Not listed as any effect, only base points set
int32 bp = impAurEff->GetSpellInfo()->Effects[EFFECT_1].CalcValue(); int32 bp = impAurEff->GetSpellInfo()->Effects[EFFECT_1].CalcValue();
@@ -115,6 +115,17 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
**Manual upload:** `bash scripts/upload-release-to-gitea.sh /path/to/files v1.0.0` with the same env vars as CI. **Manual upload:** `bash scripts/upload-release-to-gitea.sh /path/to/files v1.0.0` with the same env vars as CI.
**Legacy “bridge” after changing `baked-gitea-channel.js`:** Players still using the old Gitea URL only receive launcher updates from that host. Build **Windows + Linux** installers (e.g. download **Fractured launcher CI** artifacts, or run **`npm run pack:win`** / **`npm run pack:linux`**), put **`dist/`** contents in one folder if needed, then:
```bash
export GITEA_BASE_URL=http://your-old-host:port # legacy base, no trailing slash
export GITEA_TOKEN=... GITEA_OWNER=Dawnsorrow GITEA_REPO=Fractured-Distro
bash tools/fractured-launcher-electron/scripts/gitea-replace-launcher-only.sh \
tools/fractured-launcher-electron/dist latest
```
That script deletes only **`Fractured-Launcher*`**, **`latest.yml`** (only if you supply a new **`latest.yml`** in **`dist/`**), **`latest-linux.yml`** (only if supplied), **`*.blockmap`**, and **`builder-debug.yml`** on the release, then uploads the new files — **Wow.exe**, MPQs, and **`patch-manifest.json`** are left alone. If you only built Linux locally, merge **Windows CI `dist/`** files into the same folder first so **`latest.yml`** is not removed without a replacement. Use release tag **`latest`** if that is what **`release_tag`** points at.
### Sync did not run / Gitea unchanged — checklist ### Sync did not run / Gitea unchanged — checklist
1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag). 1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag).
@@ -130,6 +141,7 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
11. **`EBUSY` / file locked on Windows** — The client (or antivirus) may keep **`.MPQ`** files open. The launcher **retries** for a short window and downloads the new file **before** replacing the old one; if sync still fails, **exit WoW** (and any tool previewing that folder) and run **Download updates** again. 11. **`EBUSY` / file locked on Windows** — The client (or antivirus) may keep **`.MPQ`** files open. The launcher **retries** for a short window and downloads the new file **before** replacing the old one; if sync still fails, **exit WoW** (and any tool previewing that folder) and run **Download updates** again.
12. **`.bak-*` clutter** — When **Download updates** finishes without error, the launcher removes matching **`*.bak-YYYYMMDD-HHmmss`** files from earlier runs (same pattern it uses when replacing files). Failed syncs do not delete backups. 12. **`.bak-*` clutter** — When **Download updates** finishes without error, the launcher removes matching **`*.bak-YYYYMMDD-HHmmss`** files from earlier runs (same pattern it uses when replacing files). Failed syncs do not delete backups.
13. **Gitea still shows an old launcher version** — The sync workflow overlays **`tools/fractured-launcher-electron` from the default branch**, so **`package.json`** there defines the built version. Ensure launcher changes are **merged to `main`** before publishing the GitHub release (or re-run **Actions → Sync release to Gitea** after merging). Previously, **Fractured-Launcher\*** files **attached on the GitHub release** were merged too, which could leave **two** installer versions on Gitea; those assets are now skipped in favor of CI-only builds. 13. **Gitea still shows an old launcher version** — The sync workflow overlays **`tools/fractured-launcher-electron` from the default branch**, so **`package.json`** there defines the built version. Ensure launcher changes are **merged to `main`** before publishing the GitHub release (or re-run **Actions → Sync release to Gitea** after merging). Previously, **Fractured-Launcher\*** files **attached on the GitHub release** were merged too, which could leave **two** installer versions on Gitea; those assets are now skipped in favor of CI-only builds.
14. **Migrating `baked-gitea-channel.js` to a new host** — Publish **`gitea-replace-launcher-only.sh`** (see **Manual upload** above) on the **old** Gitea **`latest`** release so auto-update still works until clients move to the new URL.
### Private Gitea token for players ### Private Gitea token for players
@@ -4,10 +4,12 @@
* Production Gitea mirror (non-secret). Edit here and ship no inject script, * Production Gitea mirror (non-secret). Edit here and ship no inject script,
* no fractured-release-channel.json, no CI env needed for these fields. * no fractured-release-channel.json, no CI env needed for these fields.
* Token stays in env: GITEA_TOKEN or launcher.json gitea.token_env. * Token stays in env: GITEA_TOKEN or launcher.json gitea.token_env.
*
* Use origin only (no /releases path): API is {base_url}/api/v1/
* Web: https://git.hisora.dev/Dawnsorrow/Fractured-Distro/releases
*/ */
module.exports = { module.exports = {
// http:// kept as-is; bare host gets https in gitea-release.js base_url: 'https://git.hisora.dev',
base_url: 'http://brassnet.ddns.net:33983',
owner: 'Dawnsorrow', owner: 'Dawnsorrow',
repo: 'Fractured-Distro', repo: 'Fractured-Distro',
release_tag: 'latest', release_tag: 'latest',
@@ -1,6 +1,6 @@
{ {
"name": "fractured-launcher-electron", "name": "fractured-launcher-electron",
"version": "1.0.12", "version": "1.0.13",
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update", "description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
"main": "main.js", "main": "main.js",
"repository": { "repository": {
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Replace only launcher installer + latest.yml attachments on a Gitea release.
# Does NOT delete Wow.exe, MPQs, or patch-manifest — use this to publish a
# "bridge" build (e.g. 1.0.13 with new baked Gitea URL) on a legacy host while
# keeping game assets already on that release.
#
# Usage:
# export GITEA_BASE_URL=http://legacy-host:port # or https://...
# export GITEA_TOKEN=gta_...
# export GITEA_OWNER=Dawnsorrow
# export GITEA_REPO=Fractured-Distro
# ./gitea-replace-launcher-only.sh /path/to/electron/dist latest
#
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=release-sync-filters.sh
. "$SCRIPT_DIR/release-sync-filters.sh"
DIST_DIR="${1:?first arg: electron-builder dist directory (contains .exe / .AppImage / latest*.yml)}"
TAG="${2:?second arg: release tag (e.g. latest)}"
: "${GITEA_BASE_URL:?Set GITEA_BASE_URL}"
: "${GITEA_TOKEN:?Set GITEA_TOKEN}"
: "${GITEA_OWNER:?Set GITEA_OWNER}"
: "${GITEA_REPO:?Set GITEA_REPO}"
BASE="${GITEA_BASE_URL%/}"
API="$BASE/api/v1"
AUTH_H=(-H "Authorization: token ${GITEA_TOKEN}" -H "Accept: application/json")
TAG_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$TAG")
REL_JSON=$(mktemp)
trap 'rm -f "$REL_JSON"' EXIT
code=$(curl -sS -o "$REL_JSON" -w "%{http_code}" "${AUTH_H[@]}" \
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${TAG_ENC}")
if [ "$code" != "200" ]; then
echo "Gitea GET release by tag failed HTTP $code (release must already exist):" >&2
cat "$REL_JSON" >&2
exit 1
fi
rel_id=$(jq -r '.id' "$REL_JSON")
if [ -z "$rel_id" ] || [ "$rel_id" = "null" ]; then
echo "Could not resolve Gitea release id" >&2
exit 1
fi
should_delete_attachment() {
local l
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
case "$l" in
fractured-launcher*) return 0 ;;
*.blockmap) return 0 ;;
builder-debug.yml|builder-debug.yaml) return 0 ;;
esac
return 1
}
should_delete_yml_attachment() {
local l
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
case "$l" in
latest.yml) [ -f "$DIST_DIR/latest.yml" ] ;;
latest-linux.yml) [ -f "$DIST_DIR/latest-linux.yml" ] ;;
latest-mac.yml) [ -f "$DIST_DIR/latest-mac.yml" ] ;;
*) return 1 ;;
esac
}
while read -r line; do
[ -z "$line" ] && continue
aid=$(printf '%s' "$line" | cut -f1)
aname=$(printf '%s' "$line" | cut -f2-)
if should_delete_attachment "$aname" || should_delete_yml_attachment "$aname"; then
echo "Removing old attachment: $aname (id=$aid)"
curl -fsS -X DELETE "${AUTH_H[@]}" \
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets/${aid}" || true
fi
done < <(jq -r '(.attachments // .assets // [])[] | "\(.id)\t\(.name)"' "$REL_JSON")
shopt -s nullglob
upload_paths=()
for f in "$DIST_DIR"/Fractured-Launcher*.exe "$DIST_DIR"/Fractured-Launcher*.AppImage \
"$DIST_DIR"/latest.yml "$DIST_DIR"/latest-linux.yml "$DIST_DIR"/latest-mac.yml; do
[ -f "$f" ] || continue
bn=$(basename "$f")
if should_skip_gitea_upload "$bn"; then
continue
fi
upload_paths+=("$f")
done
if [ "${#upload_paths[@]}" -eq 0 ]; then
echo "No launcher files to upload under $DIST_DIR (expected Fractured-Launcher*.exe, *.AppImage, latest.yml, latest-linux.yml)." >&2
exit 1
fi
for f in "${upload_paths[@]}"; do
bn=$(basename "$f")
echo "Uploading $bn"
curl -fsS -X POST "${AUTH_H[@]}" \
-F "attachment=@${f}" \
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets"
done
echo "Done. Release $TAG (id=$rel_id): replaced ${#upload_paths[@]} launcher file(s); game assets left intact."