Building an Actions Per Minute counter - part 1, Keylogger
06 February 2022
This is part 1 of a 3 about creating an Actions Per Minute (APM) tracker for playing video games on windows.
The idea is to have a program that displays a small box with our current APM number in the corner of the screen.
A few games I had in mind to track my APM with are League of Legends, Dota 2, and Age of Empires. I don’t play competitively, but I thought it would be cool to build a little widget that shows me my APM. For this we need 3 components:
- Count actions (keystrokes or mouse clicks)
- Calculate a per minute average
- Display the number on the screen
The first part we will start with is registering keyboard and mouse actions, which will be the input to our APM counter. There are two architectural styles I looked at for counting keystrokes. Both styles use the Win32 API.
- Looping over all keys
- Hooks
1) Looping over all keys
This method is easier to implement. The win32 function GetAsyncKeyState(key)
takes a keycode and checks if that key is currently pressed. Once we know that key is pressed, we can increment a counter. The keycodes go up to 254, so what we can do is constantly loop through and check all keycodes and check if they are pressed.
#include <windows.h>
int counter;
int main() {
while (true) {
for (int key = 0; key <= 254; key++) {
if (GetAsyncKeyState(key) == -32767) {
counter++;
}
}
}
return 0;
}
The magic number -32767
has some meaning. The return value from GetAsyncKeyState(key)
is a signed 16-bit int, where the most significant bit indicates the key is currently being held down and the least significant bit indicates the key has transitioned from released to pressed since the last call to GetAsyncKeyState
.
The downside to this approach is we are not actually tracking when a key is pressed. We check if a key is currently pressed and count that as a keystroke. In addition, the docs on GetAsyncKeyState mention the pressed to released state flag may not be reliable.
2) Keyboard and mouse event hooks
SetWindowsHookEx
is a win32 method to attach a hook method to either keyboard or mouse events. With this, we can get a counter increment per action instead of the previous method.
The hook functions only have 3 parts to them. The most complicated part is knowing which WM_
constants to track.
LRESULT mouseProc(int nCode, WPARAM wparam, LPARAM lparam)
{
// 1) function requirement to exit early
if (nCode < 0)
return CallNextHookEx(kHook, nCode, wparam, lparam);
// 2) where we detect actions
if (wparam == WM_LBUTTONDOWN ||
wparam == WM_RBUTTONDOWN ||
wparam == WM_XBUTTONDOWN ||
wparam == WM_MBUTTONDOWN)
counter++;
// 3) function return values
return CallNextHookEx(kHook, nCode, wparam, lparam);
}
- The win32 spec requires us to immediately call
CallNextHookEx
with the input parameters and return its value without further processing ifnCode < 0
- Our tracking code. The
wparam
variable holds the type of event this was, we want to filter for “button down” events and increment our counter whenever one happens. This excludes events like “mouse move” which we do not want to track - Our required return value, the same as #1
The code for the keyboard hook is very similar
LRESULT keyboardProc(int nCode, WPARAM wparam, LPARAM lparam)
{
// 1) function reqirements
if (nCode < 0)
CallNextHookEx(mHook, nCode, wparam, lparam);
// 2) where we detect actions
if (wparam == WM_KEYDOWN || wparam == WM_SYSKEYDOWN)
counter++;
// 3) function reqirements
return CallNextHookEx(mHook, nCode, wparam, lparam);
}
The only difference here is which wparam
values to check. We could combine these two functions into a single function that checks both the keyboard and mouse events if we wanted.
Registering the hooks requires calling SetWindowsHookEx
.
kHook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)keyboardProc, GetModuleHandle(NULL), 0);
mHook = SetWindowsHookEx(WH_MOUSE_LL, (HOOKPROC)mouseProc, GetModuleHandle(NULL), 0);
SetWindowsHookEx
needs 4 parameters:
- The type of hook we register, either keyboard or mouse hooks in our case
- The hook function
- A handle to the current calling function
GetModuleHandle(NULL)
does the trick - Which thread to associate the hook with,
0
means all threads since we want to track events across all applications
This function needs to be called twice, once for the keyboard hooks WH_KEYBOARD_LL
and one for the mouse hooks WH_MOUSE_LL
. You will notice the return values kHook
and mHook
were used inside the hooks functions themselves, the hooks do need to be called there.
Take a look at the GitHub repo if you want to see the full code in action