Skip to content

About

AzerothCore module: Vanilla-style spell batching (~400ms tick windows) for WotLK 3.3.5a — mutual kills, mutual CC, counterspell races and ghost charges

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mod-spell-batching

AzerothCore module that implements Vanilla-style spell batching (WoW 1.12 behavior), reproducing the ~400 ms tick window where all spell effects within the same tick resolve simultaneously.


Compatibility

Field Value
AzerothCore 3.0.0 — commit bf770c9 or later
WoW Client 3.3.5a (Wrath of the Lich King)
Platform Windows / Linux

This module was developed and tested against AzerothCore commit bf770c9. Later commits may require adjusting the offsets in core.patch.


What is Spell Batching?

On the original Vanilla WoW servers (patch 1.12), spells were not processed in real time. The server accumulated all spells cast during a window of approximately 400 milliseconds and resolved them all simultaneously at the end of that window.

This produced PvP interactions impossible in later versions of the game:

  • Mutual kill: two players kill each other simultaneously — both die.
  • Mutual CC: two crossed Polymorphs turn both players into sheep at the same time.
  • Counterspell race: two simultaneous Counterspells lock the school of both mages.
  • Ghost charge: a single-charge aura (Divine Favor, Hot Streak, etc.) can benefit multiple spells that complete their cast within the same tick.

In standard WotLK, the first packet to reach the server always wins. This module restores the Vanilla behavior.


Architecture

The module intercepts spells at three points in the spell lifecycle.

Point 1 — Opcode handler (cast start)

HandleCastSpellOpcode → QueueSpell() instead of spell->prepare()

When a player presses a spell, instead of executing it immediately, it is added to that player's queue. When the next batch tick arrives, the manager runs a two-phase cycle:

Phase 1 — VALIDATE
  For each spell in queue:
    CheckCast() against the current world state.
    If target is alive and in range → mark as valid.

Phase 2 — EXECUTE
  For each validated spell (in arrival order):
    spell->prepare(skipInitialCheck=true)
    The skip prevents a heal earlier in the batch from invalidating the next damage spell.

This ensures all spells in the same tick see the same world state at validation time, not the state altered by earlier spells in the same batch.

Point 2 — Spell::update() (effects of cast-time spells)

When a cast-time spell's timer reaches zero, the engine normally calls cast() immediately. The module defers that call until the next batch tick:

m_timer == 0 in Spell::update()
  → sSpellBatchMgr->ShouldBatchCast() == true
  → sSpellBatchMgr->QueueCast(this, skipCheck)   // defer
  // cast() is called in Phase 3 of the next batch tick

This means for a 2.5 s Frostbolt:

  • The cast bar starts immediately (the client is accurate).
  • The damage applies at the next batch tick boundary after the cast completes.

Point 3 — Phase 3 of the batch (cast queue)

Phase 3 — CAST EFFECTS
  For each spell in the cast queue:
    spell->cast(true)   // skipCheck=true — atomic, does not re-validate conditions
                        // that may have changed within the same tick

skipCheck=true is critical for mutual kills and mutual Polymorph: if the first spell in the tick kills or CCs the target of the second spell, the second still applies because its effects are part of the same atomic tick.

Fixed-interval tick

The accumulator always advances regardless of whether there are spells in queue:

_accumulator += diff;
if (_accumulator < _windowMs)
    return;
_accumulator %= _windowMs;   // preserve remainder — no accumulated drift

This produces a fixed server clock, not a rolling window that only runs when there are pending spells.


Ghost Charge

Some abilities are single-charge auras (spell mods with 1 charge): they are consumed when the player casts an eligible spell. With spell batching, if two eligible spells complete their cast within the same 400 ms tick, both see the charge as available. The charge is consumed exactly once at the end of the tick.

How it works internally

Charge consumption occurs in Player::RemoveSpellMods(), called at the end of Spell::cast(). During batch processing (_processing == true), instead of calling DropCharge() immediately, the module defers it:

Spell A cast() → RemoveSpellMods() → IsProcessing() == true → DeferChargeDrop(aura)
Spell B cast() → RemoveSpellMods() → IsProcessing() == true → DeferChargeDrop(aura) (already in set, ignored)
FlushDeferredChargeDrops() → aura->DropCharge() exactly once
_processing = false

Timing requirement

Both spells must complete their cast within the same 400 ms window:

400 ms window
├── t=0 ms    Spell A (cast-time) completes → enters cast queue
├── t=50 ms   Player presses Spell B (instant) → enters prepare queue
└── t=400 ms  BATCH TICK
              ├── Phase 2: Spell B fires → aura available → crit → DeferChargeDrop
              ├── Phase 3: Spell A fires → aura still available (deferred) → crit
              └── Flush: aura consumed exactly once → both critted

If Spell B is pressed much earlier (more than 400 ms before Spell A completes), they fall into different batches and the ghost charge does not apply — identical behavior to how Vanilla 1.12 worked.

Ghost Charges by class

Paladin

