Bloog Bot

Chapter 13
Drew Kestell, 2018
[email protected]

Bytecode

Again I direct you to Robert Nystrom's Game Programming Patterns to read the chapter on Bytecode. It does a perfect job of explaining why WoW exposes a Lua API for addon creation. Go read that before moving on.

So - here's the gist. Generally speaking, you want to use a lower level language like C++ to build your game engine for performance reasons, but your game designers (think of the people doing level design, or spell design, story work, cinematics) need a way to iterate quickly and not be bogged down by the nuances of a low level language like C++. This is where the bytecode pattern really shines. Your core engine can expose its own API that lets your game designers write code in a higher level language, but still have access to the game's core functions. Another added benefit of this approach is that you can selectively choose which core engine functions you expose to your designers. You don't want them to have access to sensitive information, and you may want to restrict what they're able to do to prevent things from becoming too powerful or unfair.

In the case of the WoW client, their designers are also their players. The Lua API allows players to design addons to extend the functionality of the WoW client. I think this sort of thing is so cool. It's a really interesting programming problem to implement, and it's closely related to the open-closed principle. Due to the fact that the WoW client exposes it's Lua API to it's own player base who has an inherent self interest in trying to take advantage of such a system, it's especially important that the WoW client restricts access to a limited subset of functionality.

So what's the point of all this discussion about Lua? Well, when it comes to writing the behavioral logic for our bot, we're going to be performing actions like casting spells, attacking monsters, and gathering information about the world around us, and a lot of that behavior is exposed by the Lua API. Sure, all of this underlying functionality is exposed by the WoW engine somewhere, but why go through the laborious process of reverse engineering the client to find the memory location and function parameters, then wiring up the delegate function, when we can just call the function from the Lua API?

Some of you may be wondering why we don't use the Lua API for everything? Like EnumerateVisibleObjects, for example. As I mentioned earlier, the developers that used the bytecode pattern to implement the Lua API in the Wow engine were very deliberate in choosing which functions to expose. There's really no reason why a player should be able to see all units in an area, and there's obvious potential for abuse.

Throughout the rest of this series, there will be a clear distinction between functionality exposed by the Lua API, and functionality we'll have to get at by calling straight into the WoW engine. The former will be a bit faster to implement, so we'll use the Lua API when it's appropriate.

So how do we hook into the Lua API from our bot? There's a single function in the WoW client that accepts a Lua string and tries to interpret it. Adding that function to our bot is similar to other functions we've added. Find the offset, add the supplementary code to the Functions file, then call that function from the WoWPlayer class. Additionally, this code uses Fastcall, so just like our EnumerateVisibleObjects function, we're going to have to add a function to our FastCall library, then hook into that from our bot.

FastCall/dllmain.cll

void __declspec(dllexport) __stdcall LuaCall(char* code, unsigned int ptr)
{
    typedef void __fastcall func(char* code, const char* unused);
    func* f = (func*)ptr;
    f(code, "Unused");
    return;
}

Functions.cs

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) => LuaCallFunction(code, LUA_CALL_FUN_PTR);

WoWPlayer.cs

internal void LuaCall(string code) => functions.LuaCall(code);

Unfortunately I don't know of a very good place to find documentation for the 1.12.1 client's API. This site is pretty good, but the documentation is for the modern version of the WoW client. Nonetheless, you can usually find what you're looking for. For example, we're going to try to make the character jump using the Lua API. The command we want to use in the 1.12.1 client is Jump() but you won't find that in the list of functions at the wowwiki site. The closest function is JumpOrAscendStart(). If you look at that page, you'll see: "Makes the player jump, this is a protected function that replaces Jump().". The API has changed over time, and the Jump() function no longer exists. Long story short - you can find what you're looking for if you dig, and if all else fails, the 1.12.1 dump thread at OwnedCore is a great resource.

So, let's see if we can make the character jump using the Lua API. Let's change our UI to remove a few old test buttons and add a new button that will call our new LuaCall method with the string "Jump()". Here's the code from MainViewModel:

ICommand jumpCommand;

void Jump() => objectManager.Player.LuaCall("Jump()");

public ICommand JumpCommand =>
    jumpCommand ?? (jumpCommand = new CommandHandler(Jump, true));

You'll also have to modify MainWindow.xaml to wire up the button with the JumpCommand. Fire up the client, log in, and click the button to make the player jump.

Nothing happens. What gives?

When WoW first came out, the Lua API was quite extensive. Over time, clever players found ways of exploiting the system to gain an unfair advantage over other players. This led to a number of patches which restricted access to many of the functions exposed in the Lua API. I direct you to this article for more information. Instead of just removing all sensitive functions, the developers decided to restrict access to these functions and put them behind a permissions flag. I don't know the history of this decision, but I'm assuming it had something to do with either maintaining backward compabitility, or giving GMs access to certain functionality while restricting access to the average player.

"Jump()" is one of these protected functions. Thankfully there's a flag that we can modify in memory that allows us to access the protected Lua functions. Add the following line of code to the constructor of MainViewModel to unlock the protected Lua functions:

memoryManager.WriteBytes((IntPtr)LUA_UNLOCK, new byte[] { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xc3 });

Fire up your client and test it again. You're likely to see one of two things happen. Your player will either jump, or your game will crash. If you hammer on the jump button enough, your game will inevitably crash. What's happening here?

The WoW client is a multi-threaded application, and it uses something called thread-local storage. In a multi-threaded application, there are situations where a single global variable isn't appropriate. If multiple threads read and write that variable, you may end up with race conditions and other threading-related issues. Thread-local storage is a solution to that problem. Essentially, a variable appears to be global, but in fact each thread has its own instance of that variable.

Our bot has an ObjectManager class that calls EnumerateVisibleObjects. This is exactly what happens internally in the WoW client (where do you think we got the idea?). The internal object manager is only available to the main thread in the WoW client, and much of the Lua interpreting engine is dependent on the object manager, so all Lua scripts are interpreted and executed on the main thread. When we call LuaCall from a thread that is NOT the main thread (Thread2 let's say), the WoW client interprets and executes that script on Thread2, but the object manager is not available from Thread2 (or more accurately - it may be partially available, or in a weird state), and this is going cause our bot to crash with an AccessViolationException as we attempt to read/write protected memory. This thread at OwnedCore has more information if you're curious.

We can address this issue by making sure that our LuaCall function is only called from the game's main thread, and there are a number of ways of accomplishing that. This blog post describes a technique using SetWindowLongPtr and WndProc that I prefer. The WoW client has its main window running on the main thread, and the WndProc function is used to handle input and other messages on a given window, so by hooking into WndProc, we can execute arbitrary code and ensure it's executing on the main thread. The blog post does a great job of explaining the technique, so I encourage you to read that first. Let's work on our implementation.

The vast majority of our functionality will exist in a new ThreadSynchronizer class. If you've read the blog post in the previous paragraph you should be able to understand the code, but we'll walk through it either way.

public class ThreadSynchronizer
{
    [DllImport("user32.dll")]
    static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll")]
    static extern int CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, int Msg, int wParam, int lParam);

    [DllImport("user32.dll")]
    static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll")]
    static extern int GetWindowTextLength(IntPtr hWnd);

    [DllImport("user32.dll")]
    static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

    [DllImport("user32.dll")]
    static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [DllImport("user32.dll")]
    static extern int SendMessage(
        int hWnd,
        uint Msg,
        int wParam,
        int lParam
    );

    delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    delegate int WindowProc(IntPtr hWnd, int Msg, int wParam, int lParam);

    readonly Queue<Action> actionQueue = new Queue

Here's the high level: we want to be able to execute arbitrary code on the WoW client's main thread. Why does this not happen from our bot by default? When we fire up out bot, everything starts from the Bootstrapper project. Remember that we call CreateRemoteThread to invoke our WPF application from within the WoW process, but it starts execution in a different thread than the main thread of the WoW client (this is by design - if the WPF application ran on the main thread it would be blocked by the WoW client and the UI wouldn't work). The technique outlined in the article solves this problem that by hooking into the CallWindowProc function which is part of the Windows API. CallWindowProc is called automatically whenever a message is sent to that window (examples of messages are mouse movements or clicks, or keyboard events). So we're going to essentially override the CallWindowProc function, modifying it to first execute our custom code, then forward on to the default CallWindowProc function that will handle input and do whatever else the WoW client needs it to do.

Overriding CallWindowProc is done by calling SetWindowLong, also part of the Windows API. It takes three parameters: the handle to the window, the index, and the new value. We pass in GWL_WNDPROC (-4) as the index which specifies that we want to change the memory address of the CallWindowProc function, providing the memory address of our custom WndProc function (obtained by calling Marshal.GetFunctionPointerForDelegate()). SetWindowLong returns the original value that we're overriding, and we store a reference to that so we can forward the call on to the original CallWindowProc function after executing our code that we want to run on the main thread. All this happens in the ThreadSynchronizer constructor.

Another tricky bit is how we actually find the window handle that we need to pass into SetWindowLong. We do that with the EnumWindows function, also part of the Windows API. Unfortunately we can't do something simple like Process.GetProcessesByName("WoW")[0].MainWindowHandle because what is considered the "main window" is somewhat arbitrary - it's actually the first window that gets created when a process starts. If you try it, it won't work. Instead, we use EnumWindows, also part of the Windows API. This function takes as its first parameter a callback function that it will call on every window. We define that callback as FindWindowProc which has some logic to find the right window. It uses a few other functions from the Windows API, IsWindowVisible, GetWindowTextLength, and GetWindowText, to ignore certain windows that we know we don't care about, which will improve performance.

The SendUserMessage function is how you send messages (mouse and keyboard events, etc) to a window. If we excluded this bit of functionality, our custom code in the WndProc override would only get called on a frame that the window received a message (if the user moved their mouse, or used their keyboard). This obviously isn't ideal. Thankfully we can get around that by manually sending a message to the window. That brings us to the last piece of this code that we need to talk about - the RunOnMainThread method.

This is actually what we'll call from our bot to queue an action that we want to execute on the main thread. We use a Queue to enqueue actions that we want to execute on the main thread. We only try to dequeue a single action each time WndProc is called to avoid locking up the main thread for too long and slowing down the game. Then we call SendUserMessage() to make sure WndProc actually gets called.

Before showing off our new RunOnMainThread method, let's do some housekeeping. Newing up our dependencies from the constructor of MainViewModel is pretty nasty, so instead we're going to create all these dependencies from the entry point of our bot in App.xaml.cs. Unfortunately, configuring your view model in WPF to accept constructor parameters is a pain in the ass, so for now we're just going to expose some public getters and setters and set them from the constructor of MainWindow.xaml.cs. After moving everything out of MainViewModel, here's what App.xaml.cs looks like:

public partial class App : Application
{
    [DllImport("Kernel32")]
    public 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();
        
        var functions = new Functions();
        var memoryManager = new MemoryManager();
        var objectManager = new ObjectManager(functions, memoryManager);
        var threadSync = new ThreadSynchronizer();

        // throttle framerate to fix ClickToMove on higher refresh rate monitors
        var directXManager = new DirectXManager(memoryManager);
        directXManager.ThrottleFPS();

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

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

        // call EnumerateVisibleObjects every 3 seconds
        EnumObjects();

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

        base.OnStartup(e);

        async void EnumObjects()
        {
            while (true)
            {
                if (functions.IsLoggedIn())
                {
                    objectManager.EnumerateVisibleObjects();
                }

                await Task.Delay(2000);
            }
        }
    }
}

... and here's MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    public MainWindow(Functions functions, ObjectManager objectManager, ThreadSynchronizer threadSync)
    {
        InitializeComponent();
        var context = (MainViewModel)DataContext;
        context.Functions = functions;
        context.ObjectManager = objectManager;
        context.ThreadSynchronizer = threadSync;
    }
}

