Fixing my BF1942 woes with Win32 APIs
Tags: csharp, dotnet, win32
Published: 2022-01-21 03:40:44

Battlefield 1942 was one of a number of great games I played when I was growing up. I was first introduced to the game through a demo disc on a magazine - it was the expansion "Secret Weapons of WWII". I spent many hours playing that demo and eventually managed to snag the complete set of the game with both expansions. It was game that ran pretty well on old hardware and have a lot of fun memories playing it.

Jumping forward _several_ years, I wanted to give the old game another play. I knew the graphics wouldn't hold up but to play in its nostalgic sandbox would more than overcome that. I installed the game and the various patches, modified configuration files to use my monitor's native resolution (1080p) and tried to launch it. Unfortunately, it wouldn't launch at all.

Some people online suggest it was issues with SecureROM as [that no longer works with Windows 10](https://www.rockpapershotgun.com/windows-10-safedisc-securom-drm) while others suggest it is an issue with Direct Play. What ultimately solved it for me was an unofficial patch by a group called [Team Simple](https://team-simple.org/).

Now with the game launching, I start to hear [the classic BF1942 main menu music](https://www.youtube.com/watch?v=kaopMpvMZbg). Sped through the profile setup, jumped to instant battle, picked my favourite level (Hellendoorn) and pressed start. The music changes to, in my opinion, [an even more iconic piece when the level is loading](https://www.youtube.com/watch?v=IPMnEmkoPFs). The progress bar moves maybe a quarter of the way and then... I'm back on my desktop.

After going back and forth with settings and the resolution changes I made, I found it just didn't want to work in full screen.

![Battlefield 1942 in Window mode](https://turnerj.com/blog/images/fixing-bf1942-initial-window-mode.jpg)

> For the sharp-eyed individuals, you might notice that this is actually playing through [Parsec](https://parsec.app/) - this is part of [my vision for a remote desktop experience](https://turnerj.com/blog/remote-desktop-experience-part-1-planning). It actually plays great (no noticeable input lag) via Parsec, over Wi-Fi, from my desktop to my laptop. That said, I did try the game directly on that machine and it still crashed so something else was to blame.

While I could play in window mode, I couldn't actually move the window so I could see the whole screen. Anytime I attempted to drag the window, it would just bring the cursor back in the game. Tried shortcuts to maximise the window but none of those worked either.

So what would any good programmer do? ~~Search for an existing solution online.~~ Write their own program to fix it!

Seemed like the fun thing to do anyway.

## The Plan

This is what I wanted to do:
- Get rid of the game's window border as it was just taking space
- Position the window so it is centered to the monitor

The second point is important - the menu displays at 800x600 but the game when loading and playing is at whatever resolution I configured in window mode. My plan was to build a launcher that would bootstrap the main game.

I've messed around with removing borders from applications years back when I wanted to run a console application in the background. The way I achieved it back then was to invoke Win32 APIs from .NET and figured that would be a good starting place. My initial task was to find the APIs I need to use.

Fortunately [someone already found the APIs to use to remove borders and reposition the window](https://github.com/dtgDTGdtg/SRWE/blob/b439859e15ca44b6c4715fdb015c321a49ef634a/SRWE/Window.cs). My job then was to work out how to bring that into my application.

```csharp
//A snippet of the code that helped me with the Win32 APIs from the Simple Runtime Window Editor (SRWE)
//Source: https://github.com/dtgDTGdtg/SRWE/blob/b439859e15ca44b6c4715fdb015c321a49ef634a/SRWE/Window.cs
public void RemoveBorders()
{
	uint nStyle = (uint)WinAPI.GetWindowLong(m_hWnd, WinAPI.GWL_STYLE);
	nStyle = (nStyle | (WinAPI.WS_THICKFRAME + WinAPI.WS_DLGFRAME + WinAPI.WS_BORDER)) ^ (WinAPI.WS_THICKFRAME + WinAPI.WS_DLGFRAME + WinAPI.WS_BORDER);
	WinAPI.SetWindowLong(m_hWnd, WinAPI.GWL_STYLE, nStyle);

	nStyle = (uint)WinAPI.GetWindowLong(m_hWnd, WinAPI.GWL_EXSTYLE);
	nStyle = (nStyle | (WinAPI.WS_EX_DLGMODALFRAME + WinAPI.WS_EX_WINDOWEDGE + WinAPI.WS_EX_CLIENTEDGE + WinAPI.WS_EX_STATICEDGE)) ^ (WinAPI.WS_EX_DLGMODALFRAME + WinAPI.WS_EX_WINDOWEDGE + WinAPI.WS_EX_CLIENTEDGE + WinAPI.WS_EX_STATICEDGE);
	WinAPI.SetWindowLong(m_hWnd, WinAPI.GWL_EXSTYLE, nStyle);

	uint uFlags = WinAPI.SWP_NOSIZE | WinAPI.SWP_NOMOVE | WinAPI.SWP_NOZORDER | WinAPI.SWP_NOACTIVATE | WinAPI.SWP_NOOWNERZORDER | WinAPI.SWP_NOSENDCHANGING | WinAPI.SWP_FRAMECHANGED;
	WinAPI.SetWindowPos(m_hWnd, 0, 0, 0, 0, 0, uFlags);
}
```

I put together bits and pieces from that codebase like the remove border and window positioning code and combined it with additional API calls for monitor information. I needed the following Win32 APIs to do everything I wanted:

- [GetWindowInfo](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowinfo) (for the current window bounds)
- [MonitorFromWindow](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-MonitorFromWindow) (getting the monitor handle the window is on)
- [GetMonitorInfoW](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-GetMonitorInfoW) (getting the monitor bounds)
- [SetWindowPos](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-SetWindowPos) (setting the window position)
- [SendMessageW](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-SendMessageW) (to send `WM_EXITSIZEMOVE` to the window, a tip from [SRWE](https://github.com/dtgDTGdtg/SRWE#exitsizemove) - never checked if it was strictly necessary for BF1942 though)
- [GetWindowLongW](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongw) (getting window settings)
- [SetWindowLongW](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongw) (updating window settings)

After doing a rough integration with pieces from that codebase, I gave it a run and... my application crashed. I was using `process.MainWindowHandle` to get BF1942's game window. Turns out that it isn't set till, well, there is a main window available. So I wrote some code to wait for that and bingo - it launched the game and worked!

Well, it mostly worked - see BF1942 has an interesting quirk where it launches a new process when you end a match and go back to the main menu. This required me to write logic to track when processes changed while also still allowing it to exit when BF1942 is closed properly.

## Improving the Win32 APIs

While my prototype worked, cobbled together from bits of SRWE and my own bits, I wasn't entirely happy with how I integrated the Win32 APIs. Below is the snippet of code I had that takes a window handle, gets the window's size, the monitor's size, calculates the position and finally sets it.

```csharp
unsafe static void UpdateWindowPosition(int handle)
{
	var info = new WINDOWINFO();
	var success = WinAPI.GetWindowInfo(handle, ref info);
	if (success)
	{
		var windowDimensions = info.rcWindow;
		var monitorHandle = WinAPI.MonitorFromWindow(handle, 0);
		var monitorInfo = new LPMONITORINFO
		{
			cbSize = (uint)sizeof(LPMONITORINFO)
		};
		WinAPI.GetMonitorInfoA(monitorHandle, ref monitorInfo);
		var monitorDimensions = monitorInfo.rcMonitor;
		var x = monitorDimensions.Width / 2 - windowDimensions.Width / 2;
		var y = monitorDimensions.Height / 2 - windowDimensions.Height / 2;
		SetPosition(handle, x, y);
	}
}

static void SetPosition(int handle, int x, int y)
{
	uint uFlags = WinAPI.SWP_NOSIZE | WinAPI.SWP_NOZORDER | WinAPI.SWP_NOACTIVATE | WinAPI.SWP_NOOWNERZORDER | WinAPI.SWP_NOSENDCHANGING | WinAPI.SWP_FRAMECHANGED;
	WinAPI.SetWindowPos(handle, WinAPI.HWND_TOPMOST, x, y, 0, 0, uFlags);
	WinAPI.SendMessage(handle, WinAPI.WM_EXITSIZEMOVE, 0, 0);
}
```

What I would prefer is to actually have it feel more like a typical .NET API, something more like:

```csharp
static void UpdateWindowPosition(Window window)
{
	var monitorBounds = window.GetCurrentMonitor().GetBounds();
	var windowBounds = window.GetBounds();
	var x = monitorBounds.Width / 2 - windowBounds.Width / 2;
	var y = monitorBounds.Height / 2 - windowBounds.Height / 2;
	window.SetPosition(x, y);
}
```

All I'm doing is abstracting away the Win32 APIs but it makes my "business logic" here far cleaner. While doing this, I also decided to remove the pieces of SRWE and replace it with a more maintainable interface to the APIs.

I tried out both [TerraFX.Interop.Windows](https://github.com/terrafx/terrafx.interop.windows) and [CsWin32](https://github.com/microsoft/CsWin32), ultimately settling on the latter. CsWin32 was a little less intimidating as the API is generated based on strings in a text file rather than containing everything at once. Also I like jumping to definition of types to read more and explore APIs etc and doing that to one of the types in the TerraFX library crashed Visual Studio. That's more of a VS problem than a TerraFX library but still - CsWin32 would work great for what I'm doing.

The way I went about achieving my desired interface to the Win32 APIs I needed was via creating record-struct wrappers around the various native handles and having instance methods wrap the API calls themselves. For example, below is my `Window` type that I have most of my functionality hanging off of.

```csharp
public readonly record struct Window(nint Handle)
{
	private HWND Win32Handle => new(Handle);

	public Monitor GetCurrentMonitor()
	{
		nint handle = PInvoke.MonitorFromWindow(Win32Handle, 0);
		return new(handle);
	}

	public void SetPosition(int x, int y)
	{
		var flags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE |
			SET_WINDOW_POS_FLAGS.SWP_NOOWNERZORDER | SET_WINDOW_POS_FLAGS.SWP_NOSENDCHANGING | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED;
		PInvoke.SetWindowPos(Win32Handle, PInvoke.HWND_TOPMOST, x, y, 0, 0, flags);
		PInvoke.SendMessage(Win32Handle, PInvoke.WM_EXITSIZEMOVE, default, default);
	}

	public Rectangle GetBounds()
	{
		var windowInfo = new WINDOWINFO();
		PInvoke.GetWindowInfo(Win32Handle, ref windowInfo);
		return Rectangle.From(windowInfo.rcWindow);
	}

	public void RemoveBorders()
	{
		var style = PInvoke.GetWindowLong(Win32Handle, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
		style &= ~(int)(WINDOW_STYLE.WS_THICKFRAME | WINDOW_STYLE.WS_DLGFRAME | WINDOW_STYLE.WS_BORDER);
		_ = PInvoke.SetWindowLong(Win32Handle, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);

		style = PInvoke.GetWindowLong(Win32Handle, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
		style &= ~(int)(WINDOW_EX_STYLE.WS_EX_DLGMODALFRAME | WINDOW_EX_STYLE.WS_EX_WINDOWEDGE | WINDOW_EX_STYLE.WS_EX_CLIENTEDGE | WINDOW_EX_STYLE.WS_EX_STATICEDGE);
		_ = PInvoke.SetWindowLong(Win32Handle, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, style);
		PInvoke.SendMessage(Win32Handle, PInvoke.WM_EXITSIZEMOVE, default, default);
	}
}
```

## The End Result

![BF1942 in a borderless window for the intro cinematics](https://turnerj.com/blog/images/fixing-bf1942-menu-window.jpg)
![BF1942 in a borderless window at fullscreen](https://turnerj.com/blog/images/fixing-bf1942-borderless-fullscreen.jpg)

I called my project _Borderless 1942_ and [is available on GitHub](https://github.com/Turnerj/Borderless1942). It is a self-contained, single-file .NET 6 application. Because it is self-contained, you don't need .NET 6 installed to run it.

I'm quite happy that I got this working and could enjoy the game again. In terms of the code, the main thing I'd want to change is to move from a constant loop resetting the window position to something that listens on window resize events. This is possible via the Win32 APIs but has its own complications which I haven't got around to addressing yet.

I'm also looking at turning the style of wrapper I wrote into a dedicated library. There seems to be [some interest](https://www.reddit.com/r/dotnet/comments/r1pz3x/would_you_wantuse_an_improved_interface_to_native/) in improved access to the Win32 APIs. I don't know how far I'd go with it (what Win32 APIs I'd support) but I think it could make this a lot easier for developers in certain situations.