Aura Eligible spells Effect
Divine Favor Holy Light, Flash of Light, Holy Shock Both spells are guaranteed critical
Infusion of Light Flash of Light, Holy Light Both receive cast time reduction

Classic scenario — Divine Favor:

  1. Activate Divine Favor.
  2. Start casting Holy Light (2.5 s).
  3. Press Holy Shock in the last ~400 ms of the Holy Light cast.
  4. Both spells crit from the ghost charge.

Mage

Aura Eligible spells Effect
Presence of Mind Any spell with cast time Both become instant
Hot Streak Pyroblast Both Pyroblasts are instant
Brain Freeze Frostfire Bolt, Fireball Both are instant and free
Missile Barrage Arcane Missiles Both channels are instant and cheaper

Druid

Aura Eligible spells Effect
Clearcasting (Omen of Clarity) Any eligible spell Both free (no mana cost)
Nature's Grace Next spell with cast time Both receive −0.5 s cast time

Shaman

Aura Eligible spells Effect
Elemental Focus Next 2 spells Spells in the batch cost no mana
Tidal Waves Greater Healing Wave, Healing Wave Both receive cast time bonus

Priest

Aura Eligible spells Effect
Surge of Light Smite Both Smites are instant
Serendipity Greater Heal, Prayer of Healing Both receive cast time reduction stacks

Warlock

Aura Eligible spells Effect
Nightfall Shadow Bolt Both Shadow Bolts are instant
Backlash Shadow Bolt Both instant after taking a melee critical
Decimation Soul Fire Both Soul Fires with reduced cast time

Warrior

Aura Eligible spells Effect
Bloodsurge Slam Both Slams are instant
Sudden Death Execute Execute available outside 20 % HP for both
Taste for Blood Overpower Both Overpowers available

Hunter

Aura Eligible spells Effect
Lock and Load Explosive Trap, Immolation Trap Both charges preserved for both spells in the tick
Cobra Strikes Pet attacks Both attacks are guaranteed critical

Death Knight

Aura Eligible spells Effect
Rime / Freezing Fog Howling Blast Both free and instant
Sudden Doom Death Coil Both cost no runes
Killing Machine Frost Strike, Obliterate Both guaranteed critical

Rogue

Aura Eligible spells Effect
Ruthlessness Finishing moves Both generate an extra combo point
Relentless Strikes Finishing moves Both restore energy
Initiative Openers (Ambush, Garrote…) Both generate an extra combo point

PvP Interactions by class

The following interactions occur when two players use abilities against each other within the same 400 ms window. The "With batching" column describes the result this module produces.

Warrior

Scenario Without batching With batching
Charge vs Charge First one lands, second fails Both stunned simultaneously
Intercept vs Intercept First one applies Both apply the stun
Charge vs lethal damage Order decides who dies first Both effects apply in the same tick
Intimidating Shout vs Intimidating Shout First one fears the other Both players fear each other

Mage

Scenario Without batching With batching
Polymorph vs Polymorph First one lands, second fails Both turned into sheep simultaneously
Counterspell vs Counterspell First one locks the other's school Both school-locked simultaneously
Frostbolt vs Frostbolt First may kill before second lands Both damage instances apply in the same tick
Spellsteal vs Spellsteal First one steals the buff Both steal each other's buff

Paladin

Scenario Without batching With batching
Hammer of Justice vs HoJ First one stuns the other Both stunned simultaneously
Repentance vs Repentance First one CCs the other Both CCed simultaneously
Hammer of Wrath vs HoW Only one can execute Both can execute if both are below 20 % HP

Priest

Scenario Without batching With batching
Psychic Scream vs Psychic Scream First one fears the other Both fear each other simultaneously
Silence vs Silence First one silences the other Both silenced simultaneously

Shaman

Scenario Without batching With batching
Hex vs Hex First one hexes the other Both hexed simultaneously
Frost Shock vs Frost Shock First one slows the other Both slowed simultaneously

Druid

Scenario Without batching With batching
Cyclone vs Cyclone First one CCs the other Both Cycloned simultaneously
Entangling Roots vs Entangling Roots First one roots the other Both rooted simultaneously

Rogue

Scenario Without batching With batching
Kidney Shot vs Kidney Shot First one stuns the other Both stunned simultaneously
Blind vs Blind First one blinds the other Both blinded simultaneously
Kick vs Kick First one interrupts the other Both interrupted simultaneously

Death Knight

Scenario Without batching With batching
Death Grip vs Death Grip First one pulls the other Both displacement effects apply
Strangulate vs Strangulate First one silences the other Both silenced simultaneously

Healing vs damage (heal racing)

With spell batching, a heal and a damage spell that apply in the same tick resolve simultaneously. Both effects are validated before either executes, so the target's state at validation time determines whether spells are valid — not the state after the first spell applies.

Scenario Result
Player at 1 HP — lethal damage and a heal land in the same tick Both apply: if damage exceeds HP after the heal → dies; if heal exceeds damage → survives
Healer and DPS target the same player in the same tick Both effects apply without one invalidating the other
Two heals on the same target in the same tick Both heals apply (skipInitialCheck ensures the second heal is not rejected by state changed by the first)

