Making a simple cursor hold in the Warcraft 3 window

Published on September 23, 2013

Making a simple cursor hold in the Warcraft 3 window

Greetings to you, reader. I have a hobby - this is the good old Warcraft 3. The Habré already had a series of articles devoted to this wonderful game. I want to share with the community one utility that came in handy when streaming me. All interested please go to cat.

Foreword

It all started with the fact that one of the weekends against the background of ongoing repairs, I decided to watch a stream of Warcraft III. There are enough sites at the moment, but my preferences relate to www.goodgame.ru (not advertising). I was disappointed that nothing interesting was broadcast at that time. And then the thought arose - why not make your own stream with blackjack, etc.

Related software

For broadcasting, among other things, you need an application to capture content. At the moment, two of them can be distinguished: xsplit and openbroadcaster . Honestly, I did not use the first one. In the free version, basic functionality is available. But to download the basic version you will have to undergo mandatory registration (not that it would be a problem, but ...). The GPL license and, accordingly, the availability of the source code incline to the second option. I stopped at openbroadcaster.

Difficulties

There were no problems installing and configuring OBS. But the running game did not want to be captured in the recommended Game capture mode (this is probably due to the use of the old version of directx when developing the game). Having played with other capture modes, we managed to find two that provided the necessary behavior - Monitor capture and Window capture.
The first is quite a strong affectite performance. Feels during the game. But it was a working version of what is called "out of the box."
The second option led to discomfort during the game - the cursor constantly went beyond the window. In general, it was absolutely unplayable.

Decision

The second option was chosen and it was decided to write a utility to eliminate the discomfort described above.
Initially, Warcraft III runs in full screen mode.
To run in windowed mode, you must use the "-window" switch in the application launch command, this will just allow you to capture in Windows capture mode.

To hold the cursor within the client area of ​​the window, the first version of the utility was written. The main cycle of her work is given below:

/* polling version */
void Controller::RunPollingLoop()
{		
	while (true)
	{
		HWND activeWindow		= GetForegroundWindow();
		HWND requiredWindow		= FindRequiredWindow(m_className, m_winTitle, 5);
		if (requiredWindow == NULL)
			throw std::runtime_error("Required window not found");
		m_fullScreen.Init(requiredWindow);
		m_clipHelper.Init(requiredWindow);
		if (activeWindow == requiredWindow)
		{
			if (m_clipHelper.IsClipped() || !CursorInClientArea(requiredWindow))
			{
				Sleep(g_SleepTimeOut);
				continue;
			}
			if (m_fullScreen.Enter()) 
			{	
				DEBUG_TRACE("EnterFullscreen success"); 
				m_clipHelper.Clip();
				DEBUG_TRACE("Clip");
			}
			else
			{	DEBUG_TRACE("EnterFullscreen failed"); }
		}
		else
		{
			if (m_clipHelper.IsClipped())
			{
				if (m_fullScreen.Leave())
				{ DEBUG_TRACE("LeaveFullscreen success"); }
				else
				{ DEBUG_TRACE("LeaveFullscreen failed"); }
				m_clipHelper.UnClip();
				DEBUG_TRACE("UnClip");
			}
			Sleep(g_SleepTimeOut);
		}
	}
}


It uses the ClipHelper helper class to control the cursor retention process and the FullScreen class to control the transition to full-screen mode and restore from it. The loop itself implements an active window polling algorithm with a timeout of 500 ms. I did not like this moment right away, but to move on I needed to check the whole concept, and then do some optimization.

In the process of using the utility, the following Wishlist immediately appeared:
- Clip should be performed only in case of a click (hold for the polling version) in the client area in order to be able to drag the window;
- annoyed the appearance of the taskbar during the game (relevant if it is fixed). The first thought was to hide it programmatically. But in this case, it would be necessary to track the moments of the user's exit from the game and show the taskbar back. The risk of leaving the user without a taskbar increased. Therefore, I decided to make a fullscreen implementation by resizing the game window to the monitor resolution size, to which this window is assigned:

