|
| 1 | +# Age of Empires II scrollbug fix |
| 2 | + |
| 3 | +## What is the scrollbug? |
| 4 | + |
| 5 | +Sometimes when you tab out of the game, after tabbing back in the game will be stuck in a state where it is constantly scrolling in some direction. |
| 6 | + |
| 7 | +This makes the game unplayable and requires a restart of the game. This is especially annoying in multiplayer games. |
| 8 | + |
| 9 | +The scrollbug happens mostly when running the game in Wine, but has supposedly also been observed on Windows. |
| 10 | + |
| 11 | +## How do I reliably reproduce the scrollbug? |
| 12 | + |
| 13 | +- Use Wine. I don't know if these steps work on Windows. |
| 14 | +- Start a game, e.g. a standard single-player random map game |
| 15 | +- Hold the left-arrow key for 3 seconds |
| 16 | +- Release the left-arrow key |
| 17 | +- Wait for one second |
| 18 | +- Tab out of the game |
| 19 | +- Wait for one second |
| 20 | +- Tab back into the game |
| 21 | +- Congratulations; you now have a scroll bug |
| 22 | + |
| 23 | +## Why is there a scrollbug? |
| 24 | + |
| 25 | +Age of Empires II uses the `GetKeyboardState()` and `GetKeyState()` Win32 API functions to determine whether one of the scrolling keys is pressed. |
| 26 | + |
| 27 | +In the result of these functions, the key stats are reported as a single byte. |
| 28 | + |
| 29 | +According to the MSDN documentation, |
| 30 | + |
| 31 | +- Bit 7 of the byte says whether the key is currently pressed |
| 32 | +- Bit 0 of the byte toggles each time the key is pressed |
| 33 | +- All other bits are not documented |
| 34 | + |
| 35 | +Age of Empires II wants to check whether the key is currently pressed. To do this it checks whether the byte value is `>= 1` instead of checking if Bit 7 is set (`& 0x80`). |
| 36 | + |
| 37 | +This works as long as bits 1..6 are zero. |
| 38 | + |
| 39 | +Unfortunately, wine seems to use bit 2 to provide some other metadata, obviously related to tabbing out of the window, so it doesn't guarantee that bits 1..6 are always 0. |
| 40 | + |
| 41 | +Windows also seems to sometimes use some of the other bits. |
| 42 | + |
| 43 | +## Which versions of Age of Empires II have the scrollbug? |
| 44 | + |
| 45 | +Every single one, i.e.: |
| 46 | + |
| 47 | +- AoK |
| 48 | +- AoC |
| 49 | +- Forgotten Empires |
| 50 | +- UserPatch |
| 51 | +- AoE2:HD |
| 52 | +- AoE2:DE |
| 53 | + |
| 54 | +## How does this fix work? |
| 55 | + |
| 56 | +This uses the Microsoft Research [Detours](https://github.com/microsoft/Detours) library to inject replacements of `GetKeyState()` and `GetKeyboardState()` into the running Age of Empires II binary. |
| 57 | + |
| 58 | +The replacement functions mask out bits 1..6 in the results. |
| 59 | + |
| 60 | +The fix consists of two binaries: |
| 61 | + |
| 62 | +- `age2_x1_fixed.exe` reads the configuration from `age2_x1_fixed.ini` and calls the `exe` that is specified in the `ini` file with the injector `dll` file that is specified in the `ini` file. |
| 63 | +- `fix_scrollbug32.dll` injects fixed versions of `GetKeyState()` and `GetKeyboardState()` into any 32-bit Windows executable it is used with. |
| 64 | + |
| 65 | +## How do I build this fix? |
| 66 | + |
| 67 | +- Use Windows (please, somebody port this to winegcc or mingw or _something_ that works on Linux) |
| 68 | +- Install Visual C++ Build Tools 2015 |
| 69 | +- Get this source code (duh.) |
| 70 | +- Get `detours.lib` and `detours.h`, e.g. by building the Detours library from source by invoking `nmake`, or by downloading the binaries from this Github repo. Place these two files in a subfolder `detours/` of this source code |
| 71 | +- Call `build.bat` |
| 72 | +- You will now have a whole bunch of garbage files, plus |
| 73 | + - `fix_scrollbug32.dll` |
| 74 | + - `dll_inject.exe` |
| 75 | + - `dll_inject.ini` |
| 76 | + |
| 77 | +## How do I use this fix? |
| 78 | + |
| 79 | +`dll_inject.exe` can be renamed to whatever name. It is recommended to rename it to `{name_of_the_exe_you_want_to_fix}_fixed.exe`. |
| 80 | + |
| 81 | +When launched, `dll_inject.exe` will |
| 82 | + |
| 83 | +- open the `.ini` file with the same name |
| 84 | +- in that `.ini` file in the section `[dll_inject]`, read the keys `exe_file` and `dll_file` |
| 85 | +- launch the specified `exe` file and inject the specified `dll` file |
| 86 | +- wait until the exe file has finished |
| 87 | + |
| 88 | +If you use the pre-compiled and pre-configured `age2_x1_fixed.exe`, `age2_x1_fixed.ini`, `fix_scrollbug32.dll` you can just place them in the `age2_x1` folder and launch `age2_x1_fixed.exe` instead of `age2_x1.exe`. |
0 commit comments