My first contact with World of Warcraft was through a private server. I didn’t have the money for an abo yet and had some friends playing there.
I was fascinated by the idea of my own private WoW server. After weeks of fiddling I finally got it running.
I learned a lot about servers, networks, and especially the technical design of World of Warcraft.
WoW Spell implementation
Disclaimer: I base my whole argumentation on the code of a WoW private server on the patch level of Wrath of the Lich King. I don’t make any assumptions about the current WoW code base.
WoW stores all items and spells in a gigantic database. For example, there is a table that contains all spells. It has columns for everything an ability can do.
# https://raw.githubusercontent.com/azerothcore/azerothcore-wotlk/master/data/sql/base/db_world/spell_dbc.sql
CREATE TABLE IF NOT EXISTS `spell_dbc` (
`ID` int NOT NULL DEFAULT '0',
`Category` int unsigned NOT NULL DEFAULT '0',
`DispelType` int unsigned NOT NULL DEFAULT '0',
`Mechanic` int unsigned NOT NULL DEFAULT '0',
`Attributes` int unsigned NOT NULL DEFAULT '0',
`AttributesEx` int unsigned NOT NULL DEFAULT '0',
`AttributesEx2` int unsigned NOT NULL DEFAULT '0',
`AttributesEx3` int unsigned NOT NULL DEFAULT '0',
`AttributesEx4` int unsigned NOT NULL DEFAULT '0',
`AttributesEx5` int unsigned NOT NULL DEFAULT '0',
`AttributesEx6` int unsigned NOT NULL DEFAULT '0',
`AttributesEx7` int unsigned NOT NULL DEFAULT '0',
`ShapeshiftMask` bigint unsigned NOT NULL DEFAULT '0',
`unk_320_2` int NOT NULL DEFAULT '0',
`ShapeshiftExclude` bigint unsigned NOT NULL DEFAULT '0',
`unk_320_3` int NOT NULL DEFAULT '0',
`Targets` int unsigned NOT NULL DEFAULT '0',
`TargetCreatureType` int unsigned NOT NULL DEFAULT '0',
`RequiresSpellFocus` int unsigned NOT NULL DEFAULT '0',
`FacingCasterFlags` int unsigned NOT NULL DEFAULT '0',
`CasterAuraState` int unsigned NOT NULL DEFAULT '0',
`TargetAuraState` int unsigned NOT NULL DEFAULT '0',
`ExcludeCasterAuraState` int unsigned NOT NULL DEFAULT '0',
`ExcludeTargetAuraState` int unsigned NOT NULL DEFAULT '0',
`CasterAuraSpell` int unsigned NOT NULL DEFAULT '0',
`TargetAuraSpell` int unsigned NOT NULL DEFAULT '0',
`ExcludeCasterAuraSpell` int unsigned NOT NULL DEFAULT '0',
`ExcludeTargetAuraSpell` int unsigned NOT NULL DEFAULT '0',
`CastingTimeIndex` int unsigned NOT NULL DEFAULT '0',
`RecoveryTime` int unsigned NOT NULL DEFAULT '0',
`CategoryRecoveryTime` int unsigned NOT NULL DEFAULT '0',
`InterruptFlags` int unsigned NOT NULL DEFAULT '0',
`AuraInterruptFlags` int unsigned NOT NULL DEFAULT '0',
`ChannelInterruptFlags` int unsigned NOT NULL DEFAULT '0',
`ProcTypeMask` int unsigned NOT NULL DEFAULT '0',
`ProcChance` int unsigned NOT NULL DEFAULT '0',
`ProcCharges` int unsigned NOT NULL DEFAULT '0',
`MaxLevel` int unsigned NOT NULL DEFAULT '0',
`BaseLevel` int unsigned NOT NULL DEFAULT '0',
`SpellLevel` int unsigned NOT NULL DEFAULT '0',
`DurationIndex` int unsigned NOT NULL DEFAULT '0',
`PowerType` int NOT NULL DEFAULT '0',
`ManaCost` int unsigned NOT NULL DEFAULT '0',
`ManaCostPerLevel` int unsigned NOT NULL DEFAULT '0',
`ManaPerSecond` int unsigned NOT NULL DEFAULT '0',
`ManaPerSecondPerLevel` int unsigned NOT NULL DEFAULT '0',
`RangeIndex` int unsigned NOT NULL DEFAULT '0',
`Speed` float NOT NULL DEFAULT '0',
`ModalNextSpell` int unsigned NOT NULL DEFAULT '0',
`CumulativeAura` int unsigned NOT NULL DEFAULT '0',
`Totem_1` int unsigned NOT NULL DEFAULT '0',
`Totem_2` int unsigned NOT NULL DEFAULT '0',
`Reagent_1` int NOT NULL DEFAULT '0',
...
`EffectChainAmplitude_2` float NOT NULL DEFAULT '0',
`EffectChainAmplitude_3` float NOT NULL DEFAULT '0',
`MinFactionID` int unsigned NOT NULL DEFAULT '0',
`MinReputation` int unsigned NOT NULL DEFAULT '0',
`RequiredAuraVision` int unsigned NOT NULL DEFAULT '0',
`RequiredTotemCategoryID_1` int unsigned NOT NULL DEFAULT '0',
`RequiredTotemCategoryID_2` int unsigned NOT NULL DEFAULT '0',
`RequiredAreasID` int NOT NULL DEFAULT '0',
`SchoolMask` int unsigned NOT NULL DEFAULT '0',
`RuneCostID` int unsigned NOT NULL DEFAULT '0',
`SpellMissileID` int unsigned NOT NULL DEFAULT '0',
`PowerDisplayID` int NOT NULL DEFAULT '0',
`EffectBonusMultiplier_1` float NOT NULL DEFAULT '0',
`EffectBonusMultiplier_2` float NOT NULL DEFAULT '0',
`EffectBonusMultiplier_3` float NOT NULL DEFAULT '0',
`SpellDescriptionVariableID` int unsigned NOT NULL DEFAULT '0',
`SpellDifficultyID` int unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`) USING BTREE
)
The original unshortened CREATE TABLE
statement has 237 lines. Even then, you have restrictions on how many special effects, buffs, and attributes you can modify.
The problem is that there are some effects you cannot implement with fixed columns.
Some effects might need to react to dynamic inputs, or they want to use four auras instead of three.
WoW solves this problem by referencing functions of the game in the database.
These functions are for special effects on abilities and items like Val’anyr. They are implemented in C++ and called from the game server. Here is an example of the code from the Val’anyr proc:
// https://github.com/azerothcore/TrinityCore/blob/a4a266ed2c3cbc0e32c601256ea55f2f26ef077f/src/server/scripts/Spells/spell_item.cpp#L422
void HandleProc(AuraEffect const* aurEff, ProcEventInfo& eventInfo)
{
PreventDefaultAction();
HealInfo* healInfo = eventInfo.GetHealInfo();
if (!healInfo || !healInfo->GetHeal())
return;
int32 absorb = int32(CalculatePct(healInfo->GetHeal(), 15.0f));
if (AuraEffect* protEff = eventInfo.GetProcTarget()->GetAuraEffect(SPELL_PROTECTION_OF_ANCIENT_KINGS, EFFECT_0, eventInfo.GetActor()->GetGUID()))
{
// The shield can grow to a maximum size of 20,000 damage absorbtion
protEff->SetAmount(std::min<int32>(protEff->GetAmount() + absorb, 20000));
// Refresh and return to prevent replacing the aura
protEff->GetBase()->RefreshDuration();
}
else
GetTarget()->CastCustomSpell(SPELL_PROTECTION_OF_ANCIENT_KINGS, SPELLVALUE_BASE_POINT0, absorb, eventInfo.GetProcTarget(), true, nullptr, aurEff);
}
This code is hard to read for me. But this could be just C++ and pointer magic.
I am more concerned that the information about a spell is scattered among the entire code base. You find some stuff in the database and other stuff implemented in code.
That makes it hard to reason about the entire system.
Implement spells as code
For Raiding.Zone I tried a different approach.
I don’t need a database and will just put all information about abilities, buffs, and items in the codebase.
If you are searching for the implementation of spell, there should be only one location to look at.
We will start with the simple ability Fireball
class Fireball : Ability {
// All interfaces and types are available in this gist:
// https://gist.github.com/klg71/89adffc2a90352639ed338ee7a84542a
companion object : AbilityCompanion {
override fun id(): UUID = UUID.fromString("cefab9cd-ade1-466c-a122-76442263e43f")
override fun name() = "Fireball"
override fun description() = "Does 10 damage"
}
override fun static() = Fireball
override fun resourceType() = ResourceType.MANA
override fun resourceAmount(source: Target?) = 20L
// cast-time in milliseconds
override fun castTime(source: Target?) = 1500L
// list of effects on the target
override fun effect(source: Target, target: Target) = AbilityEffect(listOf(ResourceChange.magicDamage(10)))
}
It does 10 damage to your target after 1.5s and consumes 20 mana.
A Fireball alone is boring, so we want to add a Debuff. Every Fireball shall also apply an Ablaze
-Debuff, that deals damage over time.
class AblazeBuff : BuffAbility {
companion object : BuffAbilityCompanion {
override fun id(): UUID = UUID.fromString("d65be02c-4d99-477b-ab14-30a9710747a7")
override fun name() = "Ablaze"
override fun description() = "Deals 5 damage each tick"
override fun initialTime(): Long = 3000
}
override fun static(): BuffAbilityCompanion = AblazeBuff
override fun tickDelay() = 600L
override fun effect(effectType: BuffEffectType, buffHolderId: UUID, buff: Buff, gameState: GameState): List<BuffEffectOnTarget> {
// We have 3 possible effect-types: APPLY, TICK and REMOVE
if (effectType == BuffEffectType.TICK) {
val source = gameState.target(buff.sourceId) ?: return emptyList()
return listOf(
BuffEffectOnTarget(buffHolderId to BuffEffect(listOf(ResourceChange.magicDamage(5))))
)
}
return emptyList()
}
}
Ablaze
deals 5 damage every 600ms for 3000 ms. We will now modify the Fireball.effect()
method so that it also applies the Ablaze
-Debuff.
override fun effect(source: Target, target: Target) = AbilityEffect(
listOf(ResourceChange.magicDamage(10)),
newBuffs=listOf(NewBuff(AblazeBuff))
)
We are using the companion object
of AblazeBuff
. Companion objects in Kotlin are similar to Java’s static classes.
You can think of it as a type definition with some compile-time properties. Instead of using the UUID or the name, we will directly reference this type if we need to apply a new buff.
In our example, the NewBuff-class expects a buff. So we use the AblazeBuff-companion.
These companions exist for Buffs, Abilities (if you want to reset the Cooldown of another ability), Pets, and AreaEffects.
Whenever you need to reference another effect, you can just use the static companion.
A typo in AblazeBuff
would directly cause a compile-time error.
That eliminates a whole category of spelling and referencing problems.
With all information in the code, you can refactor names and ids easily.
You can also jump directly to the implementation of the reference Buff.
So you can look at the implementation of Fireball
, and if you want to know how the Ablaze
buff works, you can instantly jump to it.
With this approach, you can write test code for your abilities. But I admit that I didn’t write lots of tests yet.
In this graph, you can see the general control flow for spells:
The database has one gigantic advantage. You can query it easily and performant.
But for buffs and abilities, you only need to do it once when the server starts up.
I use reflection to find all implementations for Ability
and cache them in a Map.
The library org.reflections:reflections
is pretty good for this job.
Spells in a database have another advantage. You can modify them at runtime. You could dynamically create new spells and have them available without restarting the server.
Theoretically, the JVM can also load classes at runtime. But it is really tedious to implement cleanly.
I have decided to ignore this use case till I need it.
Module Structure for spells
So we solved the problem of having the effect implementation scattered around the codebase. But we still have a bunch of different abilities/buffs etc.
We don’t want to put them all in the same folder.
Instead, we want to isolate them from one another as much as we can.
I’m a big fan of the Information Hiding/Encapsulation principle.
Basically, it says that each class has only access to the information it needs.
It’s useful for reducing complexity.
For example, if you have a boss that throws Acid
at the player.
Now you might have a rogue class that also has an Acid
debuff.
You can put both in different modules and never worry about mixing up the debuffs.
In Raiding.Zone your weapon determines your skillset. The spells of a weapon interact with each other. But your cast won’t interact with an ability from another one. So we put each into a different module.
I’m using Gradle for my build process. Gradle lets you define modules and submodules conveniently. I have separated all weapons into different Gradle modules that don’t know each other. Only the game server and some tools will load all of them and cache them in catalogs.
The only problem is that the game server needs to reference each weapon separately.
I have some ideas about dynamically loading them, but I did not try out any of them.
Wrapping up
The type-safe spell implementation restrictive and modules make adding spells and levels trivial.
I can even imagine opening up the system sometime.
That could give mods and level designers an MMO engine to implement custom levels.
You can find most of the referenced code in this gist: https://gist.github.com/klg71/89adffc2a90352639ed338ee7a84542a
If you miss a part, feel free to write me at klg71@web.de