Let’s code a small utility in assembly to resize Hearthstone
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.
Â
Tools needed
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.
Â
Program overview
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.
Includes
include \masm32\include\masm32rt.inc
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
Prototypes
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
Data
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.
Single instance
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.
Initialization boilerplate
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.
WndProc
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.
Â
A launcher
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.
That’s it
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!!!