Bloog Bot

Chapter 17
Drew Kestell, 2024
drew.kestell@gmail.com

Combat

This chapter won't introduce any new concepts. Instead we're going to expose a few new functions from the WoW client and bolster our combat state to use some of the new abilities we've unlocked for our warrior in the first 6 levels.

The first thing we want to do is improve our targeting logic in GrindState. Instead of looking for a specific enemy by name ("Plainstrider" is what I was using before), we ideally want to target anything killable within a reasonable level range. Exposing the Level of a unit is easy, so let's do that quick. Add this to WoWUnit:

const int LEVEL_OFFSET = 0x88;

public int Level => MemoryManager.ReadInt(GetDescriptorPtr() + LEVEL_OFFSET);

Now, add the following two enums to the project in the /Enums folder, and then we'll discuss them:

CreatureType.cs

public enum CreatureType
{
    Beast = 1,
    Dragonkin = 2,
    Demon = 3,
    Elemental = 4,
    Giant = 5,
    Undead = 6,
    Humanoid = 7,
    Critter = 8,
    Mechanical = 9,
    NotSpecified = 10,
    Totem = 11
}

UnitReaction.cs

public enum UnitReaction
{
    Hated,
    Hostile,
    Unfriendly,
    Neutral,
    Friendly,
    Honored,
    Revered,
    Exalted
}

CreatureType will be useful because there are a few types we want to ignore - specifically "NotSpecified", "Totem", and "Critter". This will also become useful later on when our bot's combat logic becomes more complicated. For example, certain CreatureTypes tend to me immune to bleed damage, so our warrior will want to avoid using rend against them.

UnitReaction is also useful because certain reaction types should be excluded from targeting. Hated is only found on guards of the opposing faction, and those are never a good idea to fight, so we can exclude those outright. Friendly, Honored, Revered and Exalted can't be attacked, so we can exclude those too. You may choose to target or ignore hostile or neutral targets depending on your goals.

Now we need to write some code that exposes the CreatureType and UnitReaction of a given unit. These are both functions in the WoW client, so add this code to Functions:

// GetCreatureType
const int GET_CREATURE_TYPE_FUN_PTR = 0x00605570;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate int GetCreatureTypeDelegate(IntPtr unitPtr);

static GetCreatureTypeDelegate GetCreatureTypeFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)GET_CREATURE_TYPE_FUN_PTR);

internal static CreatureType GetCreatureType(IntPtr unitPtr) =>
    (CreatureType)GetCreatureTypeFunction(unitPtr);

// GetUnitReaction
const int GET_UNIT_REACTION_FUN_PTR = 0x006061E0;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate int GetUnitReactionDelegate(IntPtr unitPtr1, IntPtr unitPtr2);

static GetUnitReactionDelegate GetUnitReactionFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)GET_UNIT_REACTION_FUN_PTR);

internal static UnitReaction GetUnitReaction(IntPtr unitPtr1, IntPtr unitPtr2) =>
    (UnitReaction)GetUnitReactionFunction(unitPtr1, unitPtr2);

GetCreatureType is straightforward. GetUnitReaction takes two parameters - the player's pointer, and the target unit's pointer. Now, add two methods to WoWUnit:

public CreatureType CreatureType => Functions.GetCreatureType(Pointer);

public UnitReaction UnitReaction(IntPtr playerPtr) => Functions.GetUnitReaction(Pointer, playerPtr);

Back in GrindState, let's update the query that finds the nearest target to use our new functionality:

public void Update()
{
    var newTarget = ObjectManager
        .Units
        .Where(u => u.Health > 0)
        .Where(u => 
            u.CreatureType != CreatureType.Critter && 
            u.CreatureType != CreatureType.NotSpecified && 
            u.CreatureType != CreatureType.Totem)
        .Where(u => 
            u.UnitReaction(player.Pointer) == UnitReaction.Hostile || 
            u.UnitReaction(player.Pointer) == UnitReaction.Unfriendly || 
            u.UnitReaction(player.Pointer) == UnitReaction.Neutral)
        .Where(u => u.Level <= player.Level + 2 && u.Level >= player.Level - 3)
        .OrderBy(u => u.Position.DistanceTo(player.Position))
        .FirstOrDefault();

    if (newTarget != null)
    {
        player.SetTarget(newTarget.Guid);
        botStates.Push(new MoveToTargetState(botStates, newTarget));
    }
}

This excludes ineligible CreatureTypes and creatures with certain UnitReactions, and will only target enemies up to 3 levels below the player, and 2 levels above the player. Now we won't need to change this query as we move to new areas because we aren't hard-coding units by name. One more thing we should improve: If our query excludes a particular unit, but that unit is attacking us (for example if that unit is 5 levels below us) we still want to kill that unit eventually. So we're going to modify our query a bit more to make sure we first prioritize any units that are attacking us before finding one. Here it is:

public void Update()
{
    var newTarget = ObjectManager
        .Units
        .FirstOrDefault(u => u.TargetGuid == ObjectManager.Player.Guid) ??

    ObjectManager
        .Units
        .Where(u => u.Health > 0)
        .Where(u => 
            u.CreatureType != CreatureType.Critter && 
            u.CreatureType != CreatureType.NotSpecified && 
            u.CreatureType != CreatureType.Totem)
        .Where(u => 
            u.UnitReaction(player.Pointer) == UnitReaction.Hostile || 
            u.UnitReaction(player.Pointer) == UnitReaction.Unfriendly || 
            u.UnitReaction(player.Pointer) == UnitReaction.Neutral)
        .Where(u => u.Level <= player.Level + 2 && u.Level >= player.Level - 3)
        .OrderBy(u => u.Position.DistanceTo(player.Position))
        .FirstOrDefault();

    if (newTarget != null)
    {
        player.SetTarget(newTarget.Guid);
        botStates.Push(new MoveToTargetState(botStates, newTarget));
    }
}

Now let's work on improving our CombatState.

We want to use Battle Shout, but we only want to use it if our character doesn't already have the effect. Therefore, we'll need a way of seeing which buffs our character has. In v1.12.1 of WoW, a unit can have a maximum 10 buffs. Strangely enough, instead of being stored in an array, these 10 buffs are stored as 10 separate descriptor fields. These fields are 4 bytes, and store the spellId of the buff. We know the descriptor offset of the first field, so we can simply iterate at 4 byte intervals to find all the values. Add the following to WoWUnit:

IList Buffs
{
    get
    {
        var buffs = new List<int>();
        var currentBuffOffset = BUFFS_BASE_OFFSET;
        for (var i = 0; i < 10; i++)
        {
            var buffId = MemoryManager.ReadInt(GetDescriptorPtr() + currentBuffOffset);
            if (buffId != 0)
                buffs.Add(buffId);
            currentBuffOffset += 4;
        }
        return buffs;
    }
}

Also notice that this is an array of spellIds, and as of right now we have no way of converting between a spellName and a spellId. In the WoW client, spells are loaded from a database in the game directory and stored in an array at a static offset in the WoW process. You can think of the SpellId as an auto-incrementing primary key for spells, so we know that the spells in memory will be ordered by SpellId. This means we can index the spell array in memory with the SpellId to find the spell we're looking for, then use another offset from the spell's base address to get the name property. Here's the code to find a spell's name given its id:

const int SPELLS_BASE_PTR = 0x00C0D788;
const int SPELL_NAME_OFFSET = 0x1E0;

protected string GetSpellName(int spellId)
{
    var spellsBasePtr = MemoryManager.ReadUint((IntPtr)(SPELLS_BASE_PTR));
    var spellPtr = MemoryManager.ReadUint((IntPtr)(spellsBasePtr + spellId * 4));
    var spellNamePtr = MemoryManager.ReadUint((IntPtr)spellPtr + SPELL_NAME_OFFSET);
    return MemoryManager.ReadString((IntPtr)spellNamePtr);
}

Putting the two together, we can define a helper method to determine whether a unit has a given buff:

public bool HasBuff(string name) => Buffs.Any(a => GetSpellName(a) == name);

Debuffs work in almost exactly the same way. The only difference is that a unit can have a maximum of 16 buffs, so we'll have to change the for loop a bit. We can still use the same GetSpellName method though. Here's the code to find whether a unit has a debuff.

IList Debuffs
{
    get
    {
        var debuffs = new List<int>();
        var currentDebuffOffset = DEBUFFS_BASE_OFFSET;
        for (var i = 0; i < 16; i++)
        {
            var debuffId = MemoryManager.ReadInt(GetDescriptorPtr() + currentDebuffOffset);
            if (debuffId != 0)
                debuffs.Add(debuffId);
            currentDebuffOffset += 4;
        }
        return debuffs;
    }
}

public bool HasDebuff(string name) => Debuffs.Any(a => GetSpellName(a) == name);

Warriors gain a resource called rage from dealing damage and receiving damage, and use rage to perform certain combat abilities. We're going to need to read the character's rage to know when to use certain abilities in combat, so let's do that now. Rage is stored as a descriptor, so we'll implement that exactly like we have other descriptors. Rage is a bit weird - it's stored internally in the range of 0 - 1000, but in the game client it's displayed in the range of 0 - 100, so we're going to divide by 10 to make sure we see it in the range of 0 - 100. Add the following to WoWUnit:

const int RAGE_OFFSET = 0x60;

public int Rage => MemoryManager.ReadInt(GetDescriptorPtr() + RAGE_OFFSET) / 10;

In an earlier chapter we added a method to read a unit's current health, but that's not all that useful, because the health of units changes dramatically as they increase in level. What we really need is health percentage. Both health and max health are stored as descriptors, so let's add some code to find a unit's current health percentage:

const int HEALTH_OFFSET = 0x58;
const int MAX_HEALTH_OFFSET = 0x70;

public int Health => MemoryManager.ReadInt(GetDescriptorPtr() + HEALTH_OFFSET);

public int MaxHealth => MemoryManager.ReadInt(GetDescriptorPtr() + MAX_HEALTH_OFFSET);

public int HealthPercent => (int)(Health / (float)MaxHealth * 100);

Now let's put some of this work to use. Shift mental context over to the WarriorBot project. Before we get started, let's take a look at our CombatState:

class CombatState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;
    readonly WoWUnit target;

    internal CombatState(Stack botStates, WoWUnit target)
    {
        this.botStates = botStates;
        this.target = target;
        player = ObjectManager.Player;

        player.LuaCall("CastSpellByName('Attack')");
    }

    public void Update()
    {
        if (target.Health == 0)
        {
            botStates.Pop();
            botStates.Push(new LootState(botStates, target));
        }
    }
}

There's not a whole lot going on. As it stands, when we push CombatState onto the stack, its constructor gets called and we use LuaCall to turn on auto-attack, so your character will just stand there bashing the enemy in the face until it dies. We're going to improve things. I'm going to assume that the character is at least level 6, which means you'll have access to the following abilities:

  • Heroic Strike
  • Charge
  • Rend
  • Battle Shout

Let's start with Battle Shout. Battle Shout costs 10 rage and buffs our attack power for 2 minutes, so we want to keep that up at all times. But we don't want to waste rage, so we're only going to cast Battle Shout if the character doesn't have the buff. We can do this by putting some of the new properties we added to the WoWUnit class to use. Modify the Update method like so:

public void Update()
{
    if (!player.HasBuff("Battle Shout") && player.Rage >= 10)
        player.LuaCall("CastSpellByName('Battle Shout')");

    if (target.Health == 0)
    {
        botStates.Pop();
        botStates.Push(new LootState(botStates, target));
    }
}

There's one problem here. What if the character isn't level 6, or hasn't trained the Battle Shout ability? Well, interestingly enough, nothing. The Lua interpreter is smart enough to just ignore a call for a spell the player doesn't know, or the interpreter doesn't recognize. But a better question is: what if you DO know the spell, and it has 60 second cooldown for example, and you try to call CastSpellByName? The same thing that happens if you try to cast it in game! Your screen is going to get spammed with "That spell is not ready", and it's going to waste cycles in your bot and degrade performance. And perhaps an even more fundamental consideration - what happens if the character is starting from level 1? We want our bot to work at any level without any modifications, so we need to make sure there are guards in place to prevent the usage of unlearned abilities. What we really want is the ability to determine whether a spell is "ready" or not before attempting to cast it. Fortunately, we know the memory location that stores the player's spellbook. So let's go back to the BloogBot project and add that functionality.

Earlier in this chapter when we implemented the HasBuff and HasDebuff methods, we talked about where to find in memory the array of ALL spells, and we found a spell at an offset based on the SpellId. We're going to do something similar here, but instead of finding the SpellId in the unit's descriptors, there's a separate place in memory that stores the local player's list of known spells. But once we get the SpellId, our technique of looking up the spell by it's id is going to be the same as before. The local player's spells are stored as SpellIds (4 byte fields), in an array of size 1024. So we're going to walk that array, indexing it every 4 bytes, and then looking up that SpellId in the same way we did before. Once we get 0 back for a SpellId we know we're done, so we break out of the loop. One more thing to consider is that a player can learn more spells while the bot is running, so we're going to need the ability to refresh the player's spell list. So let's put this logic into a method, then call that method in the constructor of LocalPlayer. Later on, we can call this again when leveling up or training to refresh the spell list again. Here's the code:

