Bloog Bot

Chapter 14
Drew Kestell, 2018
[email protected]

Autonomy

Before working on the behavioral logic for the bot, let's clean something up quick. In the last chapter, we established that the bot would crash if we don't execute certain actions on the main thread (LuaCall for example), so we created the ThreadSynchronizer class to allow us to do so. We're asking for trouble if we have to remember to use RunOnMainThread every time we use LuaCall, so let's move that into the Functions class.

public class Functions
{
    readonly ThreadSynchronizer threadSync;

    public Functions(ThreadSynchronizer threadSync)
    {
        this.threadSync = threadSync;
    }

    ...

    // LuaCall
    const int LUA_CALL_FUN_PTR = 0x00704CD0;

    [DllImport("FastCall.dll", EntryPoint = "LuaCall")]
    static extern void LuaCallFunction(string code, int ptr);

    internal void LuaCall(string code) => threadSync.RunOnMainThread(() => { LuaCallFunction(code, LUA_CALL_FUN_PTR); });
}

You'll also have to modify the initialization code in App.xaml.cs when instantiating the Functions object. Now we can be sure our code will be safely running on the main thread when we call LuaCall.

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
}

Now modify the ClickToMove method on the Functions class like so:

internal void ClickToMove(IntPtr playerPtr, ClickType clickType, Position position)
{
    ulong interactGuidPtr = 0;
    var xyz = new XYZ(position);
    ClickToMoveFunction(playerPtr, clickType, ref interactGuidPtr, ref xyz, 2);
}

Then on WoWPlayer, modify the ClickToMove method and 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 (note that we run this on the main thread - this function depends on the presence of the ObjectManager much like LuaCall):

const int SET_TARGET_FUN_PTR = 0x00493540;

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

delegate void SetTargetDelegate(ulong guid);

internal void SetTarget(ulong guid) => threadSync.RunOnMainThread(() => { SetTargetFunction(guid); });

And add a SetTarget method to WoWPlayer:

internal 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. We're going to be working with descriptors more in the future, so first add a helper method to WoWObject:

const int DESCRIPTOR_OFFSET = 0x8;

protected IntPtr GetDescriptorPtr() =>
    MemoryManager.ReadIntPtr(IntPtr.Add(Pointer, DESCRIPTOR_OFFSET));

Then 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 ObjectManager objectManager;
    readonly WoWPlayer player;

    WoWUnit target;

    public BasicState(ObjectManager objectManager)
    {
        this.objectManager = objectManager;
        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.

Notice that the BasicState has a constructor parameter, so we need to pass that in from MainViewModel. Modify the Start method in MainViewModel like so:

void Start() => bot.Start(Functions, ObjectManager);

Then modify the Bot class like so:

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

    bool Running;

    internal void PushState(IBotState state) => botStates.Push(state);

    ...

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

        Running = true;
        botStates.Push(new BasicState(objectManager));
        StartInternal();
    }
    
    async void StartInternal()
    {
        while (Running)
        {
            botStates.Peek()?.Update();
            await Task.Delay(500);
        }
    }
}

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. If you already have a target before clicking Start the bot will crash will a null reference exception (looking at the logic of BasicState will reveal why). Additionally, 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 ObjectManager objectManager;
    readonly WoWPlayer player;

    public GrindState(ObjectManager objectManager)
    {
        this.objectManager = objectManager;
        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 WoWPlayer player;
    readonly WoWUnit target;

    internal MoveToTargetState(WoWPlayer player, WoWUnit target)
    {
        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 WoWPlayer player;

    internal CombatState(WoWPlayer player)
    {
        this.player = 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 ObjectManager objectManager;
    readonly WoWPlayer player;

    public GrindState(Stack<IBotState> botStates, ObjectManager objectManager)
    {
        this.botStates = botStates;
        this.objectManager = objectManager;
        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, player, newTarget));
        }
    }
}

MoveToTargetState

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

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

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

        player.ClickToMove(target.Position);
    }
}

CombatState

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

    internal CombatState(Stack<IBotState> botStates, WoWPlayer player)
    {
        this.botStates = botStates;
        this.player = 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

Comments? Leave me a note: