Bloog Bot

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

Recap (2) - 11/29/2020

Greetings!

I recently took some time off work for the Thanksgiving holiday. At the beginning of my vacation, I received a few emails from folks about these BloogBot posts, which rekindled my interest in this project. I've been thinking about picking it up for a while now, but it's a daunting task because while development of the bot continued, I didn't do a good job of keeping up with these articles, so the code deviated quite a bit.

So I took advantage of my free time and spent some time getting the code back into the state it was in after the last article I wrote here. I also went back to all the previous articles to correct errors, and update the code to be more in line with the current state of my bot (I made some changes after learning along the way). I tried to be careful to make sure everything is consistent between the articles, but if anybody notices mistakes, please let me know!

So, in this article, I'm going to post some code snippets of the most important parts of the codebase. If anybody is following along, it would be smart to make sure your code looks like this before moving on to future articles.

You'll notice my code references a WardenDisabler class, and a Hack class. I haven't written about these in any previous chapter. I'm still on the fence about whether I'm going to show this code or not, out of respect for all the private server operators out there. I may change my mind in the future, but if you'd like to learn how to circumvent Warden in the vanilla client, the information is out there if you look around on Google.

The current version of my bot does some pretty cool stuff that I'm excited to write about. You can define grinding hotspots which are persisted in an Azure SQL database that allows multiple people each running the bot locally to share the same grinding hotspots in the cloud. I also added Discord integration that allows you to command the bot via Discord commands. Plus a bunch of other exciting stuff. I plan on writing about some of this stuff over the upcoming holiday season, so please check back soon!

Here are some code snippets of how your code should look before moving on:


App.xaml.cs

public partial class App : Application
{
    [DllImport("Kernel32")]
    static extern void AllocConsole();

    const int CLICK_TO_MOVE_FIX = 0x860A90;
    const int LUA_UNLOCK = 0x494A50;

    protected override void OnStartup(StartupEventArgs e)
    {
        Debugger.Launch();
        AllocConsole(); // this will launch the Console so we can see our debug text

        // throttle framerate to fix ClickToMove on higher refresh rate monitors
        DirectXManager.ThrottleFPS();

        // enable ClickToMove fix
        MemoryManager.WriteBytes((IntPtr)CLICK_TO_MOVE_FIX, new byte[] { 0, 0, 0, 0 });

        // unlock protected Lua functions
        MemoryManager.WriteBytes((IntPtr)LUA_UNLOCK, new byte[] { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xc3 });

        ObjectManager.StartEnumeration();

        var mainWindow = new MainWindow();
        Current.MainWindow = mainWindow;
        mainWindow.Closed += (sender, args) => { Environment.Exit(0); };
        mainWindow.Show();

        base.OnStartup(e);
    }
}

MainViewModel.cs

public class MainViewModel : INotifyPropertyChanged
{
    readonly BotLoader botLoader = new BotLoader();
    BotSettings botSettings;

    public ObservableCollection ConsoleOutput { get; } = new ObservableCollection();

    public ObservableCollection Bots { get; private set; }

    public MainViewModel()
    {
        ReloadBots();
        LoadBotSettings();
    }

    #region Commands
    // Start command
    ICommand startCommand;

    void Start() => currentBot.Start(Food);

    public ICommand StartCommand => 
        startCommand ?? (startCommand = new CommandHandler(Start, true));

    // Stop command
    ICommand stopCommand;

    void Stop() => currentBot.Stop();

    public ICommand StopCommand =>
        stopCommand ?? (stopCommand = new CommandHandler(Stop, true));

    // ReloadBot command
    ICommand reloadBotCommand;

    void ReloadBots()
    {
        Bots = new ObservableCollection(botLoader.ReloadBots());
        currentBot = Bots.First();

        Log("Bots successfully loaded!");
    }

    public ICommand ReloadBotCommand =>
        reloadBotCommand ?? (reloadBotCommand = new CommandHandler(ReloadBots, true));

    // SaveSettings command
    ICommand saveSettingsCommand;

    void SaveSettings()
    {
        var botSettings = new BotSettings()
        {
            Food = food
        };

        var currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var botSettingsFilePath = Path.Combine(currentFolder, "botSettings.json");
        var serializer = new DataContractJsonSerializer(typeof(BotSettings));
        var fileStream = File.Open(botSettingsFilePath, FileMode.Truncate);
        serializer.WriteObject(fileStream, botSettings);
    }

    public ICommand SaveSettingsCommand =>
        saveSettingsCommand ?? (saveSettingsCommand = new CommandHandler(SaveSettings, true));
    #endregion

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

    IBot currentBot;
    public IBot CurrentBot
    {
        get => currentBot;
        set
        {
            currentBot = value;
            OnPropertyChanged(nameof(CurrentBot));
        }
    }
    #endregion

    public event PropertyChangedEventHandler PropertyChanged;

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

    void Log(string message)
    {
        ConsoleOutput.Add($"({DateTime.Now.ToShortTimeString()}) {message}");
        OnPropertyChanged(nameof(ConsoleOutput));
    }

    void LoadBotSettings()
    {
        var currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var botSettingsFilePath = Path.Combine(currentFolder, "botSettings.json");
        var serializer = new DataContractJsonSerializer(typeof(BotSettings));
        var fileStream = File.OpenRead(botSettingsFilePath);
        botSettings = serializer.ReadObject(fileStream) as BotSettings;

        Food = botSettings.Food;
    }
}

MainWindow.xaml

<Window x:Class="BloogBot.UI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BloogBot.UI"
        mc:Ignorable="d"
        Title="BloogBot" MinHeight="350" Height="350" MaxHeight="350" MinWidth="400" Width="400" MaxWidth="400" ResizeMode="NoResize" SizeToContent="WidthAndHeight">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="240"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="392"/>
        </Grid.ColumnDefinitions>

        <Button Grid.Row="0" Grid.Column="0" Command="{Binding StartCommand}" Content="Start" Padding="1" Margin="6,10,344,0"/>
        <Button Grid.Row="0" Grid.Column="0" Command="{Binding StopCommand}" Content="Stop" Padding="1" Margin="54,10,296,0"/>
        <Button Grid.Row="0" Grid.Column="0" Command="{Binding ReloadBotCommand}" Content="Reload Bots" Padding="1" Margin="102,10,222,0"/>
        <Button Grid.Row="0" Grid.Column="0" Command="{Binding SaveSettingsCommand}" Content="Save Settings" Padding="1" Margin="176,10,133,0"/>

        <Label Grid.Row="1" Grid.Column="0" VerticalContentAlignment="Center" HorizontalAlignment="Right" VerticalAlignment="Center" Padding="0" Content="Food:" Margin="0,0,183,0" Width="204" Height="50"/>
        <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=Food}" Margin="43,16,188,13"/>

        <Label
            Grid.Row="2"
            Content="Bot:"
            VerticalContentAlignment="Center"
            HorizontalContentAlignment="Right" Margin="0,0,355,0"/>
        <ComboBox
            Grid.Row="2"
            Margin="43,10,10,10"
            VerticalContentAlignment="Center"
            SelectedItem="{Binding Path=CurrentBot, Mode=TwoWay}"
            ItemsSource="{Binding Path=Bots, Mode=OneWay}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock>
                        <TextBlock.Text>
                            <MultiBinding StringFormat="{}{0}">
                                <Binding Path="Name"/>
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>

        <ScrollViewer Grid.Row="3" Grid.Column="0" Padding="10" Name="_console" Background="DimGray" Margin="10,10,10,59">
            <StackPanel>
                <ItemsControl ItemsSource="{Binding ConsoleOutput, Mode=OneWay}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Path=.}" TextWrapping="Wrap" MaxWidth="348"  Foreground="White" FontFamily="Consolas"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Window>

ObjectManager.cs

public static class ObjectManager
{
    const int OBJECT_TYPE_OFFSET = 0x14;

