Add resolution handling to Logical Stones

3 minute read

Problem

The heap corruption is gone, but the game is still unusable because the UI was not designed using responsive technologies and it appears broken using today’s common aspect ratios. Logical Stones lacks resolution handling - it goes full screen and uses the current resolution of your display, which just looks weird at the widespread 1920x1080 resolution:

Broken main menu of Logical Stones Broken level selection screen of Logical Stones Broken Logical Stones level

Investigation

First of all, I looked for the location in the code where the main game window is created:

{
[...]
  WndClass.style = 32;
  WndClass.lpfnWndProc = sub_41599C;
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hInstance = hInstance;
  WndClass.hIcon = LoadIconA(0, (LPCSTR)0x7F00);
  WndClass.hCursor = LoadCursorA(0, (LPCSTR)0x7F00);
  WndClass.hbrBackground = (HBRUSH)GetStockObject(4);
  WndClass.lpszMenuName = 0;
  WndClass.lpszClassName = "LGStones";
  RegisterClassA(&WndClass);
  dword_44797C = GetSystemMetrics(0);
  dword_447980 = GetSystemMetrics(1);
  v4 = CreateWindowExA(
         0,
         "LgStones",
         "Logical Stones Game",
         0x90880000,
         0,
         0,
         dword_44797C,
         dword_447980,
         0,
         0,
         hInstance,
         0);
[...]
}

As you can see dword_44797C and dword_447980 are the global variables which contain the width and height of your current display obtained by calling GetSystemMetrics with parameters SM_CXSCREEN and SM_CYSCREEN respectively.

In order to fix the resolution of the game, we need to take care of the above-mentioned variables and constants.

Solution

Instead of setting the values of dword_44797C and dword_447980 based on the values reported by GetSystemMetrics a custom value can be used by patching the game binary. I used OllyDbg to apply the changes.

Binary patching

[...]
PUSH 0
CALL <JMP.&USER32.GetSystemMetrics>
MOV DWORD PTR DS:[44797C],EAX
PUSH 1
CALL <JMP.&USER32.GetSystemMetrics>
MOV DWORD PTR DS:[447980],EAX
ADD ESP,-4
PUSH 0
PUSH EBX
PUSH 0
PUSH 0
PUSH EAX
PUSH DWORD PTR DS:[44797C]
PUSH 0
PUSH 0
PUSH 90880000
PUSH LgStones.00415843                   ; ASCII "Logical Stones Game"
PUSH LgStones.00415857                   ; ASCII "LgStones"
PUSH 0
CALL <JMP.&USER32.CreateWindowExA>
[...]

The CreateWindowExA function expects the width of the window to be located at 0x0044797C and the height of the window to be in the EAX register.

The task is to initialize dword_44797C, dword_447980 and EAX with hard coded constant values which can be easily modified later. The default resolution is going to be 1024x768 which used to be a very common resolution with 4:3 aspect ratio back then.

The hexadecimal value of 1024 is moved to EAX then copied to dword_44797C. The same applies to the height - the hexadecimal value of 768 is moved to EAX then copied to dword_447980. Now the global variables are initialized and EAX contains the height of the window as it is expected by CreateWindowExA.

MOV EAX,400
MOV DWORD PTR DS:[44797C],EAX
MOV EAX,300
MOV DWORD PTR DS:[447980],EAX
NOP
NOP
NOP
NOP
ADD ESP,-4
PUSH 0
PUSH EBX
PUSH 0
PUSH 0
PUSH EAX
PUSH DWORD PTR DS:[44797C]
PUSH 0
PUSH 0
PUSH 90880000
PUSH LgStones.00415843                   ; ASCII "Logical Stones Game"
PUSH LgStones.00415857                   ; ASCII "LgStones"
PUSH 0
CALL <JMP.&USER32.CreateWindowExA>

Configurable resolution

I have created a small utility which can directly overwrite our hard-coded 1024x768 resolution. When you start the tool you will be asked about the desired width of the game window. The height is automatically calculated to match the 4:3 aspect ratio required by the UI.

Resolution changer tool in action
#include <iostream>
#include <fstream>
#include <string>

static const size_t WINDOW_WIDTH_OFFSET = 0x00014CC7;
static const size_t WINDOW_HEIGHT_OFFSET = 0x00014CD1;

template <typename T>
void patch(std::ofstream& executable, size_t offset, const T& value)
{
    executable.seekp(offset, std::ofstream::beg);
    executable.write(reinterpret_cast<const char*>(&value), sizeof(T));
    executable.flush();
}

int main() {
    std::ofstream executable("LgStones.exe", std::ofstream::in | std::ofstream::out | std::ofstream::binary);

    if (executable) {
        std::cout << "Width of the game window: ";

        std::string width_str;
        std::getline(std::cin, width_str);

        uint32_t width = std::stoi(width_str);
        uint32_t height = uint32_t(width / 4.0f * 3.0f);

        std::cout << "Height of the game window: " << height << std::endl;

        patch<uint32_t>(executable, WINDOW_WIDTH_OFFSET, width);
        patch<uint32_t>(executable, WINDOW_HEIGHT_OFFSET, height);

        std::cout << "Done!" << std::endl;
    }
    else {
        std::cerr << "Couldn't find LgStones.exe!" << std::endl;
    }

    system("pause");
    return 0;
}

Downloads

You may download and play the game, including all the fixes for free.

The source code for LgStonesAllocator and LgStonesResolutionChanger are also available.

Are you stuck on a planet? Let me know in the comments!

Leave a comment