What is batched and what is not

Spell type Batched? Reason
Player instant spells (non-triggered) ✅ Yes Opcode handler → prepare queue
Player cast-time spells — cast start ✅ Yes Opcode handler → prepare queue
Player cast-time spells — effects ✅ Yes Spell::update() → cast queue
Finishing moves, shots, player AoE ✅ Yes Normal player casts
Triggered spells / server procs ❌ No Not initiated by the client
Passive spells ❌ No Always-on auras, irrelevant
Channeled spells (Mind Flay, Blizzard…) ❌ No Own tick system
Auto Shot / Wand Shoot ❌ No Auto-repeat, own rhythm
Heroic Strike / Cleave / Maul (on-next-swing) ❌ No Tied to melee attack timer
Vehicle spells ❌ No Vehicle abilities, not direct player casts
Pet spells ❌ No Separate HandlePetCastSpellOpcode handler
Totem spells ❌ No Server-side AI
NPC spells ❌ No Not player input

Core patch

The core.patch file documents the changes required in the AzerothCore core. It touches 5 files:

File Change
Spell.h Adds skipInitialCheck parameter to prepare()
Spell.cpp Include, destructor cleanup, defer cast() in prepare() (instant) and update() (cast-time)
SpellHandler.cpp Queue spells in opcode handler; cancel from queue on CMSG_CANCEL_CAST
Unit.cpp Guard in InterruptSpell(): does not interrupt spells already in the cast queue (mutual Polymorph fix)
Player.cpp Guard in RemoveSpellMods(): defers charge consumption during batch processing (ghost charge)

Installation

  1. Copy this folder to modules/mod-spell-batching/ in your AzerothCore tree.

  2. Apply the core patch:

    git apply modules/mod-spell-batching/core.patch

    If offsets are stale, apply the hunks manually following the comments inside core.patch.

  3. Re-run CMake and build:

    mkdir -p build && cd build
    cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DMODULES=static
    make -j$(nproc) && make install
  4. Enable the module in the generated config (mod_spell_batching.conf):

    SpellBatch.Enable = 1
    SpellBatch.Window = 400

Configuration

Key Default Description
SpellBatch.Enable 0 Enable the module
SpellBatch.Window 400 Batch window in milliseconds
SpellBatch.MaxQueuePerPlayer 5 Max queued spells per player; extras fall through to normal cast
SpellBatch.Dedup 1 If the same spell is queued twice, replace the older entry
SpellBatch.Debug 0 Verbose per-spell log output (SpellBatch.Debug = 1 to enable)

Window recommendations

Use case Recommended window
Authentic Vanilla 1.12 feel 400 ms
WotLK server with competitive PvP 100–150 ms
PvE only Does not matter; 400 ms works fine

Testing

Mutual kill

  1. Two players in arena, both at 1 HP (.modify hp 1).
  2. Both press an instant damage spell at the same time.
  3. With batching: both die simultaneously.

Mutual Polymorph

  1. Two mages in arena.
  2. Both complete their Polymorph cast in the same 400 ms tick.
  3. With batching: both are turned into sheep simultaneously.

Ghost charge (Divine Favor)

  1. Activate Divine Favor.
  2. Start casting Holy Light.
  3. Press Holy Shock in the last ~400 ms of the cast.
  4. Both spells should crit.

Debug logs

Enable SpellBatch.Debug = 1 and look in Server.log:

SpellBatch: queued spell=12826 caster=... queueSize=1
SpellBatch: batch#N prepare: total=2 executed=2 rejected=0 expired=0
SpellBatch: cast-effect deferred spell=12826
SpellBatch: cast-effect deferred spell=12826
SpellBatch: batch#N+1 cast-effects fired=2

total=2 executed=2 and cast-effects fired=2 confirm that two spells resolved in the same tick.


Safety guarantees

  • No dangling pointers: Spell::~Spell() calls RemoveFromCastQueue(this), so a cancelled or interrupted spell can never be invoked from the cast queue.
  • Nulled before cast(): each cast queue entry is nulled before calling cast(), so if one spell's effects destroy another queued spell, that spell's destructor marks its entry null and the iterator skips it safely.
  • No re-batching: ShouldBatchCast() returns false while _processing == true, preventing spells fired inside the batch from being re-batched.
  • Full queue = fallback: if a player's queue is full (MaxQueuePerPlayer), the excess spell is cast normally rather than silently dropped.
  • Expiry: spells that have been in queue for more than Window * 2 ms are discarded to prevent stale spells from firing under severe lag conditions.
  • Atomic ghost charge: spell mod charge consumption is deferred until all effects in the batch have resolved, ensuring the set of consumed auras is consistent with the state at the start of the tick.

About

AzerothCore module: Vanilla-style spell batching (~400ms tick windows) for WotLK 3.3.5a — mutual kills, mutual CC, counterspell races and ghost charges

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages