Bloog Bot

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

Rest

At this point, if you let the bot run for long enough, he'll eventually get worn down and initiate combat when he's too injured, and die. We need a new state that gives the bot a chance to rest and regain his health (and mana if you're playing a spellcaster). In WoW, characters can eat food to regain health (or cast spells to regain health), and drink to regain mana. Let's add a RestState to accomplish this. RestState will get pushed onto the stack after looting. Our new RestState will depend on some new functionality from the WoW client that allows our bot to find and use items in the player's inventory.

n In chapter 15 we implemented looting, and worked with the LootFrame to interact with lootable items, but I mentioned that items in the loot frame were not exposed by the EnumerateVisibleObjects fuction. Items in the player's inventory, however, are exposed by EnumerateVisibleObjects, so we can use ObjectManager.Items to find any food in our inventory. I'm playing a Tauren Warrior, and the Innkeeper near Bloodhoof Village sells different types of food. The food appropriate for my level 8 character is named "Tough Hunk of Bread". To start, let's create the LootState and from the constructor add a reference to the food item:

class RestState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;
    readonly WoWItem food;

    public RestState(Stack<IBotState> botStates)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
        food = ObjectManager.Items
            .FirstOrDefault(f => f.Info.Name == "Tough Hunk of Bread");
    }

    public void Update()
    {
        if (player.HealthPercent >= 80 || ObjectManager.Units.Any(u => u.TargetGuid == player.Guid))
        {
            botStates.Pop();
            return;
        }
    }
}

Now that we have a reference to our food, we need the ability to eat it. Add the new UseItem function to Functions (like other functions we've implemented, there are some parameters we don't need to worry about, so we can pass in garbage. if it becomes relevant later on, I'll address it):

const int USE_ITEM_FUN_PTR = 0x005D8D00;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void UseItemDelegate(IntPtr itemPtr, ref ulong unused1, int unused2);

static UseItemDelegate UseItemFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)USE_ITEM_FUN_PTR);

internal static void UseItem(IntPtr itemPtr)
{
    ulong unused1 = 0;
    UseItemFunction(itemPtr, ref unused1, 0);
} 

Then from WoWItem, wrap UseItem with a method that calls it and passes in the item's Pointer:

public void Use() => Functions.UseItem(Pointer);

We don't want to waste food by eating every time Update is called, so we need the ability to determine whether the player is eating or not. Eating is considered a buff, and we implemented the ability to see buffs/debuffs in a previous chapter, so we can add a simple helper method to WoWPlayer that looks like this:

public bool IsEating => HasBuff("Food");

Now, to put it all together, let's go back to RestState and modify it to eat:

class RestState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;
    readonly WoWItem food;

    public RestState(Stack<IBotState> botStates)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
        food = ObjectManager.Items
            .FirstOrDefault(f => f.Info.Name == "Tough Hunk of Bread");
    }

    public void Update()
    {
        if (player.HealthPercent >= 80)
        {
            botStates.Pop();
            return;
        }

        if (food != null && !player.IsEating)
            food.Use();
    }
}

The transition back to GrindState occurs when the player is above 80% health. Until then, if the player isn't eating, we eat "Tough Hunk of Bread" and wait. When working with the state machine, I can't help but be reminded of recursion. With recursion, you always need a base case to avoid infinite recursion. Similarly, with our state machine, we always need at least one transition, otherwise we'll get stuck in that state and never transition out. Again, in this case, our transition occurs when the player reaches 80% health. But there's actually another transition we need to consider. Consider what happens if our bot gets attacked while resting. Getting attacked will stop the player eating, and will likely prevent the player from ever reaching 80% health, so he'll sit idle until he dies. Therefore we need an additional transition - if the player is in combat. Or in other words, if there's an enemy nearby that has the player's guid set as its "TargetGuid" property. Modify the LootState like so:

class RestState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;
    readonly WoWItem food;

    public RestState(Stack<IBotState> botStates)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
        food = ObjectManager.Items
            .FirstOrDefault(f => f.Info.Name == "Tough Hunk of Bread");
    }

    public void Update()
    {
        if (player.HealthPercent >= 80 || ObjectManager.Units.Any(u => u.TargetGuid == player.Guid))
        {
            botStates.Pop();
            return;
        }

        if (food != null && !player.IsEating)
            food.Use();
    }
}

Fire up the client, make sure you have the appropriate food in your inventory, and give it a test:

The quality of food we need to eat as our character levels up increases, and currently we have the name of food hard coded in the bot's RestState. It would be nice if we could modify that without having to change the code. To do so, we're going to create a text input in the bot's UI that allows us to specify the name of the food we're currently using at runtime. Add something like this to MainWindow.xaml:

<Label HorizontalAlignment="Right" VerticalAlignment="Center" Padding="0" Grid.Row="0" Grid.Column="0" Content="Food:" Margin="0,12,-1,238" Width="204" Height="21" Grid.RowSpan="2"/>
<TextBox Grid.Row="0" Grid.Column="0" Text="{Binding Path=Food}" Margin="226,10,5,0"/>

Then add this to MainViewModel.cs:

string food;
public string Food
{
    get => food;
    set
    {
        food = value;
        OnPropertyChanged(nameof(Food));
    }
}

If you remember way back, I talked about WPF applications and two-way data binding. We have to implement the setter this way otherwise our ViewModel won't be notified when the text area's value changes. I showed this earlier, but as a refresher, here's what OnPropertyChanged does:

public event PropertyChangedEventHandler PropertyChanged;

void OnPropertyChanged(string name) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

Now, pass food into the Start method as a parameter. You'll also have to pass it through all the states, storing references along the way, until you pass it into RestState (this is tedious, and as we need more parameters for our states, it will only get worse. We'll address this at some point later on). Finally, here's what RestState should look like:

class RestState : IBotState
{
    readonly Stack<IBotState> botStates;
    readonly LocalPlayer player;
    readonly WoWItem foodItem;

    public RestState(Stack<IBotState> botStates, string food)
    {
        this.botStates = botStates;
        player = ObjectManager.Player;
        foodItem = ObjectManager.Items
            .FirstOrDefault(f => f.Info.Name == food);
    }

    public void Update()
    {
        if (player.HealthPercent >= 80 || ObjectManager.Units.Any(u => u.TargetGuid == player.Guid))
        {
            botStates.Pop();
            return;
        }

        if (foodItem != null && !player.IsEating)
            foodItem.Use();
    }
}

RestState will now use the value of whatever you type in the text input in the UI when searching for food in the player's inventory. Let's do a final test. Fire up the bot, type the name of the food you're using ("Tough Hunk of Bread" in my case), and start the bot. He should find and eat food whenever he needs to rest inbetween kills:

The exact same technique can be used to drink water inbetween combat if you're playing a spellcaster.

Right now the bot simply runs in a straight line to get to its next target, but he'll get stuck if there are any obstacles in his way (trees, buildings, etc). In the next chapter we're going to create a pathfinding library using Recast and Detour (open source navigation mesh libraries) that will let our bot plot and traverse a safe path to its destination.

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