summaryrefslogtreecommitdiffhomepage
path: root/src/rinput.c
blob: 6b6d4d72e582e295c54b74cec087e44f1829a687 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
 * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
 * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

// NOTE: compiled on Windows only. All Linux Source releases are new enough to
// have raw input already.
// TODO(linux): actually, we DO want the scaling on Linux, so we need offsets
// for GetRawMouseAccumulators, etc.

#include <Windows.h>

#include "con_.h"
#include "engineapi.h"
#include "errmsg.h"
#include "feature.h"
#include "gamedata.h"
#include "hook.h"
#include "intdefs.h"
#include "mem.h"
#include "vcall.h"

FEATURE("scalable raw mouse input")

// We reimplement m_rawinput by hooking cursor functions in the same way as
// RInput (it's way easier than replacing all the mouse-handling internals of
// the actual engine). We also take the same window class it does in order to
// either block it from being loaded redundantly, or be blocked if it's already
// loaded. If m_rawinput already exists, we do nothing; people should use the
// game's native raw input instead in that case.
//
// As an *additional* feature, we also implement hardware input scaling, meaning
// that some number of counts are required from the mouse in order to move the
// cursor a single unit in game. This is useful for doing "minisnaps" (imprecise
// but accurate mouse movements at high sensitivity) on mice which don't allow
// their CPI to be lowered very far. It's implemented either in our own raw
// input functionality, or by hooking the game's, as required.

#define USAGEPAGE_MOUSE 1
#define USAGE_MOUSE 2

static int cx, cy, rx = 0, ry = 0; // cursor xy, remainder xy
static union { // space saving
	void *inwin;
	void **vtable_insys;
} U;
#define inwin U.inwin
#define vtable_insys U.vtable_insys

DEF_CVAR_UNREG(m_rawinput, "Use Raw Input for mouse input (SST reimplementation)",
		0, CON_ARCHIVE | CON_HIDDEN)

DEF_CVAR_MINMAX(sst_mouse_factor, "Number of hardware mouse counts per step",
		1, 1, 20, /*CON_ARCHIVE |*/ CON_HIDDEN)