readonly IDictionary playerSpells = new Dictionary();

public void RefreshSpells()
{
    playerSpells.Clear();
    for (var i = 0; i < 1024; i++)
    {
        var currentSpellId = MemoryManager.ReadInt((IntPtr)(PLAYER_SPELLS_BASE + 4 * i));
        if (currentSpellId == 0) break;
        var name = GetSpellName(currentSpellId);

        if (playerSpells.ContainsKey(name))
            playerSpells[name] = new List(playerSpells[name])
            {
                currentSpellId
            }.ToArray();
        else
            playerSpells.Add(name, new[] { currentSpellId });
    }
}

Now that we have a list of the player's known spells, there are two ways a spell could be not ready: the player doesn't know the spell, or the spell is on cooldown. The first is easy enough - we simply check the dictionary of the player's known spells and check for the presence of a spell. To handle the second, we'll need to add a new function from the WoW client. This function actually takes a ref parameter: cooldownDuration that will be set when calling the function. So to determine whether a given spell is ready or not, we call this function and check of the returned value is 0. This function is interesting in that it uses the ThisCall calling convention, because it's an instance method on a class. So we need to pass SPELL_COOLDOWN_BASE as a parameter, which is a pointer to the class instance that manages this data structure for spells.

const int GET_SPELL_COOLDOWN_FUN_PTR = 0x006E13E0;
const int GET_SPELL_COOLDOWN_BASE_PTR = 0x00CECAEC;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void GetSpellCooldownDelegate(
    IntPtr spellCooldownPtr,
    int spellId,
    int unused1,
    ref int cooldownDuration,
    int unused2,
    bool unused3);

static GetSpellCooldownDelegate GetSpellCooldownFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)GET_SPELL_COOLDOWN_FUN_PTR);

internal static bool IsSpellReady(int spellId)
{
    var cooldownDuration = 0;
    GetSpellCooldownFunction(
        (IntPtr)GET_SPELL_COOLDOWN_BASE_PTR,
        spellId, 
        0, 
        ref cooldownDuration,
        0,
        false);

    return cooldownDuration == 0;
}

Finally, let's bring it all together back in LocalPlayer:

public bool IsSpellReady(string spellName, int rank = -1)
{
    if (!playerSpells.ContainsKey(spellName))
        return false;

    int spellId;

    var maxRank = playerSpells[spellName].Length;
    if (rank < 1 || rank > maxRank)
        spellId = playerSpells[spellName][maxRank - 1];
    else
        spellId = playerSpells[spellName][rank - 1];

    return Functions.IsSpellReady(spellId);
}

We first check that the player knows the spell, then we confirm that the rank is in a valid range, then we get the spellId from the dictionary of the player's known spells, and we pass it into the new Functions.IsSpellReady method. Let's go back to our WarriorBot's CombatState and use this new method. Here's what our new Battle Shout code looks like now:

public void Update()
{
    if (target.Health == 0)
    {
        botStates.Pop();
        botStates.Push(new LootState(botStates, target));
        return;
    }

    if (!player.HasBuff("Battle Shout") && player.Rage >= 10 && player.IsSpellReady("Battle Shout"))
        player.LuaCall("CastSpellByName('Battle Shout')");
}

Now the bot will only attempt to cast Battle Shout if it's in the player's list of known spells, and it's not on cooldown. The latter is less of an issue because Battle Shout doesn't have a cooldown, but it will become relevant for other abilities down the road. Also notice that we have an explicit return; statement here. We want to try to break out of the CombatState first thing if the conditions have been met, because otherwise we may have undefined behavior - for example, trying to attack a corpse in this case.

Now let's teach the bot to use Rend. Rend is an instant cast damage over time ability that costs 10 rage. The only trick here is that we don't want to re-cast rend if the enemy is already affected by it, so we'll have to check the enemy's debuffs. As an optimization, we also don't want to cast Rend if the player is less than half health or so because they might die before the damage over time effect has had a chance to finish. Check it:

public void Update()
{
    if (target.Health == 0)
    {
        botStates.Pop();
        botStates.Push(new LootState(botStates, target));
        return;
    }

    if (!player.HasBuff("Battle Shout") && player.Rage >= 10 && player.IsSpellReady("Battle Shout"))
        player.LuaCall("CastSpellByName('Battle Shout')");

    if (!target.HasDebuff("Rend") && player.Rage >= 10 && target.HealthPercent > 50 && player.IsSpellReady("Rend"))
        player.LuaCall("CastSpellByName('Rend')");
}

Next let's do Heroic Strike. Heroic Strike makes your next auto-attack do bonus damage, and costs 15 rage. This is the first time we've dealt with "on your next auto-attack" type abilities. We need a way to determine whether the player is currently preparing to use the ability on its next attack, otherwise the Update call would just constantly toggle on/off the prepared attack. Let's head back to BloogBot.

There are a few different types of "spellcasts" in WoW. There are "on next auto-attack" type abilities like Heroic Strike in our case. There's also the more typical "start casting a spell, you'll see a progress bar ticking down, and once it completes the spell is cast". There's also channeled spells where the effect starts right away and lasts for the entire duration that you channel the spell. All three of these categories put the player into the "Is Casting" state according to the game engine. The SpellId of the spell that a unit is currently casting is stored at a static offset from the unit's base pointer, so we can simply read from memory, and if the SpellId is greater than 0, we know the unit is currently casting. Add the following to WoWUnit:

public int CurrentSpellcastId => MemoryManager.ReadInt(Pointer + CURRENT_SPELLCAST_OFFSET);

public bool IsCasting => CurrentSpellcastId > 0;

Let's make CurrentSpellcastId public because it might be useful down the road. For example, if you're playing a mage, you can check if a unit is casting a fire spell, and cast Fire Shield.

Back in our CombatState let's put the new IsCasting method to use. Here's how we can wire up Heroic Strike:

public void Update()
{
    if (target.Health == 0)
    {
        botStates.Pop();
        botStates.Push(new LootState(botStates, target));
        return;
    }

    if (!player.HasBuff("Battle Shout") && player.Rage >= 10 && player.IsSpellReady("Battle Shout"))
        player.LuaCall("CastSpellByName('Battle Shout')");

    if (!target.HasDebuff("Rend") && player.Rage >= 10 && target.HealthPercent > 50 && player.IsSpellReady("Rend"))
        player.LuaCall("CastSpellByName('Rend')");

    if (player.Rage >= 15 && !player.IsCasting)
        player.LuaCall("CastSpellByName('Heroic Strike')");
}

It's also worth noting that the order of statements does matter in our CombatState. The bot will try to cast Battle Shout before it tries to cast anything else. It will try to cast Rend before Heroic Strike. So you want to think about the decision tree that normally goes through your head in combat - placing the higher priority abilities toward the top of Update. I know some people have used Fluent Behavior Trees to model the behavior of their bot. I haven't had much chance to explore this, but it seems promising. I may revisit this down the road.

We're going to be adding a lot more to our CombatState, so we can add a helper method to make coding the logic of our combat rotation a bit cleaner. We can also make our spell names const strings for better code quality. Try this:

const string BattleShout = "Battle Shout";
const string HeroicStrike = "Heroic Strike";
const string Rend = "Rend";

public void Update()
{
    if (target.Health == 0)
    {
        botStates.Pop();
        botStates.Push(new LootState(botStates, target));
        return;
    }

    TryUseAbility(BattleShout, 10, !player.HasBuff(BattleShout));

    TryUseAbility(Rend, 10, !target.HasDebuff(Rend) && target.HealthPercent > 50);

    TryUseAbility(HeroicStrike, 15, !player.IsCasting);
}

void TryUseAbility(string name, int rageRequired = 0, bool condition = true)
{
    if (player.IsSpellReady(name) && player.Rage >= rageRequired && condition)
    {
        player.LuaCall($"CastSpellByName('{name}')");
    }
}

Our CombatState is much more effective now. Let's test it.

Much better! But if you watch to the end of the video, you see a pretty nasty problem. Our bot does quite well against neutral enemies, but it has trouble with aggressive enemies. What's happening is that when the player gets closed enough to an aggressive enemy, the enemy starts charging the player and ends up getting behind you. The bot isn't smart enough to turn around, so it just stands there getting hit in the back. We'll solve that problem, and also implement Charge, in the next chapter.

Back to Top
...
Subscribe to the RSS feed to keep up with new chapters as they're released