Spells as Code

posted in Raiding.Zone Devlog for project Raiding.Zone
Published July 01, 2023
Advertisement
cover

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:

executionFlow

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.

modules

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.

modules

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

1 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement