Bloog Bot

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

Autonomy

If you think back to the chapter where we first talked about using a state machine to model our bot, our test allowed us to push new states onto the stack and pop states off the stack, but those states weren't very useful - they just printed some text to the console. We're finally at the point where we'll set up some basic states that allow our bot to operate autonomously. The three basic pieces of functionality we need in order for our bot to operate autonomously are:

  1. Find an eligible target
  2. Move to the target
  3. Kill the target

Let's start with a very rudamentary implementation using a single state. We'll modify the Start button to push a single state onto the stack, then start the bot running. The Stop button will stay the same. Our new BasicState will use two new functions. The first one, ClickToMoveStop, we'll add to the WoWPlayer class, and this will tie into the existing ClickToMove method on the Functions class which we'll modify slightly. First modify the ClickType enum to add the new value of None:

enum ClickType
{
    Move = 0x4,
    None = 0xD
}

Then on WoWPlayer, add the new ClickToMoveStop method so they look like this:

internal void ClickToMove(Position position) => Functions.ClickToMove(Pointer, ClickType.Move, position);

internal void ClickToMoveStop() => Functions.ClickToMove(Pointer, ClickType.None, Position);

Now, add a new SetTarget method to the Functions class:

const int SET_TARGET_FUN_PTR = 0x00493540;
        
delegate void SetTargetDelegate(ulong guid);

static readonly SetTargetDelegate SetTargetFunction =
    Marshal.GetDelegateForFunctionPointer<SetTargetDelegate>((IntPtr)SET_TARGET_FUN_PTR);

static internal void SetTarget(ulong guid) => SetTargetFunction(guid);

And add a SetTarget method to LocalPlayer:

public void SetTarget(ulong guid) => Functions.SetTarget(guid);

Last, we're going to need to check the player's current target. This is stored as a descriptor, which we first discussed in Chapter 8. Because both NPC Units and Players can have targets, add this to WoWUnit:

const int TARGET_GUID_OFFSET = 0x40;

public ulong TargetGuid => MemoryManager.ReadUlong(GetDescriptorPtr() + TARGET_GUID_OFFSET);

Now we have everything we need to implement our new BasicState class. Think back to our list of required functionality above: find target, move to target, kill target, then check out the code:

class BasicState : IBotState
{
    readonly LocalPlayer player;

    WoWUnit target;

    public BasicState()
    {
        player = ObjectManager.Player;
    }

    public void Update()
    {
        // 1. first we have to find an eligible target
        if (player.TargetGuid == 0)
        {
            var newTarget = ObjectManager
                .Units
                .OrderBy(u => u.Position.DistanceTo(player.Position))
                .FirstOrDefault(u => u.Name == "Plainstrider");

            if (newTarget != null)
            {
                target = newTarget;
                player.SetTarget(newTarget.Guid);
                player.LuaCall("CastSpellByName('Attack')");
            }
        }

        // 2. once we have a target, move to it
        else if (player.TargetGuid > 0 && player.Position.DistanceTo(target.Position) > 5)
        {
            player.ClickToMove(target.Position);
        }

        // 3. once we're in range, stop at a good distance
        else
        {
            player.ClickToMoveStop();
        }
    }
}

Let's think about how this is going to work. Our character is standing around idle. We click Start, which sets running to true (which will cause Update to get called on a loop), and we instantiate a new BasicState object and push it onto the stack. The first time Update gets called, we search the ObjectManager for an eligible target (I'm testing this with a Tauren Warrior, and the starting zone has plenty of Plainstriders around - you may have to adjust the code depending on where you start). If no targets are in range, this first branch of code will continue executing everytime Update is called until a Plainstrider is found. As soon as we find one, we use the new SetTarget function which will cause our character to set the Plainstrider as its target, and we use LuaCall to turn on auto-attack. Once a target is found and set, Update will hit the second branch of code that simply uses ClickToMove to move toward the target. Once our character is within 5 feet from the target, Update will hit the third branch that calls the new ClickToMoveStop method to ensure that we don't stand on top of the target (this will make it difficult to attack the target correctly. Update the Bot class to look like this:

class Bot
{
    readonly Stack<IBotState> botStates = new Stack<IBotState>();

    bool running;

    internal void Stop()
    {
        Console.WriteLine("\n--- STOPPING BOT ---\n");

        running = false;
        while (botStates.Count > 0)
            botStates.Pop();
    }

    internal void Start()
    {
        Console.WriteLine("\n--- STARTING BOT ---\n");

        running = true;
        botStates.Push(new BasicState());
        StartInternal();
    }
    
    async void StartInternal()
    {
        while (running)
        {
            try
            {
                ThreadSynchronizer.RunOnMainThread(() =>
                {
                    botStates.Peek()?.Update();
                });

                await Task.Delay(500);
            }
            catch (Exception e)
            {
                Console.WriteLine($"An error occured during the bot's main loop: {e}");
            }
        }
    }
}

Notice that we're wrapping our entire bot's loop inside ThreadSynchronizer.RunOnMainThread. This ensures our bot's code will always execute on the main thread. As a reminder, remember that certain functions internal to the WoW client depend on data structures being present that are only accessible from the main thread, so the safest way to go is to just execute all of our bot's code on the main thread. There may be exceptions to this later on, for example, if any long-running calls need to be made that don't depend on the main thread, so keep an eye out for this in future chapters.

There's one more thing we need to do before our next test. Let's circle back to our EnumerateVisibleObjects code. Up until now, we've been manually calling ObjectManager.EnumerateVisibleObjects() any time we want to see what's around us. This is going to get tedious, so we're going to kick off what is essentially a background job that will call EnumerateVisibleObjects on a loop in a background thread. Add the following code to ObjectManager (note that we're now running this code on the game's Main thread):

public static class ObjectManager
{
    ...

    static public bool IsLoggedIn => Functions.GetPlayerGuid() > 0;

    static internal async void StartEnumeration()
    {
        while (true)
        {
            try
            {
                if (IsLoggedIn)
                    ThreadSynchronizer.RunOnMainThread(() => EnumerateVisibleObjects());

                await Task.Delay(50);
            }
            catch (Exception e)
            {
                Console.WriteLine($"An error occurred during object enumeration: {e}");
            }
        }
    }
}

Now that object enumeration is happening on a different thread than where our bot's code is executing, there's another problem we need to solve. Our bot is going to be poking around in the ObjectManager to view the state of the objects around us, but consider what the EnumerateVisibleObjects is doing. Here's the code for a visual aid:

internal static void EnumerateVisibleObjects()
{
    if (IsLoggedIn)
    {
        Objects.Clear();

        Functions.EnumerateVisibleObjects(callbackPtr, 0);
    }
}

The first thing we do is Clear the object list, then repopulate it by falling Functions.EnumerateVisibleObjects(callbackPtr, 0). Because this code is executing on a different thread from our bot's logic, it's possible for our bot's logic to peer into the ObjectManager immediately after the object list has been cleared, in which case we'd see an empty or incomplete list of objects. To solve this problem, we're going to use a Double Buffer pattern. Essentially, we will only clear and rebuild a new private ObjectBuffer field, then swap that with the publicly exposed Objects list only after enumeration is complete. That way, any external threads looking at the ObjectManager will always see a full list of game objects. Update your ObjectManager to look like this:

...

static internal IList<WoWObject> Objects = new List<WoWObject>();
static readonly IList<WoWObject> ObjectsBuffer = new List<WoWObject>();