bool FullScreen::Enter()
{
	if (m_fullScreen)
		return true;
	assert(m_hwnd);
	if (m_hwnd == NULL)
		return false;
	HMONITOR hmon = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST);
	MONITORINFO mi = { sizeof(mi) };
	if (!GetMonitorInfo(hmon, &mi)) 
		return false;
	if (!GetWindowRect(m_hwnd, &m_origWindowRect))
	{
		SecureZeroMemory(&m_origWindowRect, sizeof(m_origWindowRect));
		return false;
	}
	if (!SetWindowPos(m_hwnd, HWND_TOPMOST, 
					   mi.rcMonitor.left,
					   mi.rcMonitor.top,
					   mi.rcMonitor.right - mi.rcMonitor.left,
					   mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_SHOWWINDOW))
		return false;
	m_fullScreen = true;
	return true;
}


Optimization

In the second version of the utility, the polling of the active window was replaced with the WM_ACTIVATE and WM_LBUTTONDOWN message hook. For this, I used two types of hooks: WH_CALLWNDPROC and WH_MOUSE. The bottom line is that we track the required events of the game window and notify our utility through the server window. The hook was hung only for the game process. Thus, the game must be running before the utility:

BOOL SetWinHook(HWND hWnd, DWORD threadId)
{
	if (g_hWndSrv != NULL)
		return FALSE; //already hooked
	g_hCallWndHook = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)CallWndHookProc, g_hInst, threadId);
	if (g_hCallWndHook != NULL)
	{ 
		g_hMouseHook = SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseHookProc, g_hInst, threadId);
		if (g_hMouseHook != NULL)
		{
			g_hWndSrv = hWnd;
			return TRUE;
		}
		ClearWinHook();
	}
	return FALSE;
}

And the main cycle of work was reduced to the following procedure:

LRESULT CALLBACK Controller::MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	if (uMsg == WM_ACTIVATE) 
    { 
		switch (wParam)
		{
		case WA_ACTIVE:
			DEBUG_TRACE("WA_ACTIVE");
			gs_ActivateClip = true;
			break;
		case WA_CLICKACTIVE:
			DEBUG_TRACE("WA_CLICKACTIVE");
			gs_ActivateClip = true;
			break;
		case WA_INACTIVE:
			DEBUG_TRACE("WA_INACTIVE");
			gs_ActivateClip = false;
			if (g_ControllerPtr->ClipCursorHelper().IsClipped())
			{
				if (g_ControllerPtr->FullScreenHelper().Leave())
				{ DEBUG_TRACE("LeaveFullscreen success"); }
				else
				{ DEBUG_TRACE("LeaveFullscreen failed"); }
				g_ControllerPtr->ClipCursorHelper().UnClip();
				DEBUG_TRACE("UnClip");
			}
			break;
		}
		return 0;
	}
	else if (uMsg == WM_LBUTTONDOWN)
	{
		DEBUG_TRACE("WM_LBUTTONDOWN");
		if (!gs_ActivateClip)
			return 0;
		if (g_ControllerPtr->ClipCursorHelper().IsClipped())
			return 0;
		if (g_ControllerPtr->FullScreenHelper().Enter()) 
		{	
			DEBUG_TRACE("EnterFullscreen success"); 
			g_ControllerPtr->ClipCursorHelper().Clip();
			DEBUG_TRACE("Clip");
		}
		else
		{	DEBUG_TRACE("EnterFullscreen failed"); }
		return 0;
	}
    return DefWindowProc(hwnd, uMsg, wParam, lParam); 
}

The helper classes used are the same as in the first version. This function is the window procedure of the utility server window. To capture the cursor and go to full screen, you need to activate the window and left-click on the client area. When the window ceases to be active, it is restored to its original size and position, and the cursor is no longer held in it.

Afterword

A utility was developed to make the process of streaming your favorite game more comfortable than the proposed working option “out of the box”. I would be glad if someone draws something interesting for himself. All source code is uploaded to github WinClipCursor .