From 9a09c605402e6ff74f93f7fe7afc50ccc785acc3 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sun, 25 Dec 2022 11:03:50 +0000 Subject: Add basic mouse input scaling --- gamedata/inputsystem.kv | 8 +++ src/engineapi.c | 4 ++ src/engineapi.h | 1 + src/nosleep.c | 10 ++-- src/rinput.c | 135 +++++++++++++++++++++++++++++++++++++----------- src/sst.c | 63 +++++++++++----------- 6 files changed, 153 insertions(+), 68 deletions(-) diff --git a/gamedata/inputsystem.kv b/gamedata/inputsystem.kv index 1e5ab88..95fa51f 100644 --- a/gamedata/inputsystem.kv +++ b/gamedata/inputsystem.kv @@ -5,4 +5,12 @@ vtidx_SleepUntilInput { Portal2 34 // IAppSystem changes } +// XXX: This function won't always exist even when has_ is true. +// It's only used by rinput.c after checking that m_rawinput exists. +vtidx_GetRawMouseAccumulators { + L4D2 37 + 2013 39 + Portal2 50 +} + // vi: sw=4 ts=4 noet tw=80 cc=80 ft=plain diff --git a/src/engineapi.c b/src/engineapi.c index 8cc3dd6..b2272f9 100644 --- a/src/engineapi.c +++ b/src/engineapi.c @@ -40,6 +40,8 @@ struct VEngineServer *engserver; DECL_VFUNC(void *, GetGlobalVars, 1) void *globalvars; +void *inputsystem; + DECL_VFUNC_DYN(void *, GetAllServerClasses) DECL_VFUNC(int, GetEngineBuildNumber_newl4d2, 99) // duping gamedata entry, yuck @@ -71,6 +73,8 @@ bool engineapi_init(int pluginver) { void *pim = factory_server("PlayerInfoManager002", 0); if (pim) globalvars = GetGlobalVars(pim); + inputsystem = factory_inputsystem("InputSystemVersion001", 0); + void *srvdll; // TODO(compat): add this back when there's gamedata for 009 (no point atm) /*if (srvdll = factory_engine("ServerGameDLL009", 0)) { diff --git a/src/engineapi.h b/src/engineapi.h index 0f4fa8f..6394ec2 100644 --- a/src/engineapi.h +++ b/src/engineapi.h @@ -124,6 +124,7 @@ struct ServerClass { extern struct VEngineClient *engclient; extern struct VEngineServer *engserver; extern void *globalvars; +extern void *inputsystem; /* * Called on plugin init to attempt to initialise various core interfaces. diff --git a/src/nosleep.c b/src/nosleep.c index 07a5500..e75c6e6 100644 --- a/src/nosleep.c +++ b/src/nosleep.c @@ -20,12 +20,13 @@ #include "feature.h" #include "gamedata.h" #include "hook.h" +#include "mem.h" #include "os.h" #include "vcall.h" FEATURE("inactive window sleep adjustment") REQUIRE_GAMEDATA(vtidx_SleepUntilInput) -REQUIRE_GLOBAL(factory_inputsystem) +REQUIRE_GLOBAL(inputsystem) DEF_CVAR_UNREG(engine_no_focus_sleep, "Delay while tabbed out (SST reimplementation)", 50, @@ -46,12 +47,7 @@ PREINIT { } INIT { - void *insys = factory_inputsystem("InputSystemVersion001", 0); - if (!insys) { - errmsg_errorx("couldn't get input system interface"); - return false; - } - vtable = *(void ***)insys; + vtable = mem_loadptr(inputsystem); if (!os_mprot(vtable + vtidx_SleepUntilInput, sizeof(void *), PAGE_READWRITE)) { errmsg_errorx("couldn't make virtual table writable"); diff --git a/src/rinput.c b/src/rinput.c index a7ad8d8..e24fc1c 100644 --- a/src/rinput.c +++ b/src/rinput.c @@ -16,16 +16,22 @@ // 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 #include "con_.h" +#include "gamedata.h" #include "hook.h" +#include "engineapi.h" #include "errmsg.h" #include "feature.h" #include "intdefs.h" +#include "mem.h" +#include "vcall.h" -FEATURE("raw mouse input") +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 @@ -33,16 +39,31 @@ FEATURE("raw mouse input") // 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 long dx = 0, dy = 0; -static void *inwin; +static int cx, cy, rx = 0, ry = 0; // cursor xy, remainder xy +static union { // cheeky space saving + void *inwin; + void **vtable_insys; +} u1; +#define inwin u1.inwin +#define vtable_insys u1.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:; @@ -52,8 +73,10 @@ static ssize __stdcall inproc(void *wnd, uint msg, ssize wp, ssize lp) { sizeof(RAWINPUTHEADER)) != -1) { RAWINPUT *ri = (RAWINPUT *)buf; if (ri->header.dwType == RIM_TYPEMOUSE) { - dx += ri->data.mouse.lLastX; - dy += ri->data.mouse.lLastY; + 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; @@ -65,27 +88,59 @@ static ssize __stdcall inproc(void *wnd, uint msg, ssize wp, ssize lp) { } typedef int (*__stdcall GetCursorPos_func)(POINT *p); -static GetCursorPos_func orig_GetCursorPos; +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 = dx; p->y = dy; + p->x = cx; p->y = cy; return 1; } + typedef int (*__stdcall SetCursorPos_func)(int x, int y); -static SetCursorPos_func orig_SetCursorPos; +static SetCursorPos_func orig_SetCursorPos = 0; static int __stdcall hook_SetCursorPos(int x, int y) { - dx = x; dy = y; + cx = x; cy = y; return orig_SetCursorPos(x, y); } -PREINIT { - if (con_findvar("m_rawinput")) return false; // no need! - // create cvar hidden so if we fail to init, setting can still be preserved - con_reg(m_rawinput); - return true; +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 @@ -102,10 +157,26 @@ INIT { "Consider launching without that and using "); con_colourmsg(&gold, "m_rawinput 1"); con_colourmsg(&blue, " instead!\n"); - con_colourmsg(&white, "This option carries over to newer game versions " - "that have it built-in. No need for external programs :)\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); @@ -134,7 +205,8 @@ INIT { goto e3; } - m_rawinput->base.flags &= ~CON_HIDDEN; +ok: m_rawinput->base.flags &= ~CON_HIDDEN; + sst_mouse_factor->base.flags &= ~CON_HIDDEN; return true; e3: DestroyWindow(inwin); @@ -145,18 +217,23 @@ e0: UnregisterClassW(L"RInput", 0); } END { - 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); + 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 diff --git a/src/sst.c b/src/sst.c index a7eb331..d159c8e 100644 --- a/src/sst.c +++ b/src/sst.c @@ -266,22 +266,23 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { } factory_engine = enginef; factory_server = serverf; +#ifdef _WIN32 + void *inputsystemlib = GetModuleHandleW(L"inputsystem.dll"); +#else + // TODO(linux): assuming the above doesn't apply to this; check if it does! + // ... actually, there's a good chance this assumption is now wrong! + void *inputsystemlib = dlopen("bin/libinputsystem.so", + RTLD_NOW | RLTD_NOLOAD); + if (inputsystemlib) dlclose(inputsystemlib); // blegh +#endif + if (!inputsystemlib) { + errmsg_warndl("couldn't get the input system library"); + } + else if (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, + "CreateInterface"))) { + errmsg_warndl("couldn't get input system's CreateInterface"); + } if (!engineapi_init(ifacever)) return false; - - const void **p = vtable_firstdiff; - if (GAMETYPE_MATCHES(Portal2)) *p++ = (void *)&nop_p_v; // ClientFullyConnect - *p++ = (void *)&nop_p_v; // ClientDisconnect - *p++ = (void *)&nop_pp_v; // ClientPutInServer - *p++ = (void *)&SetCommandClient; // SetCommandClient - *p++ = (void *)&nop_p_v; // ClientSettingsChanged - *p++ = (void *)&nop_5pi_i; // ClientConnect - *p++ = ifacever > 1 ? (void *)&nop_pp_i : (void *)&nop_p_i; // ClientCommand - // remaining stuff here is backwards compatible, so added unconditionally - *p++ = (void *)&nop_pp_i; // NetworkIDValidated - *p++ = (void *)&nop_ipipp_v; // OnQueryCvarValueFinished (002+) - *p++ = (void *)&nop_p_v; // OnEdictAllocated - *p = (void *)&nop_p_v; // OnEdictFreed - #ifdef _WIN32 clientlib = GetModuleHandleW(gameinfo_clientlib); #else @@ -298,22 +299,20 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { "CreateInterface"))) { errmsg_warndl("couldn't get client's CreateInterface"); } -#ifdef _WIN32 - void *inputsystemlib = GetModuleHandleW(L"inputsystem.dll"); -#else - // TODO(linux): assuming the above doesn't apply to this; check if it does! - // ... actually, there's a good chance this assumption is now wrong! - void *inputsystemlib = dlopen("bin/libinputsystem.so", - RTLD_NOW | RLTD_NOLOAD); - if (inputsystemlib) dlclose(inputsystemlib); // blegh -#endif - if (!inputsystemlib) { - errmsg_warndl("couldn't get the input system library"); - } - else if (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, - "CreateInterface"))) { - errmsg_warndl("couldn't get input system's CreateInterface"); - } + + const void **p = vtable_firstdiff; + if (GAMETYPE_MATCHES(Portal2)) *p++ = (void *)&nop_p_v; // ClientFullyConnect + *p++ = (void *)&nop_p_v; // ClientDisconnect + *p++ = (void *)&nop_pp_v; // ClientPutInServer + *p++ = (void *)&SetCommandClient; // SetCommandClient + *p++ = (void *)&nop_p_v; // ClientSettingsChanged + *p++ = (void *)&nop_5pi_i; // ClientConnect + *p++ = ifacever > 1 ? (void *)&nop_pp_i : (void *)&nop_p_i; // ClientCommand + // remaining stuff here is backwards compatible, so added unconditionally + *p++ = (void *)&nop_pp_i; // NetworkIDValidated + *p++ = (void *)&nop_ipipp_v; // OnQueryCvarValueFinished (002+) + *p++ = (void *)&nop_p_v; // OnEdictAllocated + *p = (void *)&nop_p_v; // OnEdictFreed // NOTE: this is technically redundant for early versions but I CBA writing // a version check; it's easier to just do this unilaterally. @@ -467,7 +466,7 @@ static const void *vtable[MAX_VTABLE_FUNCS] = { (void *)&GetPluginDescription, (void *)&nop_p_v, // LevelInit (void *)&nop_pii_v, // ServerActivate - (void *)&GameFrame, // GameFrame + (void *)&GameFrame, (void *)&nop_v_v, // LevelShutdown (void *)&ClientActive // At this point, Alien Swarm and Portal 2 add ClientFullyConnect, so we -- cgit v1.2.3