Hearthstone has been my favorite game for several years, but there’s a small issue I find annoying.
In case you are not familiar with the game, you first have to launch Battle.net. A login dialog will popup and then Batlle.net’s main window will display. After that, you can select your game (Hearthstone in this case) and then hit Play to start the game.
In Battle.net’s case, due to the preliminary login dialog, even if you tell Windows to start the program maximized, the main window won’t be maximized and you’ll have to maximize it manually. After that, when the actual Hearthstone window is launched, there isn’t an option to start that window in maximized state either (there’s an option to display in Full Screen mode, but it covers the taskbar and that’s not what I want).
So I got annoyed of manually maximizing both windows every time and decided to code a little utility to do it for me. My last post was related to WebAssembly, so I decided to code this using “regular” assembly 😉. It might sound scary or difficult, but MASM (macro assembler) has some very nice directives that resemble high level programming.
Disclaimer: I have a customized DPI setup on my PC so maybe the issues I have with the way Hearthstone and Battle.net resize their windows is related to that. I also have one monitor only, so I haven’t tested if the utility works fine on a different setup.
First we need MASM and a collection of libraries and include files for Windows programming. For this purpose, I’m going to use the MASM32 SDK.
Next I’ve created a Visual Studio Project following these instructions.
As for debugging, it’s very useful to enable the following views in Visual Studio (they become available when you start debugging):
- Debug / Windows / Registers
- Debug / Windows / Disassembly
- Debug / Windows / Memory
By default, Visual Studio does not support syntax highlighting for ASM files, but the AsmDude extension can provide it. The default colors are quite ugly for my taste, so you might want to customize them. This is my configuration in case you want to replicate it. It’s for the Dark color theme.
I’ve also disabled the following options because the extension was getting confused when dealing with API calls.
Finally, by default, the extension highlights words when you click on them. This is not standard behavior so you might want to disable it as well.
What are we going to build
- A simple utility that will periodically ask the operating system if Battle.net or Hearthstone are running. If that’s the case, the operating system will provide us the application’s handle (it’s an identifier).
- Once we have the handle, we can use it to simulate a maximize event using a Windows API.
- We don’t want the utility to be running forever, so it will quit after it’s job has been completed.
- Just to play safe, the utility will have an icon in the system tray so manual termination will be possible if needed.
Basic x86 architecture information (32-bit)
If you’ve never seen assembly before, I would recommend reading this guide which explains registers and instructions in more detail.
In any case, registers store 32-bit values and are used for temporary data storage and memory access.
If you want to refer to the whole 32-bit value, you can use EAX. AX gives you access to the rightmost 16 bits (called low-order bits) and, in turn, AX can be further divided into AH and AL.
It’s important to know that by convention the Win32 API returns values in the EAX register. That’s why you’ll constantly see an instruction dealing with EAX (assignment or comparison) after calling a Win32 function. Besides EAX, Win32 API functions are also free to modify ECX and EDX.
Teaching assembly or the Win32 API is out of the scope of this post, but I’ll try to provide a high-level overview of how the program works. Just in case, the source code is available if you are interested.
Masm32rt.inc is a convenient MASM32 include file that contains the most common Windows libraries and it also defines which kind of executable we want to create. Here is an extract of the file for you to get an idea.
.486 ; create 32 bit code .model flat, stdcall ; 32 bit memory model option casemap :none ; case sensitive include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
In this section we are just defining the signature of a custom function named Maximize. The function will receive a parameter of type HANDLE (it’s a type defined by Windows).
Maximize proto :HANDLE
This is how we can define constants, initialized data and uninitialized data (stuff that you can only know at runtime) in assembly.
;*********************** CONSTANTS ************************** HS_ICON equ 1 ID_TIMER equ 2 IDM_EXIT equ 3 WM_SHELLNOTIFY equ WM_USER + 5 ;************************* DATA **************************** .data szClassName db "HS_Resizer_Class", 0 szDisplayName db "Hearthstone Resizer", 0 szExit db "Exit", 0 szMutex db "HS_resizer", 0 szHearthstoneTitle db "Hearthstone", 0 szBattlenetTitle db "Blizzard Battle.net", 0 ; Window handles hHearthstone HWND 0 hBattlenet HWND 0 ; cntResizeAttempts DWORD 0 maxResizeAttempts DWORD 10 cntAppsNotFound DWORD 0 maxAppsNotFound DWORD 30 timeout WORD 2000 ; milliseconds .data? hMutex HANDLE ? hInstance HMODULE ? hResizerIcon HANDLE ? hPopupMenu HMENU ? notifyData NOTIFYICONDATA <?>
You can read more about data types here.
If you are wondering about strings, “db” declares an array of bytes. The contents are given by the value enclosed in quotation marks and the array is terminated by a NUL character (0).
Finally, notice that there are some types defined by Windows such as HANDLE, HMODULE, etc. They are just an alias for a 32-bit value.
Having multiple instances shouldn’t produce any bad effects, but there’s no good reason to allow it, so we are going to make sure that only one instance of our utility is running.
As mentioned earlier, the Win32 API returns values in the EAX register. In this case, CreateMutex creates a mutex object and returns a handle to it. In this snippet, we are storing EAX’s value in the hMutex variable. If the function fails because the mutex already exists, then we know that another instance is running and we jump to the Done label which will end execution.
.code Resizer: invoke CreateMutex, NULL, FALSE, addr szMutex mov hMutex, eax invoke GetLastError cmp eax, ERROR_ALREADY_EXISTS jne Continue ; if it's already running then fine, end this instance jmp Done Continue: call StartUp Done: invoke CloseHandle, hMutex invoke ExitProcess, eax
There are some other things to highlight in this snippet:
- Invoke is a high level MASM directive and it’s useful because you can call a function just as in a high level language. For comparison purposes, this is what the CreateMutex call would look like without using invoke. Notice that we have to push the parameters to the stack in reverse order, so that the function pops them in the correct order.
push offset szMutex push FALSE push NULL call CreateMutex
- Branching based on a condition requires two instructions. Cmp compares two operands and sets some flags in the processor. Then jne (jump if not equal) evaluates the flags and branches to the Continue label if the condition is met.
- Jmp is an unconditional jump and it will always branch to the Done label if the jmp instruction is reached.
In the following code snippet, we call GetModuleHandle to get a handle to the file used to create the calling process.
After that, we need to create what Windows calls “window class attributes”, which basically are values used to define the app’s behavior. First we need to setup a structure named WNDCLASSEX. We won’t have an actual visible window so we only need to setup a few structure members.
We also register our “class” and create our invisible window. Notice that we are passing a function named WndProc as a callback as we are going to be explaining it later.
Finally, we setup the message loop.
StartUp proc LOCAL msg:MSG LOCAL wc:WNDCLASSEX invoke GetModuleHandle, NULL mov hInstance, eax ; we won't be using the other members of the structure so we init everything with zero invoke memfill, addr wc, sizeof WNDCLASSEX, 0 mov wc.cbSize, sizeof WNDCLASSEX mov wc.hInstance, eax mov wc.lpszClassName, offset szClassName mov wc.lpfnWndProc, offset WndProc ; Register the window and create it invoke RegisterClassEx, addr wc invoke CreateWindowEx, NULL, addr szClassName, addr szDisplayName, NULL, NULL, \ NULL, NULL, NULL, HWND_MESSAGE, \ NULL, hInstance, NULL .while TRUE invoke GetMessage, addr msg, NULL, 0, 0 .break .if !eax invoke TranslateMessage, addr msg invoke DispatchMessage, addr msg .endw mov eax, msg.message ret StartUp endp
Notice that we are also using the .While directive, which simulates high level languages looping functionality.
Remember the callback that I mentioned earlier? This callback will be executed when new messages arrive and has the following signature:
WndProc proc hWin:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
Keep in mind that WPARAM and LPARAM are general purpose values and will have a specific meaning depending on the message we are receiving.
System tray and timer setup
As mentioned earlier, we don’t want to display a window, but it’s a good idea to display an icon in the System tray to allow manual termination if something goes wrong. We do this when we receive the WM_CREATE message.
As a last step, we also set a timer because we want to periodically check if Battle.net and Hearthstone are running.
.if uMsg == WM_CREATE ; create popup and add our only entry invoke CreatePopupMenu mov hPopupMenu, eax invoke AppendMenu, hPopupMenu, MF_STRING, IDM_EXIT, addr szExit ; load our icon invoke LoadImage, hInstance, HS_ICON, IMAGE_ICON, 0, 0, NULL mov hResizerIcon, eax ; setup the tray icon mov notifyData.cbSize, sizeof NOTIFYICONDATA push hWin pop notifyData.hwnd mov notifyData.uID, 0 mov notifyData.uFlags, NIF_ICON or NIF_MESSAGE or NIF_TIP mov notifyData.uCallbackMessage, WM_SHELLNOTIFY push hResizerIcon pop notifyData.hIcon invoke lstrcpy, addr notifyData.szTip, addr szDisplayName invoke Shell_NotifyIcon, NIM_ADD, addr notifyData ; start timer, value is in milliseconds invoke SetTimer, hWin, ID_TIMER, timeout, NULL
This result will be something like this:
In the next snippet we are dealing with the user right clicking on the “Exit” option.
.elseif uMsg == WM_SHELLNOTIFY .if wParam == 0 .if lParam == WM_RBUTTONDOWN or WM_RBUTTONUP invoke GetCursorPos, ADDR pt invoke SetForegroundWindow, hWin invoke TrackPopupMenuEx, hPopupMenu, TPM_LEFTALIGN or TPM_LEFTBUTTON, pt.x, pt.y, hWin, 0 invoke PostMessage, hWin, WM_NULL, 0, 0 .endif .endif
The PostMessage function adds a message to the message queue and we need to react to that message handling WM_COMMAND.
In WM_COMMAND’s case, wParam will contain information related to the control and the event that was fired. The rightmost 16 bits will contain the control ID and the leftmost 16 bits will contain the event, so some bit manipulation will be needed as you can see in the source code comments.
.elseif uMsg == WM_COMMAND mov eax, wParam ; wParam's low-order 16 bits contains the control ID ; EAX is the full 32-bit value, AX is the low-order 16-bits .if ax == IDM_EXIT ; wParam's high word stores the event ; we shift the high order bits to the low order position shr eax, 16 .if ax == BN_CLICKED invoke SendMessage, hWin, WM_CLOSE, 0, 0 .endif .endif
If the user right clicked “Exit”, then a WM_CLOSE event will be created and we’ll need to free our resources once we receive the message.
.elseif uMsg == WM_CLOSE ; clean stuff invoke Shell_NotifyIcon, NIM_DELETE, addr notifyData invoke DestroyIcon, hResizerIcon invoke DestroyMenu, hPopupMenu invoke KillTimer, hWin, ID_TIMER invoke ReleaseMutex, hMutex invoke DestroyWindow, hWin .elseif uMsg == WM_DESTROY invoke PostQuitMessage, NULL
The interesting part
Let’s take a look at what happens when the timer we defined activates:
.elseif uMsg == WM_TIMER invoke FindWindow, NULL, addr szBattlenetTitle mov hBattlenet, eax invoke Maximize, eax invoke FindWindow, NULL, addr szHearthstoneTitle mov hHearthstone, eax invoke Maximize, eax ; 10 iterations every 2000 milliseconds = 20 seconds ; it should be enough, we can quit. ; We start counting only after the Hearthstone window has ; been detected (not when Battle.net is opened) .if hHearthstone != NULL mov eax, maxResizeAttempts .if cntResizeAttempts == eax invoke SendMessage, hWin, WM_CLOSE, 0, 0 .endif inc cntResizeAttempts .endif ; if we don't detect Batlle.net and Hearthstone after a while ; we also quit. This can happen if the user starts Battle.net, then ; never starts Hearthstone for some reason, then quits Battle.net ; and forgets to stop the utility manually .if hBattlenet == NULL && hHearthstone == NULL mov eax, maxAppsNotFound .if cntAppsNotFound == eax invoke SendMessage, hWin, WM_CLOSE, 0, 0 .endif inc cntAppsNotFound .endif
We use a Win32 API to find a window whose title matches a certain string, the API returns an identifier and then we call a custom function to maximize the window. After Hearthstone’s windows has been detected, we wait for 20 seconds just in case and then quit. Just to be safe, there’s also logic dealing with a case in which Battle.net and Hearthstone are no longer running.
Now let’s take a look at the Maximize function.
Maximize proc targetHwnd : HANDLE LOCAL dlgRect : RECT .if targetHwnd != NULL invoke ShowWindow, targetHwnd, SW_MAXIMIZE .endif ret Maximize endp
As you can see, it’s very simple and uses the ShowWindow API to send a SW_MAXIMIZE message to the program we want to maximize. From the program’s perspective it’s just as if the user had clicked the maximize button.
Hearthstone fights back
The previous code worked perfectly for Battle.net’s window. However, it wasn’t good enough for the Hearthstone one. It turns out than when Hearthstone is launched, our utility sends a maximize message and Hearthstone reacts accordingly. But after a few seconds, for whatever reason, it recalculates its size and returns to a non-maximized state.
As you can see in the screenshot, the window has the maximized style active, but it’s clearly not maximized.
So basically Hearthstone is resizing it’s window but it’s not aware that we have set the maximized state and we end up in an inconsistent state.
But aren’t we running every two seconds and sending a maximize event? That should take care of the problem, right? Well, no, because if the maximized style is active then the window ignores further maximize events.
Back to the drawing board
Besides what we are doing, we now need to evaluate if the upper left corner is actually located at the origin. You might think that if the window is maximized, then the top and left coordinates should be (0,0). Wrong!, because a maximized window still has a border but it’s not visible, so the corner is at (-9, -9) in this case.
With that knowledge, we are going to send a SW_RESTORE message to clear the maximized style and then send a SW_MAXIMIZE again if we detect that the coordinates are positive. By the way, GetWindowRect returns the coordinates that we need.
Signed and unsigned numbers
But before coding, there’s something important to know regarding negative numbers. In Assembly, the conditional jump instructions are different for signed and unsigned values. For example, the jl (jump if lower) instruction is used if you want to interpret myValue as signed.
cmp myValue, 0 jl myLabel
On the other hand, jb (jump if below) is used if you want to interpret myValue as unsigned.
cmp myValue, 0 jb myLabel
Improved Maximize function
This version uses cmp and jl directly.
Maximize proc targetHwnd : HWND LOCAL dlgRect : RECT .if targetHwnd != NULL invoke ShowWindow, targetHwnd, SW_MAXIMIZE invoke GetWindowRect, targetHwnd, addr dlgRect .if eax != 0 cmp dlgRect.top, 0 ; if it's negative we don't need to do anything ; jl because we want to interpret dlgRect.top as signed jl endMaximize ; invoke ShowWindow, targetHwnd, SW_RESTORE invoke ShowWindow, targetHwnd, SW_MAXIMIZE .endif .endif endMaximize: ret Maximize endp
It’s not too difficult to read, but maybe we can do better using the .IF directive. Keep in mind, however, that we need to make sure that .IF infers the values as signed. There are several ways to do it, but we are going to define our own RECT structure:
MYRECT STRUCT left SDWORD ? ; It's DWORD in MASM32 SDK top SDWORD ? ; It's DWORD in MASM32 SDK right SDWORD ? ; It's DWORD in MASM32 SDK bottom SDWORD ? ; It's DWORD in MASM32 SDK MYRECT ENDS
The SDWORD type will help the assembler to use the correct jump instruction for our case.
Maximize proc targetHwnd : HWND LOCAL dlgRect : MYRECT .if targetHwnd != NULL invoke ShowWindow, targetHwnd, SW_MAXIMIZE invoke GetWindowRect, targetHwnd, addr dlgRect .if eax != 0 .if dlgRect.top >= 0 invoke ShowWindow, targetHwnd, SW_RESTORE invoke ShowWindow, targetHwnd, SW_MAXIMIZE .endif .endif .endif ret Maximize endp
If we take a look at Visual Studio’s disassembly view, we can confirm that jl is actually being used.
.if dlgRect.top >= 0 00D72667 cmp dword ptr [ebp-0Ch],0 00D7266B jl @C002C (0D72681h)
Defining our own structure also has the benefit that Visual Studio’s debugger will interpret the value as signed as well.
We can now create a batch file to call both Battle.net and our utility. You might want to include Hearthstone Deck Tracker if you use it.
@Echo OFF start "" "C:\Program Files (x86)\Battle.net\Battle.net Launcher.exe" start "" "C:\Users\frozen\AppData\Local\HearthstoneDeckTracker\HearthstoneDeckTracker.exe" start "" "C:\Program Files (x86)\Hearthstone Resizer\hs_resizer.exe" Exit
You can’t pin a batch file to the taskbar, so you must create a link first. However, the link won’t be pinnable unless you add “cmd.exe /c” to the Target field.
Now that the utility and the launcher have been completed, my OCD regarding maximized windows can rest. 😉
This post turned out longer than I expected, so if you’ve made this far, thanks for reading!!!