    [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    delegate int EnumerateVisibleObjectsCallback(int filter, ulong guid);

    static EnumerateVisibleObjectsCallback callback;
    static IntPtr callbackPtr;

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

    static ObjectManager()
    {
        callback = Callback;
        callbackPtr = Marshal.GetFunctionPointerForDelegate(callback);
    }

    public static LocalPlayer Player { get; private set; }

    public static IEnumerable Units => Objects.OfType().Where(o => o.ObjectType == ObjectType.Unit).ToList();

    public static IEnumerable Players => Objects.OfType();

    public static IEnumerable Items => Objects.OfType();

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

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

                await Task.Delay(500);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Error occured inside EnumerateVisibleObject: {e}");
            }
        }
    }

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

        Functions.EnumerateVisibleObjects(callbackPtr, 0);

        Objects = new List(ObjectsBuffer);
    }

    static int Callback(int filter, ulong guid)
    {
        var pointer = Functions.GetObjectPtr(guid);
        var objectType = (ObjectType)MemoryManager.ReadByte(
            IntPtr.Add(pointer, 
            OBJECT_TYPE_OFFSET)
        );
        
        switch (objectType)
        {
            case ObjectType.Container:
            case ObjectType.Item:
                ObjectsBuffer.Add(new WoWItem(pointer, guid, objectType));
                break;
            case ObjectType.Player:
                var player = new LocalPlayer(pointer, guid, objectType);
                if (player.Guid == Functions.GetPlayerGuid())
                    Player = player;
                ObjectsBuffer.Add(player);
                break;
            case ObjectType.Unit:
                ObjectsBuffer.Add(new WoWUnit(pointer, guid, objectType));
                break;
            default:
                ObjectsBuffer.Add(new WoWObject(pointer, guid, objectType));
                break;
        }
        
        return 1;
    }
}

WarriorBot.cs

[Export(typeof(IBot))]
class WarriorBot : IBot
{
    public string Name => "Warrior Bot";

    readonly Stack botStates = new Stack();

    bool running;

    public void Stop()
    {
        running = false;
        while (botStates.Count > 0)
            botStates.Pop();
        WoWEventHandler.ClearAllSubscribers();
    }

    public void Start(string food)
    {
        running = true;
        botStates.Push(new GrindState(botStates, food));
        StartInternal();
    }

    async void StartInternal()
    {
        while (running)
        {
            try
            {
                ThreadSynchronizer.RunOnMainThread(() =>
                {
                    if (botStates.Count > 0)
                    {
                        botStates.Peek().Update();
                    }
                    else
                        Stop();
                });

                await Task.Delay(25);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Error occured inside Bot's main loop: {e}");
            }
        }
    }
}

Navigation.cs

public static unsafe class Navigation
{
    [DllImport("kernel32.dll")]
    static extern IntPtr LoadLibrary(string lpFileName);

    [DllImport("kernel32.dll")]
    static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    delegate XYZ* CalculatePathDelegate(
        uint mapId,
        XYZ start,
        XYZ end,
        bool straightPath,
        out int length);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    delegate void FreePathArr(XYZ* pathArr);

    static CalculatePathDelegate calculatePath;
    static FreePathArr freePathArr;

    static Navigation()
    {
        var currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var mapsPath = $"{currentFolder}\\Navigation.dll";
        var navProcPtr = LoadLibrary(mapsPath);

        var calculatePathPtr = GetProcAddress(navProcPtr, "CalculatePath");
        calculatePath = Marshal.GetDelegateForFunctionPointer(calculatePathPtr);

        var freePathPtr = GetProcAddress(navProcPtr, "FreePathArr");
        freePathArr = Marshal.GetDelegateForFunctionPointer(freePathPtr);
    }

    public static Position[] CalculatePath(uint mapId, Position start, Position end, bool straightPath)
    {
        var ret = calculatePath(mapId, start.ToXYZ(), end.ToXYZ(), straightPath, out int length);
        var list = new Position[length];
        for (var i = 0; i < length; i++)
        {
            list[i] = new Position(ret[i]);
        }
        freePathArr(ret);
        return list;
    }

    public static Position GetNextWaypoint(uint mapId, Position start, Position end, bool straightPath)
    {
        var path = CalculatePath(mapId, start, end, straightPath);

        if (path.Length <= 1)
            return path[0];

        return path[1];
    }
}

LocalPlayer.cs

public class LocalPlayer : WoWPlayer
{
    // OTHER
    const int SET_FACING_OFFSET = 0x9A8;
    const int PLAYER_SPELLS_BASE = 0x00B700F0;
    const int OBJECT_MANAGER_BASE = 0x00B41414;
    const int OBJECT_MANAGER_MAP_ID_OFFSET = 0xCC;

    // OPCODES
    const int SET_FACING_OPCODE = 0xDA;

    readonly IDictionary playerSpells = new Dictionary();

    internal LocalPlayer(
        IntPtr pointer,
        ulong guid,
        ObjectType objectType)
        : base(pointer, guid, objectType)
    {
        RefreshSpells();
    }

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

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

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

    public void LuaCall(string code) => Functions.LuaCall(code);

    public void RightClickUnit(IntPtr unitPtr) => Functions.RightClickUnit(unitPtr, 0);

    public uint MapId
    {
        get
        {
            var objectManagerPtr = MemoryManager.ReadIntPtr((IntPtr)OBJECT_MANAGER_BASE);
            return MemoryManager.ReadUint(IntPtr.Add(objectManagerPtr, OBJECT_MANAGER_MAP_ID_OFFSET));
        }
    }

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

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

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

WoWUnit.cs

public class WoWUnit : WoWObject
{
    // DESCRIPTORS
    const int RAGE_OFFSET = 0x60;
    const int CURRENT_SPELLCAST_OFFSET = 0xC8C;
    const int TARGET_GUID_OFFSET = 0x40;
    const int HEALTH_OFFSET = 0x58;
    const int MAX_HEALTH_OFFSET = 0x70;
    const int LEVEL_OFFSET = 0x88;
    const int BUFFS_BASE_OFFSET = 0xBC;
    const int DEBUFFS_BASE_OFFSET = 0x13C;
    const int DYNAMIC_FLAGS_OFFSET = 0x23C;
    const int FACING_OFFSET = 0x9C4;

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

    // OTHER
    const int NAME_OFFSET = 0xB30;
    const int POS_X_OFFSET = 0x9B8;
    const int POS_Y_OFFSET = 0x9BC;
    const int POS_Z_OFFSET = 0x9C0;

    internal WoWUnit(
        IntPtr pointer,
        ulong guid,
        ObjectType objectType)
        : base(pointer, guid, objectType)
    {
    }

    public virtual string Name
    {
        get
        {
            var ptr1 = MemoryManager.ReadInt(IntPtr.Add(Pointer, NAME_OFFSET));
            var ptr2 = MemoryManager.ReadInt((IntPtr)ptr1);
            return MemoryManager.ReadString((IntPtr)ptr2);
        }
    }

    public Position Position
    {
        get
        {
            var x = MemoryManager.ReadFloat(IntPtr.Add(Pointer, POS_X_OFFSET));
            var y = MemoryManager.ReadFloat(IntPtr.Add(Pointer, POS_Y_OFFSET));
            var z = MemoryManager.ReadFloat(IntPtr.Add(Pointer, POS_Z_OFFSET));

            return new Position(x, y, z);
        }
    }

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

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

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

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

    public bool IsCasting => CurrentSpellcastId > 0;

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

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

    public DynamicFlags DynamicFlags => (DynamicFlags)MemoryManager.ReadInt(GetDescriptorPtr() + DYNAMIC_FLAGS_OFFSET);

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

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

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

    public bool CanBeLooted => Health == 0 && DynamicFlags.HasFlag(DynamicFlags.CanBeLooted);

    IList Buffs
    {
        get
        {
            var buffs = new List();
            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;
        }
    }

    IList Debuffs
    {
        get
        {
            var debuffs = new List();
            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;
        }
    }

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

    // in radians
    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;
    }

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

    public bool IsFacing(Position position) => Math.Abs((GetFacingForPosition(position) - CurrentFacing)) < 0.3f;
}
Back to Top
...
Subscribe to the RSS feed to keep up with new chapters as they're released