And here's MainViewModel:

public class MainViewModel : INotifyPropertyChanged
{
    readonly Bot bot = new Bot();

    public ObservableCollection<string> ConsoleOutput { get; }

    public Functions Functions { get; set; }
    public ObjectManager ObjectManager { get; set; }
    public ThreadSynchronizer ThreadSynchronizer { get; set; }

    public MainViewModel()
    {
        ConsoleOutput = new ObservableCollection<string>();
    }

    #region Commands

    // Start command
    ICommand startCommand;

    void Start() => bot.Start();

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

    // Stop command
    ICommand stopCommand;

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

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

    // Jump command
    ICommand jumpCommand;

    void Jump() =>
        ThreadSynchronizer.RunOnMainThread(() => { ObjectManager.Player.LuaCall("Jump()"); });

    public ICommand JumpCommand =>
        jumpCommand ?? (jumpCommand = new CommandHandler(Jump, true));

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

Fire up the bot, and you can now hammer on the Jump button to your heart's content without crashing thanks to our LuaCall invocations executing on the main thread. Moving forward, anytime we use LuaCall we'll have to make sure it's executing on the main thread or we risk crashing.

If you look through the API documentation here at the vanilla-wow wikia, you'll see all sorts of functions that will be useful for designing the logic of our bot. Having the Lua API unlocked is going to really speed up development. In the next chapter we're going to teach our bot to fight.

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

Comments? Leave me a note: