Bloog Bot

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

Facing

We left off in Chapter 17 with a problem where the player can be left facing the wrong direction from an enemy, at which point it'll stand there getting hit in the back until it dies. What we need to do now is give the bot the ability to face an arbitrary direction. To start, let's add some code that reads the player's current facing direction from memory. This memory address is the same for all types of Unit, is stored at an offset from the unit's base pointer, and it stores a value in radians (in the range of 0 - 2pi). Add this to WoWUnit:

float CurrentFacing => MemoryManager.ReadFloat(Pointer + FACING_OFFSET);

Now we need a way to determine which direction the player SHOULD be facing given a particular Position (the position of it's target usually). Ultimately this boils down to a trigonometry problem. Consider the 2d coordinate plane. Given the player's position p1, and the target's position p2, we can calculate the deltaX and deltaY, then apply the Math.Atan2 function to find the angle in radians of the line starting at p1 and pointing to p2. See this post for an explanation of why we need to use Atan2 vs Atan. We also need two code branches to represent which direction we're rotating. If this doesn't make sense, draw it out on a piece of paper. Here's the code to find the facing angle for a given target's position:

protected float GetFacingForPosition(Position position)
{
    var f = (float)Math.Atan2(position.Y - Position.Y, position.X - Position.X);
    if (f < 0.0f)
        f += (float)Math.PI * 2.0f;
    else
    {
        if (f > (float)Math.PI * 2)
            f -= (float)Math.PI * 2.0f;
    }
    return f;
}

And putting it all together, in order to determine whether the player is facing a given position, we're going to take the difference between GetFacingForPosition and CurrentFacing and test whether it's within an acceptable threshold (I've arbitrarily chosen 0.3 for a threshold). Here's the IsFacing method:

public bool IsFacing(Position position) => (GetFacingForPosition(position) - CurrentFacing) < 0.3f;

That's half the puzzle. Testing whether the player is facing a given position is one thing, but if he's not, how do we tell the bot to face an arbitrary position? To do so, we're going to need to add two more functions from the WoW Client: SetFacing, and SendMovementUpdate. In a previous chapter I touched on multiplayer networking architecture, but as a quick recap, the WoW client is in constant communication with the WoW server. The player's position is ultimately stored on the server, but the client handles user input, updates the position of the player in the WoW client's memory, then periodically sends updates to the server with the player's position. What this means to us is we need to call two functions: one to set the player's facing, and another to send a movement update to the game server. Add these two implementations to Functions:

// SetFacing
const int SET_FACING_FUN_PTR = 0x007C6F30;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void SetFacingDelegate(IntPtr playerSetFacingPtr, float facing);

static SetFacingDelegate SetFacingFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)SET_FACING_FUN_PTR);

internal static void SetFacing(IntPtr playerPtr, float facing) =>
    SetFacingFunction(playerPtr, facing);

// SendMovementUpdate
const int SEND_MOVEMENT_UPDATE_FUN_PTR = 0x00600A30;

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void SendMovementUpdateDelegate(
    IntPtr playerPtr, 
    int timestamp, 
    int opcode, 
    float unused1, 
    int unused2);

static SendMovementUpdateDelegate SendMovementUpdateFunction =
    Marshal.GetDelegateForFunctionPointer((IntPtr)SEND_MOVEMENT_UPDATE_FUN_PTR);

internal static void SendMovementUpdate(IntPtr playerPtr, int timestamp, int opcode) =>
    SendMovementUpdateFunction(playerPtr, timestamp, opcode, 0, 0);

Before talking through these two functions, let's look at the code that calls them. Add this to WoWPlayer:

const int SET_FACING_OFFSET = 0x9A8;
const int SET_FACING_OPCODE = 0xDA;

public void Face(Position position)
{
    var requiredFacing = GetFacingForPosition(position);
    Functions.SetFacing(IntPtr.Add(Pointer, SET_FACING_OFFSET), requiredFacing);
    Functions.SendMovementUpdate(Pointer, Environment.TickCount, SET_FACING_OPCODE);
}

SetFacing takes two parameters. The first is an offset from the player's base pointer. If you remember how we read the player's current facing, we used Pointer + 0x9C4. So when we set the player's facing, why do we use Pointer + 0x9A8? This is an implementation detail of the client/server architecture of WoW. We need two separate memory locations to store the player's actual facing value (that which comes from the single source of truth - the server), and a different place to store the outgoing facing value (that which we send to the server). This becomes relevant when, for example, the player is lagging and can't send messages to the server. The client does something called "client-side prediction" and continues showing your player moving even though it's not pushing updates to the server. In this case, the rendering engine is reading your position from the first variable, FACING_OFFSET, but because the client's updates aren't making it to the server, eventually the client and server will do what's called "server reconciliation" and push a message back to the client saying "hey, sorry, your position is actually something different than what you have stored locally", and FACING_OFFSET will be overwritten with the true value from the server. If you've ever played first-person shooters, it's common on a bad connection to experience "rubber-banding", which is the direct result of client-side prediction gone wrong. The second parameter to SetFacing is self-explanatory - the new facing direction.

The second function, SendMovementUpdate, actually pushes an update to the server. To do so, we have to provide an "opcode". Network communication can be implemented using opcodes which are basically keys for each message. When the server receives the opcode 0xDA in this case, it knows that the update is meant to update the facing direction of the player, and based on the opcode it received it knows the number and type of any arguments passed along with it.

With all of this done, let's go back to our CombatState and add the code that will fix the bot's facing if he gets turned around. Here's what CombatState should look like now:

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;
    }

    if (!player.IsFacing(target.Position))
        player.Face(target.Position);

    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}')");
    }
}

If the player ever finds itself more than 0.3 radians off from directly facing the target, he'll automatically adjust himself.

The last thing we're going to do in this chapter is teach the bot to use Charge to engage enemies. Charge can only be cast between the range of 8 - 25 yards, but the player can't be in combat. Charge doesn't cost rage - in fact it generates rage. So Charge is actually an engagement ability. For that reason, we aren't going to put this code in CombatState. Instead, we're going to modify our MoveToTargetState to use Charge to engage the enemy before pushing a CombatState onto the stack. But we need to be careful - it's possible that our next target is less than 8 yards away, which means the player is too close to the target to use Charge. So we need two transitions to CombatState. We want to use ClickToMove to approach the target, attempt to use Charge, but if the player is within 5 yards from the target, engage in melee combat like we had done previously. Here's the new Update method in MoveToTargetState:

const string Charge = "Charge";

public void Update()
{
    var distanceToTarget = player.Position.DistanceTo(target.Position);

    if (distanceToTarget < 25 && distanceToTarget > 8 && !player.IsCasting && player.IsSpellReady(Charge))
    {
        player.LuaCall($"CastSpellByName({Charge})");
        botStates.Pop();
        botStates.Push(new CombatState(botStates, target));
        return;
    }

    if (distanceToTarget < 5)
    {
        player.ClickToMoveStop();
        botStates.Pop();
        botStates.Push(new CombatState(botStates, target));
        return;
    }

    player.ClickToMove(target.Position);
}

Our bot should have much more reliable behavior now. Let's do another test:

At this point, our bot is pretty self-sufficient. You may be running low on health, so in the next chapter we're going to add a RestState so the bot will take breaks and regain his health.

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