static ssize __stdcall inproc(void *wnd, uint msg, ssize wp, ssize lp) {
	switch (msg) {
		case WM_INPUT:;
			char buf[sizeof(RAWINPUTHEADER) + sizeof(RAWMOUSE) /* = 40 */];
			uint sz = sizeof(buf);
			if (GetRawInputData((void *)lp, RID_INPUT, buf, &sz,
					sizeof(RAWINPUTHEADER)) != -1) {
				RAWINPUT *ri = (RAWINPUT *)buf;
				if (ri->header.dwType == RIM_TYPEMOUSE) {
					int d = con_getvari(sst_mouse_factor);
					int dx = rx + ri->data.mouse.lLastX;
					int dy = ry + ri->data.mouse.lLastY;
					cx += dx / d; cy += dy / d; rx = dx % d; ry = dy % d;
				}
			}
			return 0;
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(wnd, msg, wp, lp);
}

typedef int (*__stdcall GetCursorPos_func)(POINT *p);
typedef uint (*VCALLCONV GetRawMouseAccumulators_func)(void *, int *, int *);
static union { // more cheeky space saving
	GetCursorPos_func orig_GetCursorPos;
	GetRawMouseAccumulators_func orig_GetRawMouseAccumulators;
} u2;
#define orig_GetCursorPos u2.orig_GetCursorPos
#define orig_GetRawMouseAccumulators u2.orig_GetRawMouseAccumulators

static int __stdcall hook_GetCursorPos(POINT *p) {
	if (!con_getvari(m_rawinput)) return orig_GetCursorPos(p);
	p->x = cx; p->y = cy;
	return 1;
}

typedef int (*__stdcall SetCursorPos_func)(int x, int y);
static SetCursorPos_func orig_SetCursorPos = 0;
static int __stdcall hook_SetCursorPos(int x, int y) {
	cx = x; cy = y;
	return orig_SetCursorPos(x, y);
}

static uint VCALLCONV hook_GetRawMouseAccumulators(void *this, int *x, int *y) {
	int dx, dy;
	uint ret = orig_GetRawMouseAccumulators(this, &dx, &dy);
	int d = con_getvari(sst_mouse_factor);
	dx += rx; dy += ry;
	*x = dx / d; *y = dy / d; rx = dx % d; ry = dy % d;
	// NOTE! This is usually void, but apparently returns a bool in the 2013
	// SDK, for reasons I didn't bother researching. In any case, we can just
	// unconditionally preserve EAX and it won't do any harm.
	return ret;
}

INIT {
	bool has_rawinput = !!con_findvar("m_rawinput");
	if (has_rawinput) {
		if (!has_vtidx_GetRawMouseAccumulators) return false;
		if (!inputsystem) return false;
		vtable_insys = mem_loadptr(inputsystem);
		// XXX: this is kind of duping nosleep, but that won't always init...
		if (!os_mprot(vtable_insys + vtidx_GetRawMouseAccumulators,
				sizeof(void *), PAGE_READWRITE)) {
			errmsg_errorx("couldn't make virtual table writable");
			return false;
		}
		orig_GetRawMouseAccumulators = (GetRawMouseAccumulators_func)hook_vtable(
				vtable_insys, vtidx_GetRawMouseAccumulators,
				(void *)&hook_GetRawMouseAccumulators);
	}
	else {
		// create cvar hidden so config is still preserved if we fail to init
		con_reg(m_rawinput);
	}
	WNDCLASSEXW wc = {
		.cbSize = sizeof(wc),
		// cast because inproc is binary-compatible but doesn't use stupid
		// microsoft typedefs
		.lpfnWndProc = (WNDPROC)&inproc,
		.lpszClassName = L"RInput"
	};
	if (!RegisterClassExW(&wc)) {
		struct rgba gold = {255, 210, 0, 255};
		struct rgba blue = {45, 190, 190, 255};
		struct rgba white = {200, 200, 200, 255};
		con_colourmsg(&gold, "SST PROTIP! ");
		con_colourmsg(&blue, "It appears you're using RInput.exe.\n"
				"Consider launching without that and using ");
		con_colourmsg(&gold, "m_rawinput 1");
		con_colourmsg(&blue, " instead!\n");
		if (has_rawinput) {
			con_colourmsg(&white, "This is built into this version of game, and"
					" will also get provided by SST in older versions. ");
		}
		else {
			con_colourmsg(&white, "This option carries over to newer game "
				"versions that have it built-in. ");
		}
		con_colourmsg(&white, "No need for external programs :)\n");
		con_colourmsg(&gold, "Additionally");
		con_colourmsg(&blue, ", you can scale down the sensor input with ");
		con_colourmsg(&gold, "sst_mouse_factor");
		con_colourmsg(&blue, "!\n");
		return false;
	}
	if (has_rawinput) {
		// no real reason to keep this around receiving useless window messages
		UnregisterClassW(L"RInput", 0);
		goto ok;
	}

	orig_GetCursorPos = (GetCursorPos_func)hook_inline((void *)&GetCursorPos,
			(void *)&hook_GetCursorPos);
	if (!orig_GetCursorPos) {
		errmsg_errorsys("couldn't hook %s", "GetCursorPos");
		goto e0;
	}
	orig_SetCursorPos = (SetCursorPos_func)hook_inline((void *)&SetCursorPos,
			(void *)&hook_SetCursorPos);
	if (!orig_SetCursorPos) {
		errmsg_errorsys("couldn't hook %s", "SetCursorPos");
		goto e1;
	}
	inwin = CreateWindowExW(0, L"RInput", L"RInput", 0, 0, 0, 0, 0, 0, 0, 0, 0);
	if (!inwin) {
		errmsg_errorsys("couldn't create input window");
		goto e2;
	}
	RAWINPUTDEVICE rd = {
		.hwndTarget = inwin,
		.usUsagePage = USAGEPAGE_MOUSE,
		.usUsage = USAGE_MOUSE
	};
	if (!RegisterRawInputDevices(&rd, 1, sizeof(rd))) {
		errmsg_errorsys("couldn't create raw mouse device");
		goto e3;
	}

ok:	m_rawinput->base.flags &= ~CON_HIDDEN;
	sst_mouse_factor->base.flags &= ~CON_HIDDEN;
	return true;

e3:	DestroyWindow(inwin);
e2:	unhook_inline((void *)orig_SetCursorPos);
e1:	unhook_inline((void *)orig_GetCursorPos);
e0:	UnregisterClassW(L"RInput", 0);
	return false;
}

END {
	if (orig_SetCursorPos) { // if null, we didn't init our own implementation
		RAWINPUTDEVICE rd = {
			.dwFlags = RIDEV_REMOVE,
			.hwndTarget = 0,
			.usUsagePage = USAGEPAGE_MOUSE,
			.usUsage = USAGE_MOUSE
		};
		RegisterRawInputDevices(&rd, 1, sizeof(rd));
		DestroyWindow(inwin);
		UnregisterClassW(L"RInput", 0);
		unhook_inline((void *)orig_GetCursorPos);
		unhook_inline((void *)orig_SetCursorPos);
	}
	else {
		unhook_vtable(vtable_insys, vtidx_GetRawMouseAccumulators,
				(void *)orig_GetRawMouseAccumulators);
	}
}

// vi: sw=4 ts=4 noet tw=80 cc=80