Bloog Bot
Events
Right now our bot runs around killing enemies, but leaves all the juicy loot behind on their bodies. It would be nice to grab that before moving on to the next kill. If you've played WoW before, you know that looting works by right clicking the body of a dead unit which opens a Loot Frame that you can interact with by right clicking any items you want to take:
Before we teach our bot to loot bodies, we need to talk through some implementation details in the WoW Client. First, let's talk about the event system.
Good resources to check out before this discussion are the Wikipedia article on the publish-subscribe pattern and the chapter on Event Queues from Roberty Nystrom's Game Programming Patterns.
WoW has two primary components - the Client and the Server. The client runs on the player's machine, and is in constant communication with a separate program running on a remote server somewhere. Most of the game state lives on the server. The client communicates with the server constantly, sending updates to the server and receiving updates from the server. There are many reasons for this - we'll touch on three in particular. First, the game world is far too large for a single player's client to handle, and performance would suffer considerably. Second, in multiplayer games, exposing the client to all information about the game world would make it really easy for would-be cheaters to exploit the game. Third, the server has to exist as the single source of truth. Game networking is an incredibly complex topic, and there's no way to ensure that all clients can stay in sync with eachother, so the server exists as the single source of truth, and while clients do their best to display accurate information to the player, if there's any discrepancy between the game state on the client and the server, the client ultimately defers to the server to resolve it.
This means that the client and server are in constant communication with eachother. Communication takes the form of "events", and the WoW client has an event handler that receives packets from the server, and different game systems can react according to the information received. This is really useful for us because we can intercept those packets and use them to inform the decisions of our bot.
The first event we're going to listen for is the "LOOT_OPENED" event that the server sends to the client when the loot frame is opened on the client. You may be wondering why the loot frame being opened requires interaction from the server at all. The state of all units in the game is stored on the server - again, this fascilitates synchronization of all clients connected to the game. The loot stored on those units is also stored on the server. The client knows nothing about it until the unit dies and the player opens the loot frame. There's also some additional work that happens behind the scenes to populate a specific area of memory with information about the items stored in the loot frame. So before our bot can interact with the loot frame, it's important that we wait for all that work to finish. We can reliably determine when all that work is finished by listening for the "LOOT_OPENED" event.
There's a specific memory address that points to the first item stored in the loot frame. We can use that, and walk the collection based on the size of each LootItem object, to see all the items in the loot frame.
Another important detail that we need to understand is the concept of the item cache. Your WoW client actually caches all items that the player sees for performance reasons. The details associated with each item are actually stored in the /WDB folder in the itemcache.wdb file. wdb files use a proprietary encrypted database created by Blizzard to store game data. For performance reasons, the client caches any items seen by the player, and when the client requests information on a particular item, it first checks the itemcache before asking the server for information on the item. We'll use a function in the game client to retrieve an item cache entry based on the item's ID.
Another important function of the item cache is to implement the flyweight pattern. Take your average low-quality item in WoW. Vendor trash. Hundreds, maybe thousands of instances of that item may exist in the game world at any given time. It would be wasteful from a memory standpoint to duplicate any data that was identical between different instances of that item. Instead, that information is stored in the item cache. We'll have to write code to retrieve that information.
The ObjectManager's EnumerateVisibleObjects
function will enumerate items that are in the player's backpack, equipped by the player, and those items that can be interacted with in the game world, but it will not include the items in a loot frame. So to handle loot frames, our strategy for the rest of this chapter will roughly be something like this:
- Hook into the event handler in the WoW client
- Allow our bot to subscribe to events in the event handler
- Implement new type
LootFrame
that will be a container for all items in a loot frame. This will access the base address where loot items are stored in the WoW client, and walk that list to retrieve the list of itemIds that exist in the loot frame - Implement new types
WoWItem
(for use by the ObjectManager) andLootItem
(for use by the LootFrame). These types will have to go through the item cache in memory for certain information. - Write a callback to the "LOOT_OPENED" event, and subscribe to that event. This callback will instantiate a new
LootFrame
and include it in the EventArgs so any subscribers to this event can access it.
Let's start by hooking the event handler. If you search around on OwnedCore you'll see a lot of ways of accomplishing this. We're going to use a technique that I found in ZzukBot (credit to Zzuk - here's a link to his blog). This hook is going to be more complicated than what we've done in the past. The way the event handler is implemented in the WoW client is tricky. There are a number of overrides that are called in different ways depending on the number and type of arguments returned from the server with the event, and retrieving these arguments from the actual funtion that handles the event in the WoW client is tricky. So we're going to hook the method that calls into the event handler, grab all the arguments, then forward the call on to WoW's event handler. The WoW client's event handler depends on a very specific set of values being present in certain CPU registers, so we need to make sure all this state isn't altered by our hook. To do so, we're going to use assembly injection for the first time. We'll use Fasm.NET for assembly injection.
If you aren't familiar with assembly injection and the use of code caves, I strongly encourage you to read this excellent article at codeproject. If you don't care, you can always just copy this code and not worry about it because we won't have to touch it again.
Clone and build Fasm.net and add a reference to FasmNET.dll to the project, then ceate a new class called SignalEventManager
. As I mentioned, the event handling code in the WoW client uses a set of overrides to handle messages of different formats. There are two categories of messages we're concerned with - those with parameters, and those without. So we're going to have to hook two different functions in the WoW client to handle each of those cases. Our SignalEventManager
class will need access to the MemoryManager
, and we'll need two delegates for our two hooks, so the class starts out like this:
public static class SignalEventManager { const int SIGNAL_EVENT_FUN_PTR = 0x00703F76; const int SIGNAL_EVENT_NO_PARAMS_FUN_PTR = 0x00703E72; delegate void SignalEventDelegate(string eventName, string format, uint firstArgPtr); delegate void SignalEventNoArgsDelegate(string eventName); static SignalEventManager() { InitializeSignalEventHook(); InitializeSignalEventHookNoArgs(); } }
First let's hook the SignalEventNoArgs function. Add the following code to the SignalEventManager
class:
static SignalEventNoArgsDelegate signalEventNoArgsDelegate; static void InitializeSignalEventHookNoArgs() { signalEventNoArgsDelegate = new SignalEventNoArgsDelegate(SignalEventNoArgsHook); var addrToDetour = Marshal.GetFunctionPointerForDelegate(signalEventNoArgsDelegate); var instructions = new[] { "push esi", "call 0x007040D0", "pushfd", "pushad", "mov edi, [edi]", "push edi", $"call 0x{((uint) addrToDetour).ToString("X")}", "popad", "popfd", $"jmp 0x{((uint) SIGNAL_EVENT_NO_PARAMS_FUN_PTR + 6).ToString("X")}" }; var codeCave = MemoryManager.CreateCodecave(instructions); MemoryManager.InjectAssembly((uint)SIGNAL_EVENT_NO_PARAMS_FUN_PTR, "jmp " + codeCave); } static void SignalEventNoArgsHook(string eventName) => OnNewSignalEventNoArgs?.Invoke(eventName); internal delegate void SignalEventNoArgsEventHandler(string parEvent, params object[] parArgs); internal static event SignalEventNoArgsEventHandler OnNewSignalEventNoArgs;
We also need to add two new methods to MemoryManager
:
public static unsafe class MemoryManager { static readonly FasmNet fasm = new FasmNet(); ... static internal IntPtr CreateCodecave(string[] instructions) { fasm.Clear(); fasm.AddLine("use32"); foreach (var x in instructions) fasm.AddLine(x); var byteCode = fasm.Assemble(); var start = Marshal.AllocHGlobal(byteCode.Length); fasm.Clear(); fasm.AddLine("use32"); foreach (var x in instructions) fasm.AddLine(x); byteCode = fasm.Assemble(start); WriteBytes(start, byteCode); return start; } static internal void InjectAssembly(uint ptr, string instructions) { fasm.Clear(); fasm.AddLine("use32"); fasm.AddLine(instructions); var start = new IntPtr(ptr); var byteCode = fasm.Assemble(start); WriteBytes(start, byteCode); } }
I intend to revisit the topic of assembly injection in a later chapter and dissect this code in detail, but for now let's keep moving forward.
Let's hook the second function, SignalEventHook
(this one has args). Add this code to the SignalEventManager
:
static SignalEventDelegate signalEventDelegate; static void InitializeSignalEventHook() { signalEventDelegate = new SignalEventDelegate(SignalEventHook); var addrToDetour = Marshal.GetFunctionPointerForDelegate(signalEventDelegate); var instructions = new[] { "push ebx", "push esi", "call 0x007040D0", "pushfd", "pushad", "mov eax, ebp", "add eax, 0x10", "push eax", "mov eax, [ebp + 0xC]", "push eax", "mov edi, [edi]", "push edi", $"call 0x{((uint) addrToDetour).ToString("X")}", "popad", "popfd", $"jmp 0x{((uint) (SIGNAL_EVENT_FUN_PTR + 7)).ToString("X")}" }; var codeCave = MemoryManager.CreateCodecave(instructions); MemoryManager.InjectAssembly(SIGNAL_EVENT_FUN_PTR, "jmp " + codeCave); } static void SignalEventHook(string eventName, string typesArg, uint firstArgPtr) { var types = typesArg.TrimStart('%').Split('%'); var list = new object[types.Length]; for (var i = 0; i < types.Length; i++) { var tmpPtr = firstArgPtr + (uint)i * 4; if (types[i] == "s") { var ptr = MemoryManager.ReadInt((IntPtr)tmpPtr); var str = MemoryManager.ReadString((IntPtr)ptr); list[i] = str; } else if (types[i] == "f") { var val = MemoryManager.ReadFloat((IntPtr)tmpPtr); list[i] = val; } else if (types[i] == "u") { var val = MemoryManager.ReadUint((IntPtr)tmpPtr); list[i] = val; } else if (types[i] == "d") { var val = MemoryManager.ReadInt((IntPtr)tmpPtr); list[i] = val; } else if (types[i] == "b") { var val = MemoryManager.ReadInt((IntPtr)tmpPtr); list[i] = Convert.ToBoolean(val); } } OnNewEventSignalEvent(eventName, list); } internal static void OnNewEventSignalEvent(string parEvent, params object[] parList) => OnNewSignalEvent?.Invoke(parEvent, parList); internal delegate void SignalEventEventHandler(string parEvent, params object[] parArgs); internal static event SignalEventEventHandler OnNewSignalEvent;
You can see the callback here is more complicated. This callback takes 3 arguments. The first is the eventName, just like our first hook. But this hook handles events with one or more arguments. typesArg
is a %
separated string that stores the data types of the eventArgs. firstArgPtr
is a pointer to the value of the first arg, so we'll have to iterate memory at 4-byte intervals to get all the arg values.
All the assembly injection business allows us to retrieve the relevant information from the event, while preserving the call stack and CPU registers in such a way that we can forward the call on to the original destination in the WoW client and not break anything.
Both our hooks trigger events: OnNewEventSignalEvent
and OnNewSignalEventNoArgs
. Next we're going to create a new WoWEventHandler
class that subscribes to these events. Here's what that class looks like:
public static class WoWEventHandler { static WoWEventHandler() { OnNewSignalEvent += EvaluateEvent; OnNewSignalEventNoArgs += EvaluateEvent; } static void EvaluateEvent(string eventName, object[] args) { // if (eventName == "LOOT_OPENED") // do stuff // else if (eventName == "SOME_OTHER_EVENT") // do other stuff } }
I mentioned above that the specific event we were looking for was called "LOOT_OPENED"
. You can find a full list if you look around on OwnedCore. This thread seems to have most of them. Let's modify the WoWEventManager
class to listen for this event, then propogate another event that our bot can subscribe to. Notice we're evaluating the event on WoW's main thread - in this case, the Loot Frame's data structures are built internally by the game client on the main thread, and if we want to read memory from that location, our code also needs to be running on the main thread.
public static class WoWEventHandler { static WoWEventHandler() { OnNewSignalEvent += EvaluateEvent; OnNewSignalEventNoArgs += EvaluateEvent; } static void EvaluateEvent(string eventName, object[] args) { ThreadSynchronizer.RunOnMainThread(() => { if (eventName == "LOOT_OPENED") { OpenLootFrame(); } // ..else }); } public static event EventHandlerOnLootOpened; static void OpenLootFrame() { var lootFrame = new LootFrame(); OnLootOpened?.Invoke(this, new OnLootFrameOpenArgs(lootFrame)); } } public class OnLootFrameOpenArgs : EventArgs { public readonly LootFrame LootFrame; internal OnLootFrameOpenArgs(LootFrame lootFrame) { LootFrame = lootFrame; } }
We haven't implemented the LootFrame
class yet, but we'll do that shortly. First, let's wire WoWEventHandler
up with our bot. Inside MainViewModel, add a new parameterless constructor that we'll use for any initialization code we need, then add and a new event listener to the WoWEventHandler.OnLootOpened
event:
public class MainViewModel : INotifyPropertyChanged { ... public MainViewModel() { WoWEventHandler.OnLootOpened += (sender, e) => { foreach (var lootItem in e.LootFrame.LootItems) { Console.WriteLine($"{lootItem.ItemId}: {lootItem.Name}"); } }; } }
Now whenever the client receives the "LOOT_OPENED" event from the server, we'll see some text written to the console that we can use for debugging. But before we try this out, we need to create the LootFrame
class. We're also going to create a new LootItem
class. LootItems and WoWItems are similar, but different enough that we're going to keep them separate. The functionality they share is that they both access certain information through the item cache, so we'll encapsulate that shared behavior in another new ItemCacheEntry
struct and add a reference to that type to both WoWItem
and LootItem
. Here's the code for the LootFrame
and LootItem
classes (I'm putting them both in the same file).
internal class LootFrame { const int LOOT_FRAME_ITEMS_BASE_PTR = 0x00B7196C; readonly public IList<LootItem> LootItems = new List<LootItem>(); internal LootFrame() { for (var i = 0; i <= 15; i++) { var itemId = MemoryManager.ReadInt((IntPtr)(LOOT_FRAME_ITEMS_BASE_PTR + i * 0x1c)); if (itemId == 0) break; LootItems.Add(new LootItem(itemId, i)); } } } internal class LootItem { internal LootItem( int itemId, int lootSlot) { ItemId = itemId; LootSlot = lootSlot; } internal int LootSlot { get; set; } internal int ItemId { get; set; } }
We know the base address of the loot frame's item collection in memory (0x00B7196C
, so we start there, iterating by the size of each item object in memory (0x1c
bytes), 16 times (that's the max amount of items that a loot frame can contain). And because we received the "LOOT_OPENED" event from the server, we can safely assume that those items will exist in memory. We're also going to store the LootSlot for each item. When it comes to actually looting items from the loot frame, we're going to call a function in the WoW client that takes the LootSlot as a parameter. We can simply use the iteration index (0 through 15) as the LootSlot for each item.
The last bit that we need to implement is the ItemCache that I mentioned earlier. Certain properties, like the item's name, exist in the ItemCache. We can use a function in the WoW client to find the item cache entry for a given itemId. First add that to Functions
:
const int GET_ITEM_CACHE_ENTRY_FUN_PTR = 0x0055BA30; const int ITEM_CACHE_BASE_PTR = 0x00C0E2A0; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] delegate IntPtr ItemCacheGetRowDelegate( IntPtr itemCacheBasePtr, int itemId, ref ulong guid, IntPtr callbackPtr, int unused1, int unused2); static readonly ItemCacheGetRowDelegate GetItemCacheEntryFunction = Marshal.GetDelegateForFunctionPointer((IntPtr)GET_ITEM_CACHE_ENTRY_FUN_PTR); static internal IntPtr GetItemCacheEntry(int itemId) { ulong guid = 0; return GetItemCacheEntryFunction( (IntPtr)ITEM_CACHE_BASE_PTR, itemId, ref guid, IntPtr.Zero, 0, 0); }
This function is a bit weird. The first parameter is the pointer to the beginning of the ItemCache - we know that from disassembling the WoW client. The second parameter is the ItemId that we want to get the ItemCacheEntry for. The third parameter is a ref parameter that gets set by the function to the guid of the item. We don't care about this, so we can ignore it. The fourth parameter is the pointer to a callback function that will get called (similar to the callback we provided for EnumerateVisibleObjects
) - we don't need this, so we can just pass in IntPtr.Zero
and ignore it. To be honest, I don't know what the last two parameters are for, but we can pass in 0 and ignore them.
Notice that GetItemCacheEntry
returns an IntPtr
. This is the pointer to the location of the ItemCacheEntry in memory. Let's add a new method to MemoryManager
that will allow us to convert that IntPtr
into an ItemCacheEntry
object:
public static unsafe class MemoryManager { ... [HandleProcessCorruptedStateExceptions] static internal ItemCacheEntry ReadItemCacheEntry(IntPtr address) { try { return *(ItemCacheEntry*)address; } catch (AccessViolationException ex) { Console.WriteLine("Access Violation on " + address + " with type ItemCacheEntry"); return default; } } ... }
And we need to actually implement the ItemCacheEntry
struct. The advantage of using a struct
over a class
is that we can use [StructLayout(LayoutKind.Explicit)]
, and give each field a [FieldOffset(0x8)]
attribute and our fields will automatically be populated with the right values using the offsets we provide when we dereference the pointer from the MemoryManager
. But the fields on the ItemCacheEntry
are actually pointers, not the values themselves, so we're going to have to write helper methods to read the correct values from memory. For that reason, we're going to wrap the ItemCacheEntry
with another class, ItemCacheInfo
that will expose methods to get the values that we care about. The code should make it clear - I've added a single field to the ItemCacheEntry
to start which is the pointer to the item's name:
internal class ItemCacheInfo { readonly ItemCacheEntry itemCacheEntry; internal ItemCacheInfo(ItemCacheEntry itemCacheEntry) { this.itemCacheEntry = itemCacheEntry; } internal string Name => MemoryManager.ReadString(itemCacheEntry.NamePtr); } [StructLayout(LayoutKind.Explicit)] struct ItemCacheEntry { [FieldOffset(0x8)] internal IntPtr NamePtr; }
Now let's modify LootFrame
to use the new GetItemCacheEntry
method and the new ItemCacheInfo
to make sure new LootItems
get a reference to their ItemCacheEntry:
internal class LootFrame { const int LOOT_FRAME_ITEMS_BASE_PTR = 0x00B7196C; readonly public IList<LootItem> LootItems = new List<LootItem>(); internal LootFrame() { for (var i = 0; i <= 15; i++) { var itemId = MemoryManager.ReadInt((IntPtr)(LOOT_FRAME_ITEMS_BASE_PTR + i * 0x1c)); if (itemId == 0) break; var itemCacheEntry = MemoryManager.ReadItemCacheEntry(Functions.GetItemCacheEntry(itemId)); var itemCacheInfo = new ItemCacheInfo(itemCacheEntry); LootItems.Add(new LootItem(itemCacheInfo, itemId, i)); } } } internal class LootItem { internal LootItem( ItemCacheInfo itemCacheInfo, int itemId, int lootSlot) { Info = itemCacheInfo; ItemId = itemId; LootSlot = lootSlot; } internal ItemCacheInfo Info { get; } internal int ItemId { get; set; } internal int LootSlot { get; set; } }
At this point we're ready for a test. Fire up the bot, kill something and manually open it's loot frame, and you should see any items contained in the loot frame printed to the console:
You can also add a Console.WriteLine statement to WoWEventHandler.EvaluateEvent
to see all the events that the client receives from the server. It's quite interesting:
The WoWEventHandler
class will continue to be useful as we move forward. It's a lot of work to get it wired up, but it's worth it.
All that remains is to actually teach our bot to take items from the loot frame. Like I mentioned earlier, there's a LootSlot
function in the WoW client that loots the item at a given container slot (0-15). This is why we added the LootSlot
field on the LootItem
class. LootSlot
uses FastCall, so first we need to add another function to our FastCall
library:
void __declspec(dllexport) __stdcall LootSlot(int slot, unsigned int ptr) { typedef void __fastcall func(unsigned int slot, int unused); func* f = (func*)ptr; f(slot, 0); }
Then add the new method to our Functions
class:
const int LOOT_SLOT_FUN_PTR = 0x004C2790; [DllImport("FastCall.dll", EntryPoint = "LootSlot")] static extern byte LootSlot(int slot, IntPtr ptr); static internal void LootSlot(int slot) => LootSlot(slot, (IntPtr)LOOT_SLOT_FUN_PTR);
Then add a method to the new LootItem
class that calls into this new method:
internal class LootItem { internal LootItem( ItemCacheInfo itemCacheInfo, int itemId, int lootSlot) { Info = itemCacheInfo; ItemId = itemId; LootSlot = lootSlot; } internal int LootSlot { get; set; } internal int ItemId { get; set; } internal ItemCacheInfo Info { get; } internal void Loot() => Functions.LootSlot(LootSlot); }
With all that done, we're now going to create a new LootState
that we'll push onto the stack after combat ends. The LootState
will subscribe to WoWEventHandler.OnLootOpened
(in the same way that we tested from MainViewModel
earlier) so that our LootState
will get notified when the client receives the "LOOT_OPENED" event from the server. Then we'll iterate over all items in the LootFrame
and call the Loot
method on each one. The one "gotchya" here is that we can't loot items too quickly or we'll get disconnected from the server. The WoW server has protections in place to prevent people from crashing the server by spamming it with requests, so if we try to loot 5 items in the same frame, we'll run into trouble. To get around that, we're going to add a new Wait
class that will allow us to wait an arbitrary amount of time before executing a piece of code. Credit goes to Zzuk for the Wait class - the thread safety probably isn't necessary because our code will always be excuting from the main thread, but it can't hurt. Here's the code for our new LootState
and Wait
classes:
public static class Wait { static readonly ConcurrentDictionary<string, Item> Items = new ConcurrentDictionary<string, Item>(); static readonly object _lock = new object(); public static bool For(string parName, int parMs, bool trueOnNonExist = false) { lock (_lock) { if (!Items.TryGetValue(parName, out Item tmpItem)) { tmpItem = new Item(); Items.TryAdd(parName, tmpItem); return trueOnNonExist; } var elapsed = (DateTime.UtcNow - tmpItem.Added).TotalMilliseconds >= parMs; if (elapsed) { Items.TryRemove(parName, out tmpItem); } return elapsed; } } public static void Remove(string parName) { lock (_lock) { Items.TryRemove(parName, out Item tmp); } } public static void RemoveAll() { lock (_lock) { Items.Clear(); } } class Item { internal DateTime Added { get; } = DateTime.UtcNow; } }
class LootState : IBotState { readonly Stack<IBotState> botStates; readonly LocalPlayer player; LootFrame lootFrame; int lootIndex = 0; internal LootState(StackbotStates, WoWUnit target) { this.botStates = botStates; player = ObjectManager.Player; WowEventHandler.OnLootOpened += WowEventHandler_OnLootOpened; } public void Update() { if (player.Position.DistanceTo(target.Position) >= 3) { player.ClickToMove(target.Position); } if (target.CanBeLooted && lootFrame == null && player.Position.DistanceTo(target.Position) < 3) { player.ClickToMoveStop(); player.RightClickUnit(target.Pointer); return; } // State Transition Conditions: // - target can't be looted (no items to loot) // - loot frame is open, but we've already looted everything we want if (!target.CanBeLooted || (lootFrame != null && lootIndex == lootFrame.LootItems.Count)) { player.ClickToMoveStop(); WoWEventHandler.OnLootOpened -= WowEventHandler_OnLootOpened; botStates.Pop(); return; } if (lootFrame != null && Wait.For("LootDelay", 150)) { lootFrame.LootItems.ElementAt(lootIndex).Loot(); lootIndex++; } } void WowEventHandler_OnLootOpened(object sender, OnLootFrameOpenArgs e) => lootFrame = e.LootFrame; }
So let's talk about what's happening in LootState
. We subscribe to the OnLootOpened
event with a method that simply sets a private variable with the LootFrame
object included in the eventargs. Notice that we also unsubscribe from that event before popping this state off the stack. We keep track of how many items we've looted, and as soon as we're looted all the items from the LootFrame
we pop the current state and return. We still need to do is implement the function that actually lets us right click on the corpse to open the loot frame. That's the RightClickUnit
you see in LootState
. Add this code to Functions
:
const int RIGHT_CLICK_UNIT_FUN_PTR = 0x60BEA0; [UnmanagedFunctionPointer(CallingConvention.ThisCall)] delegate void RightClickUnitDelegate(IntPtr unitPtr, int autoLoot); static readonly RightClickUnitDelegate RightClickUnitFunction = Marshal.GetDelegateForFunctionPointer((IntPtr)RIGHT_CLICK_UNIT_FUN_PTR); internal void RightClickUnit(IntPtr unitPtr, int autoLoot) => RightClickUnitFunction(unitPtr, autoLoot);
Notice this function takes a parameter named "autoLoot". If you pass in a 1
here, you'll autoloot the enemy in the same way you would if you shift-rightclicked the corpse. You can certainly handle things this way and avoid the item iteration and throttling with Thread.Sleep
, but using the code I showed above you have a lot more control over the items you choose to loot. For example, you can only take magical items, or you can ignore quest items, etc. We'll do some of that later on.
Last, we need to implement the (target.CanBeLooted
code you see in LootState
. This is going to be reading from a Dynamic Flags field that all WoWUnits (and their derived types) have. Create a new class called DynamicFlags.cs
:
[Flags] public enum DynamicFlags { Untouched = 0x0, CanBeLooted = 0x1, IsMarked = 0x2, Tapped = 0x4, TappedByMe = 0x8 }
Then add the following code to WoWUnit.cs
:
public DynamicFlags DynamicFlags => (DynamicFlags)MemoryManager.ReadInt(GetDescriptorPtr() + DYNAMIC_FLAGS_OFFSET); public bool CanBeLooted => Health == 0 && DynamicFlags.HasFlag(DynamicFlags.CanBeLooted);
We're finally ready for a final test. Fire up the bot, click Start, and you should see a fully autonomous killing and looting machine:
This chapter got long, but we added a very handy tool to our toolkit, the WoWEventHandler
. In the next chapter we're going to talk about Microsoft's Managed Extensibility Framework (MEF) which will allow us to modify, compile, and reload our bot at runtime which will let us iterate much more quickly.