Jens
The BestDivine
LEVEL 3
165 XP
Greetings!
Welcome to a tutorial on TrinityCore's Spell System.
Today, you'll learn, from start to finish, how TrinityCore's spell system works and how you can begin fixing spells in your core!
As we all know, spells are probably the most important aspect of World of Warcraft aside from maybe movement. The game is completely dysfunctional if your character can't cast the abilities they want to cast!
Therefore, I've spent the last few years studying and understanding exactly how the spell system works so that I can resolve any and every spell issue I run into.
Let's start from the top with a breakdown:Welcome to a tutorial on TrinityCore's Spell System.
Today, you'll learn, from start to finish, how TrinityCore's spell system works and how you can begin fixing spells in your core!
As we all know, spells are probably the most important aspect of World of Warcraft aside from maybe movement. The game is completely dysfunctional if your character can't cast the abilities they want to cast!
Therefore, I've spent the last few years studying and understanding exactly how the spell system works so that I can resolve any and every spell issue I run into.
Prefix - Do keep in mind that there are quite a few steps when casting a spell, and I will be skimming through them. If you need more information on these, do not hesitate to reach out to me. This is meant to be a beginner tutorial. We need to first discuss how TrinityCore's client and server communicates with each other.
[Opcodes] - An opcode, or operation code, is a signal sent between the client and server that holds data. Every single game uses these operation codes to allow data to be sent from your local client to the game's server. The reason operation codes need to be used is, for example when you cast a spell, only the client knows what spell you just pressed. If you pressed "1" on your keyboard, the server doesn't know that. The client needs to communicate with the server by saying: "Hey, this client just pressed the key '1'". From then on, the server will handle the information that is stored and act appropriately.
[Opcode] - CMSG_CAST_SPELL
We start off by sending this message to our server (which is the core we compile and run, our worldserver.exe). This message includes a few details but what matters is it includes the button that you pressed. If we, say, press 1 on our keyboard, the client determines what spell this may be, for example Frost Bolt, in which case the CMSG_CAST_SPELL packet(the data being transferred from client to server) will include the spell ID of Frost Bolt.
[Function] - WorldSession::HandleCastSpellOpcode
This function is then run once the opcode is sent to the server. A few things are done here, for example building the SpellInfo object that includes everything the server needs to determine what the spell will do. For example, deal damage, stun, fear, etc. Next, it will check what targets the client is providing, if any. Following these things, a new object called Spell is created. If you're unfamiliar with objects in programming, I recommend doing some research on OOP - Object Oriented Programming, as I will be using this term "object" quite frequently.
C++:
Spell* spell = new Spell(caster, spellInfo, triggerFlag);
The previous function then sends a message to the client, telling the client to prepare to cast the spell suggested.
C++:
WorldPackets::Spells::SpellPrepare spellPrepare;
spellPrepare.ClientCastID = cast.Cast.CastID;
spellPrepare.ServerCastID = spell->m_castId;
SendPacket(spellPrepare.Write());
[Function] - Spell:
We're now getting into where the magic happens. A few things happen here so I will skim through them. First, the server will load all of the necessary data into memory that it needs. For example, the spell information, the client that cast the spell, the world state, etc. Following this, it will put the spell "state" into SPELL_STATE_PREPARING. What this does is tell our update function (the function that updates our game) that this spell is being prepared to be cast. Preparing includes casting. TrinityCore, when using the word "cast", means the spell is executing it's function, not the client is casting the spell for 1-2 seconds. The SPELL_STATE_PREPARING value is used to determine whether or not the spell is still being cast.
Once in this state, there are a few things that happen that I will skip over which include triggeredByAura and Spell Events. These are a bit more advanced and, if you need more information on them, feel free to ask in the comments. For all intents and purposes, events work exactly as you'd expect where the core runs certain events and this is how it keeps track.
Next, the core determines if the spell is disabled here. This will, then, cancel the spell and tell the client that the spell is not available. Following this, the server will then check if there is another spell already being cast. This is a server-side check as there is also a client-side check as well. Next, the server loads into memory the SpellScripts which we will discuss at the end.
C++:
if ((_triggeredCastFlags & TRIGGERED_CAST_DIRECTLY) && (!m_spellInfo->IsChanneled() || !m_spellInfo->GetMaxDuration()))
cast(true);
else
{
// commented out !m_spellInfo->StartRecoveryTime, it forces instant spells with global cooldown to be processed in spell::update
// as a result a spell that passed CheckCast and should be processed instantly may suffer from this delayed process
// the easiest bug to observe is LoS check in AddUnitTarget, even if spell passed the CheckCast LoS check the situation can change in spell::update
// because target could be relocated in the meantime, making the spell fly to the air (no targets can be registered, so no effects processed, nothing in combat log)
bool willCastDirectly = !m_casttime && /*!m_spellInfo->StartRecoveryTime && */ GetCurrentContainer() == CURRENT_GENERIC_SPELL;
if (Unit* unitCaster = m_caster->ToUnit())
{
// stealth must be removed at cast starting (at show channel bar)
// skip triggered spell (item equip spell casting and other not explicit character casts/item uses)
if (!(_triggeredCastFlags & TRIGGERED_IGNORE_AURA_INTERRUPT_FLAGS) && !m_spellInfo->HasAttribute(SPELL_ATTR2_NOT_AN_ACTION))
unitCaster->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Action, m_spellInfo);
// Do not register as current spell when requested to ignore cast in progress
// We don't want to interrupt that other spell with cast time
if (!willCastDirectly || !(_triggeredCastFlags & TRIGGERED_IGNORE_CAST_IN_PROGRESS))
unitCaster->SetCurrentCastSpell(this);
}
SendSpellStart();
if (!(_triggeredCastFlags & TRIGGERED_IGNORE_GCD))
TriggerGlobalCooldown();
// Call CreatureAI hook OnSpellStart
if (Creature* caster = m_caster->ToCreature())
if (caster->IsAIEnabled())
caster->AI()->OnSpellStart(GetSpellInfo());
if (willCastDirectly)
cast(true);
}
[Function] - Spell::CheckCast
As you can probably guess, this is a server-side check to see if the client is allowed to cast this spell. The reason we check it on the server side is, if the client is compromised, the server is the only one that determines if this spell can be cast. This means that the client can't tell the server that the spell is castable. This function checks many things such as: if it's on cooldown, if you're currently moving and it doesn't allow you to move while casting, etc.
C++:
// Check global cooldown
if (strict && !(_triggeredCastFlags & TRIGGERED_IGNORE_GCD) && HasGlobalCooldown())
return !m_spellInfo->HasAttribute(SPELL_ATTR0_COOLDOWN_ON_EVENT) ? SPELL_FAILED_NOT_READY : SPELL_FAILED_DONT_REPORT;
This opcode is then sent at the end of our "prepare" function. This opcode tells the client that the spell is indeed castable and to start casting. This opcode is only sent if the spell has a cast time. This does not include channeled spells, which are simply buffs/debuffs disguised as spells.
C++:
WorldPackets::Spells::SpellStart packet;
WorldPackets::Spells::SpellCastData& castData = packet.Cast;
[Client] - From here, the client is now casting the spell. If the spell has a 2 second casttime, the "Cast Bar" will show up and the player will be casting for two seconds. All other players will also see that this player is casting the spell.
At the same time, the server is keeping track of this cast time inside of the Spell::Update function. This function starts a timer on the spell, if applicable, once the opcode is sent to the client. So, the client doesn't keep track of when the spell cast is finished, the server does. This prevents exploits such as Instant Cast hacks. If the client completes the cast before the server does, it doesn't matter. The server is what keeps track for the client.
C++:
case SPELL_STATE_PREPARING:
{
if (m_timer > 0)
{
if (difftime >= (uint32)m_timer)
m_timer = 0;
else
m_timer -= difftime;
}
if (m_timer == 0 && !m_spellInfo->IsNextMeleeSwingSpell())
// don't CheckCast for instant spells - done in spell::prepare, skip duplicate checks, needed for range checks for example
cast(!m_casttime);
break;
}
Once the cast is finished, this opcode is sent. As you can imagine, this tells the clients in the vicinity, along with the client who casted the spell, that the spell cast has been completed and everything can run smoothly. Keep in mind, the server will check to ensure that the player can still cast the spell given the situation and ensures the targets still exist. If the target or player dies during the cast, for example, the cast is canceled.
Once this opcode is run, the server calculates what the spell will do, how much damage is done, who the targets are, etc.
This concludes part 1.
Part 2 will be out ASAP. Keep in mind the TC spell system is vast and complicated and includes many different aspects.
Topics for next time: SpellScripts, Auras, AuraEffects, SpellEffects, etc.
For more information, see https://github.com/TrinityCore/TrinityCore
Thanks for reading, +1 Like if enjoyed!