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.

Battle.Net

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.

Colors

I’ve also disabled the following options because the extension was getting confused when dealing with API calls.

Parameters

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.

Parameters

 

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.

Registers

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:

Systray

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.

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.

Maximized

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.

Link

 

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.

Link

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!!!