static internal async void StartEnumeration()
{
    while (true)
    {
        try
        {
            if (IsLoggedIn)
                ThreadSynchronizer.RunOnMainThread(() => EnumerateVisibleObjects());

            await Task.Delay(50);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

static void EnumerateVisibleObjects()
{
    ObjectsBuffer.Clear();

    Functions.EnumerateVisibleObjects(callbackPtr, 0);

    Objects = new List<WoWObject>(ObjectsBuffer);
}

And finally, let's add some code in OnStartup in App.xaml.cs to start the background object enumeration:

protected override void OnStartup(StartupEventArgs e)
{
    ...

    ObjectManager.StartEnumeration();

    ...
}

Now, as soon as your bot starts, this background job will kick off, running in an endless loop. Notice that object enumeration only runs when the player is logged in - the WoW client's internal ObjectManager does not exist in memory until the player logs in. Now whenever your bot's logic needs to look at the ObjectManager, you'll have a fully populated list of game objects to look at. Feel free to experiment with the frequency of object enumeration - I have this running every 50ms, but you may have better luck with something smaller or larger.

Fire up the bot and click Start. You should see your bot move to the nearest enemy and slay it:

It's definitely working, but it's very fragile. For example, the bot doesn't do anything after its target dies. It will continue trying to attack the corpse. And if you click Stop, then Start again, the bot will identify the corpse as an eligible target.

We'll fix all of those problems, but you can imagine that the logic of this class is going to get out of hand very quickly, becoming a maintanence nightmare. What would be ideal is if we could break these 3 code branches into 3 different states. It would also be nice if the bot was smart enough to restart from the beginning as soon as the target died. What we really need is a sort of "base state" that orchestrate the overall flow of the bot. This "base state" will be responsible for the first code branch of BasicState: search for an eligible target - search for a target, standing idle if no target is found. Let's call that state GrindState, and implement it as follows:

class GrindState
{
    readonly LocalPlayer player;

    public GrindState()
    {
        player = ObjectManager.Player;
    }

    public void Update()
    {
        var newTarget = ObjectManager
            .Units
            .OrderBy(u => u.Position.DistanceTo(player.Position))
            .FirstOrDefault(u => u.Name == "Plainstrider");

        if (newTarget != null)
        {
            player.SetTarget(newTarget.Guid);
            // push new MoveToTargetState
        }
    }
}

Notice that there's a state transition commented out. Once an eligible target is found, we push a new state responsible for moving to the target. MoveToTargetState will look like this:

class MoveToTargetState
{
    readonly LocalPlayer player;
    readonly WoWUnit target;

    internal MoveToTargetState(WoWUnit target)
    {
        player = ObjectManager.Player;
        this.target = target;
    }

    public void Update()
    {
        if (player.Position.DistanceTo(target.Position) < 5)
        {
            player.ClickToMoveStop();
            // push new CombatState
        }

        player.ClickToMove(target.Position);
    }
}

Again, the state transition is commented out. Once the bot is within combat distance to the target, it stops movement and pushes a new state responsible for killing the enemy. CombatState will look like this:

class CombatState : IBotState
{
    readonly LocalPlayer player;

    internal CombatState()
    {
        player = ObjectManager.Player;
        player.LuaCall("CastSpellByName('Attack')");
    }

    public void Update()
    {
        // TODO: combat rotation (cast spells, use abilities, etc)
    }
}

The functionality is essentially the same, but we've taken the if/else logic from the BasicState class and used it to implement state transitions, and encapsulated the specific behavior into three different states. This pattern will make our state machine far more maintainable. But one big question remains - how do we actually manage pushing/popping states on/off the stack? The reference to our stack is in the Bot class.

My early iterations of this bot stuck all this state management logic in the Bot class, but that got out of hand really fast. Once we start adding a lot of states (think 20+), each with multiple different transitions to other states, stuffing all that in a single class isn't a good idea. Plus, certain state transitions require information that is only available within a particular state. For example - GrindState is responsible for finding a target. The Bot class doesn't know when GrindState has found an eligible target, so it doesn't know when to push a new MoveToTarget state onto the stack. This could probably be implemented by adding a really complex object as a return value from the Update method on IBotState, but that would also get pretty nasty.

What if each individual state was responsible for maintaining it's own state transitions? This satisfies the single responsibility principle nicely (we don't want to have to modify Bot every time some other state changes). To accomplish that, we're going to pass in our Stack<IBotState> to the constructor of our states, and let our states manipulate the stack directly. Let's look a our three states again after making those changes:

GrindState (let's clean this up to only look for targets that aren't dead (health > 0)

class GrindState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;

    public GrindState(Stack<IBotState> botStates)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
    }

    public void Update()
    {
        var newTarget = ObjectManager
            .Units
            .Where(u => u.Health > 0)
            .OrderBy(u => u.Position.DistanceTo(player.Position))
            .FirstOrDefault(u => u.Name == "Plainstrider");

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

MoveToTargetState

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

    internal MoveToTargetState(Stack<IBotState> botStates, WoWUnit target)
    {
        this.botStates = botStates;
        this.target = target;
        player = ObjectManager.Player;
    }

    public void Update()
    {
        if (player.Position.DistanceTo(target.Position) < 5)
        {
            player.ClickToMoveStop();
            botStates.Pop();
            botStates.Push(new CombatState(botStates));
        }

        player.ClickToMove(target.Position);
    }
}

CombatState

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

    internal CombatState(Stack<IBotState> botStates)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
        player.LuaCall("CastSpellByName('Attack')");
    }

    public void Update()
    {
        if (target.Health == 0)
            botStates.Pop();

        // TODO: combat rotation (cast spells, use abilities, etc)
    }
}

Last, add a debug line to the StartInternal method of the Bot class that prints the current state to the console. This will help visualize how our state machine is transitioning between states while the bot runs. Fire it up and give it a try.

Depending on which area you're testing in, your character may have trouble moving between targets. It may get hung up on trees or terrain. Eventually we're going to create a pathfinding library that our character can use to safely navigate to its target. But the functionality we have now is the fundamental core of our bot. In later chapters we'll continue improving these three states, as well as add many more to handle things like looting fallen enemies, returning to town to empty our bags, training new spells, etc.

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