mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
touch: inject mouse and poke into native touch modes (#815)
## Link to GitHub Issue or related Pull Request, if one exists Part 1 of many fixes for #814. ## Description of change For the native touch hook - add code to inject synthetic touches using Windows API (`InjectTouchInput`). Note that this is Windows 8+ only, but we can make an assumption that we are running on Win10+ for these games (TDJ/UFC/High Cheers) since the cabs assume Win10. Detect mouse events and allow IIDX poke to call into this to inject synthetic touches. Also, update the nativetouchhook to independently calculate window size and rotation, instead of relying on rawinput layer. ## Testing Tested to work with native touch option on IIDX, SDVX, POPN, all full screen / windowed / sub on/off combinations. Edge cases: * sdvx main monitor rotated 270 deg * touch invert option * windowed mode resize / moved All seem to work.
This commit is contained in:
@@ -572,7 +572,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
misc/device.cpp
|
misc/device.cpp
|
||||||
misc/eamuse.cpp
|
misc/eamuse.cpp
|
||||||
misc/extdev.cpp
|
misc/extdev.cpp
|
||||||
misc/nativetouchhook.cpp
|
|
||||||
misc/sciunit.cpp
|
misc/sciunit.cpp
|
||||||
misc/sde.cpp
|
misc/sde.cpp
|
||||||
misc/wintouchemu.cpp
|
misc/wintouchemu.cpp
|
||||||
@@ -657,6 +656,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
# touch
|
# touch
|
||||||
touch/gdi_overlay.cpp
|
touch/gdi_overlay.cpp
|
||||||
|
touch/native/inject.cpp
|
||||||
|
touch/native/inject_mouse.cpp
|
||||||
|
touch/native/inject_synthetic.cpp
|
||||||
|
touch/native/nativetouchhook.cpp
|
||||||
|
touch/native/transform.cpp
|
||||||
touch/touch.cpp
|
touch/touch.cpp
|
||||||
touch/touch_gestures.cpp
|
touch/touch_gestures.cpp
|
||||||
touch/win7.cpp
|
touch/win7.cpp
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#include "hooks/sleephook.h"
|
#include "hooks/sleephook.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "misc/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -351,7 +351,7 @@ namespace games::iidx {
|
|||||||
devicehook_init(avs::core::DLL_INSTANCE);
|
devicehook_init(avs::core::DLL_INSTANCE);
|
||||||
|
|
||||||
if (NATIVE_TOUCH) {
|
if (NATIVE_TOUCH) {
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
} else {
|
} else {
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
|
|||||||
+27
-101
@@ -11,22 +11,11 @@
|
|||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "overlay/windows/generic_sub.h"
|
#include "overlay/windows/generic_sub.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "touch/native/inject.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/libutils.h"
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
#define POKE_NATIVE_TOUCH 0
|
|
||||||
|
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
|
||||||
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
|
|
||||||
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
|
|
||||||
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
|
|
||||||
static InjectTouchInput_t pInjectTouchInput = nullptr;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace games::iidx::poke {
|
namespace games::iidx::poke {
|
||||||
|
|
||||||
static std::thread *THREAD = nullptr;
|
static std::thread *THREAD = nullptr;
|
||||||
@@ -116,73 +105,15 @@ namespace games::iidx::poke {
|
|||||||
{"1/D", 50},
|
{"1/D", 50},
|
||||||
};
|
};
|
||||||
|
|
||||||
#if POKE_NATIVE_TOUCH
|
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||||
void initialize_native_touch_library() {
|
if (touch_points.empty()) {
|
||||||
if (USER32_INSTANCE == nullptr) {
|
|
||||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
|
|
||||||
USER32_INSTANCE, "InitializeTouchInjection");
|
|
||||||
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
|
|
||||||
USER32_INSTANCE, "InjectTouchInput");
|
|
||||||
}
|
|
||||||
|
|
||||||
void emulate_native_touch(TouchPoint tp, bool is_down) {
|
|
||||||
if (pInjectTouchInput == nullptr) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINTER_TOUCH_INFO contact;
|
const auto &touch = touch_points.front();
|
||||||
BOOL bRet = TRUE;
|
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||||
const int contact_offset = 2;
|
|
||||||
|
|
||||||
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
|
|
||||||
|
|
||||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
|
||||||
contact.pointerInfo.pointerId = 0;
|
|
||||||
contact.pointerInfo.ptPixelLocation.x = tp.x;
|
|
||||||
contact.pointerInfo.ptPixelLocation.y = tp.y;
|
|
||||||
if (is_down) {
|
|
||||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
|
||||||
} else {
|
|
||||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
|
|
||||||
}
|
|
||||||
|
|
||||||
contact.pointerInfo.dwTime = 0;
|
|
||||||
contact.pointerInfo.PerformanceCount = 0;
|
|
||||||
|
|
||||||
contact.touchFlags = TOUCH_FLAG_NONE;
|
|
||||||
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
|
|
||||||
contact.orientation = 0;
|
|
||||||
contact.pressure = 1024;
|
|
||||||
|
|
||||||
contact.rcContact.top = tp.y - contact_offset;
|
|
||||||
contact.rcContact.bottom = tp.y + contact_offset;
|
|
||||||
contact.rcContact.left = tp.x - contact_offset;
|
|
||||||
contact.rcContact.right = tp.x + contact_offset;
|
|
||||||
|
|
||||||
bRet = InjectTouchInput(1, &contact);
|
|
||||||
if (!bRet) {
|
|
||||||
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
|
||||||
int i = 0;
|
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
emulate_native_touch(touch, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
emulate_native_touch(touch, false);
|
|
||||||
}
|
|
||||||
touch_points->clear();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||||
std::vector<DWORD> touch_ids;
|
std::vector<DWORD> touch_ids;
|
||||||
for (auto &touch : *touch_points) {
|
for (auto &touch : *touch_points) {
|
||||||
@@ -209,36 +140,19 @@ namespace games::iidx::poke {
|
|||||||
|
|
||||||
std::vector<TouchPoint> touch_points;
|
std::vector<TouchPoint> touch_points;
|
||||||
std::vector<uint16_t> last_keypad_state = {0, 0};
|
std::vector<uint16_t> last_keypad_state = {0, 0};
|
||||||
|
const bool use_native = games::iidx::NATIVE_TOUCH;
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
bool use_native = games::iidx::NATIVE_TOUCH;
|
|
||||||
|
|
||||||
if (use_native) {
|
|
||||||
initialize_native_touch_library();
|
|
||||||
|
|
||||||
if (pInitializeTouchInjection != nullptr) {
|
|
||||||
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
bool use_native = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// set variable to false to stop
|
// set variable to false to stop
|
||||||
while (THREAD_RUNNING) {
|
while (THREAD_RUNNING) {
|
||||||
|
|
||||||
// clean up touch from last frame
|
// clean up touch from last frame
|
||||||
if (touch_points.size() > 0) {
|
if (touch_points.size() > 0) {
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
if (use_native) {
|
if (use_native) {
|
||||||
clear_native_touch_points(&touch_points);
|
inject_native_touch_points(touch_points, false);
|
||||||
|
touch_points.clear();
|
||||||
} else {
|
} else {
|
||||||
clear_touch_points(&touch_points);
|
clear_touch_points(&touch_points);
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
clear_touch_points(&touch_points);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int unit = 0; unit < 2; unit++) {
|
for (int unit = 0; unit < 2; unit++) {
|
||||||
@@ -264,8 +178,16 @@ namespace games::iidx::poke {
|
|||||||
float x = x_iter->second / 1920.0;
|
float x = x_iter->second / 1920.0;
|
||||||
float y = y_iter->second / 1080.0;
|
float y = y_iter->second / 1080.0;
|
||||||
if (use_native) {
|
if (use_native) {
|
||||||
x *= rawinput::TOUCHSCREEN_RANGE_X;
|
if (GRAPHICS_WINDOWED) {
|
||||||
y *= rawinput::TOUCHSCREEN_RANGE_Y;
|
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||||
|
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||||
|
} else {
|
||||||
|
x = x_iter->second;
|
||||||
|
y = y_iter->second;
|
||||||
|
}
|
||||||
} else if (GRAPHICS_IIDX_WSUB) {
|
} else if (GRAPHICS_IIDX_WSUB) {
|
||||||
// Scale to windowed subscreen
|
// Scale to windowed subscreen
|
||||||
x *= GRAPHICS_WSUB_WIDTH;
|
x *= GRAPHICS_WSUB_WIDTH;
|
||||||
@@ -309,21 +231,25 @@ namespace games::iidx::poke {
|
|||||||
} // for all units
|
} // for all units
|
||||||
|
|
||||||
if (touch_points.size() > 0) {
|
if (touch_points.size() > 0) {
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
if (use_native) {
|
if (use_native) {
|
||||||
emulate_native_touch_points(&touch_points);
|
inject_native_touch_points(touch_points, true);
|
||||||
} else {
|
} else {
|
||||||
touch_write_points(&touch_points);
|
touch_write_points(&touch_points);
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
touch_write_points(&touch_points);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(50);
|
timer.sleep(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!touch_points.empty()) {
|
||||||
|
if (use_native) {
|
||||||
|
inject_native_touch_points(touch_points, false);
|
||||||
|
} else {
|
||||||
|
clear_touch_points(&touch_points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "misc/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
|
||||||
namespace games::popn {
|
namespace games::popn {
|
||||||
@@ -715,14 +715,12 @@ namespace games::popn {
|
|||||||
// 00000100 0000000B 00000001 (button 9)
|
// 00000100 0000000B 00000001 (button 9)
|
||||||
// set third column to 0 and it will work with BIO2
|
// set third column to 0 and it will work with BIO2
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (NATIVE_TOUCH) {
|
||||||
if (NATIVE_TOUCH) {
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
} else if (!GRAPHICS_WINDOWED) {
|
||||||
} else {
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
||||||
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sysutils::hook_EnumDisplayDevicesA();
|
sysutils::hook_EnumDisplayDevicesA();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "misc/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
@@ -456,7 +456,7 @@ namespace games::sdvx {
|
|||||||
|
|
||||||
if (is_valkyrie_model()) {
|
if (is_valkyrie_model()) {
|
||||||
if (NATIVETOUCH) {
|
if (NATIVETOUCH) {
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
||||||
// hook touch window
|
// hook touch window
|
||||||
// in windowed mode, game can accept mouse input on the second screen
|
// in windowed mode, game can accept mouse input on the second screen
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
#include "touch/native/inject.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
|
||||||
@@ -797,7 +798,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nWidth = rect.right - rect.left;
|
nWidth = rect.right - rect.left;
|
||||||
nHeight = rect.bottom - rect.top;
|
nHeight = rect.bottom - rect.top;
|
||||||
|
|
||||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
if (games::iidx::NATIVE_TOUCH) {
|
||||||
|
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
} else {
|
||||||
|
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||||
log_info(
|
log_info(
|
||||||
@@ -1163,6 +1168,13 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
|||||||
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
||||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||||
|
|
||||||
|
const bool native_touch_overlay =
|
||||||
|
(games::iidx::NATIVE_TOUCH && !GRAPHICS_IIDX_WSUB) ||
|
||||||
|
(games::popn::NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||||
|
if (native_touch_overlay) {
|
||||||
|
nativetouch::inject::register_and_attach_window(hWnd);
|
||||||
|
}
|
||||||
|
|
||||||
// NOLEGACY causes WM_CHAR to be not received
|
// NOLEGACY causes WM_CHAR to be not received
|
||||||
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
||||||
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ namespace overlay::windows {
|
|||||||
this->disabled_message = "Close this overlay and use the second window. (try ALT+TAB)";
|
this->disabled_message = "Close this overlay and use the second window. (try ALT+TAB)";
|
||||||
} else if (games::iidx::IIDX_TDJ_MONITOR_WARNING) {
|
} else if (games::iidx::IIDX_TDJ_MONITOR_WARNING) {
|
||||||
this->disabled_message = "TDJ mode subscreen overlay is not compatible with -dxmainadapter option; use -mainmonitor instead";
|
this->disabled_message = "TDJ mode subscreen overlay is not compatible with -dxmainadapter option; use -mainmonitor instead";
|
||||||
} else if (games::iidx::NATIVE_TOUCH) {
|
|
||||||
this->disabled_message = "Overlay disabled by user (-iidxnativetouch option is on, used for real touch screens)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float size = 0.5f;
|
float size = 0.5f;
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ namespace overlay::windows {
|
|||||||
this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!";
|
this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!";
|
||||||
} else if (games::popn::SHOW_PIKA_MONITOR_WARNING) {
|
} else if (games::popn::SHOW_PIKA_MONITOR_WARNING) {
|
||||||
this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
|
this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
|
||||||
} else if (games::popn::NATIVE_TOUCH) {
|
|
||||||
this->disabled_message = "Overlay disabled by user (-popnnativetouch option is on, used for real touch screens)";
|
|
||||||
} else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
} else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB).";
|
this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB).";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ namespace overlay::windows {
|
|||||||
this->disabled_message = "Valkyrie Model mode is not enabled!";
|
this->disabled_message = "Valkyrie Model mode is not enabled!";
|
||||||
} else if (games::sdvx::SHOW_VM_MONITOR_WARNING) {
|
} else if (games::sdvx::SHOW_VM_MONITOR_WARNING) {
|
||||||
this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
|
this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
|
||||||
} else if (games::sdvx::NATIVETOUCH) {
|
|
||||||
this->disabled_message = "Overlay disabled by user (-sdvxnativetouch option is on, used for real touch screens)";
|
|
||||||
} else if (GRAPHICS_WINDOWED) {
|
} else if (GRAPHICS_WINDOWED) {
|
||||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub).";
|
this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub).";
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||||
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <commctrl.h>
|
||||||
|
|
||||||
|
#include "inject.h"
|
||||||
|
#include "inject_internal.h"
|
||||||
|
#include "transform.h"
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/libutils.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
|
constexpr DWORD INJECTION_RETRY_DELAY_MS = 1;
|
||||||
|
constexpr POINTER_FLAGS CONTACT_DOWN_FLAGS =
|
||||||
|
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
||||||
|
constexpr POINTER_FLAGS CONTACT_UPDATE_FLAGS =
|
||||||
|
POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
||||||
|
|
||||||
|
// Windows 8 APIs are resolved dynamically to preserve older-OS compatibility
|
||||||
|
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
|
||||||
|
static decltype(InitializeTouchInjection) *InitializeTouchInjection_ptr = nullptr;
|
||||||
|
static decltype(InjectTouchInput) *InjectTouchInput_ptr = nullptr;
|
||||||
|
|
||||||
|
// state for the single synthetic touch contact
|
||||||
|
struct ContactState {
|
||||||
|
ContactOwner owner = ContactOwner::None;
|
||||||
|
HWND input_window = nullptr;
|
||||||
|
POINT position {};
|
||||||
|
UINT_PTR timer_id = 0;
|
||||||
|
|
||||||
|
bool is_active() const {
|
||||||
|
return owner != ContactOwner::None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// tracks an injector-owned contact without matching unrelated hardware touches
|
||||||
|
struct SyntheticTouchIdentity {
|
||||||
|
POINT down_position {};
|
||||||
|
HANDLE source = nullptr;
|
||||||
|
DWORD id = 0;
|
||||||
|
bool identified = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool transform_coordinates = false;
|
||||||
|
std::atomic<HWND> transform_window { nullptr };
|
||||||
|
|
||||||
|
void begin(POINT position, HWND window, bool transform_returned_coordinates) {
|
||||||
|
down_position = position;
|
||||||
|
source = nullptr;
|
||||||
|
id = 0;
|
||||||
|
identified = false;
|
||||||
|
pending = true;
|
||||||
|
transform_coordinates = transform_returned_coordinates;
|
||||||
|
transform_window.store(window, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset(HWND expected_window) {
|
||||||
|
source = nullptr;
|
||||||
|
id = 0;
|
||||||
|
identified = false;
|
||||||
|
pending = false;
|
||||||
|
transform_coordinates = false;
|
||||||
|
transform_window.compare_exchange_strong(
|
||||||
|
expected_window, nullptr, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND get_transform_window() const {
|
||||||
|
return transform_window.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matches(PTOUCHINPUT point) {
|
||||||
|
// InjectTouchInput provides no application marker in TOUCHINPUT. Claim the first
|
||||||
|
// matching DOWN, then use the source and ID assigned by Windows for this contact.
|
||||||
|
if (!identified) {
|
||||||
|
// correlate the pending injection's pixel position
|
||||||
|
constexpr LONG POSITION_TOLERANCE = 200;
|
||||||
|
const auto delta_x = point->x - down_position.x * 100;
|
||||||
|
const auto delta_y = point->y - down_position.y * 100;
|
||||||
|
if (!pending || !(point->dwFlags & TOUCHEVENTF_DOWN) ||
|
||||||
|
delta_x < -POSITION_TOLERANCE || delta_x > POSITION_TOLERANCE ||
|
||||||
|
delta_y < -POSITION_TOLERANCE || delta_y > POSITION_TOLERANCE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the identity that remains stable through this contact's MOVE and UP.
|
||||||
|
source = point->hSource;
|
||||||
|
id = point->dwID;
|
||||||
|
identified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// require both the provider and contact identities to match.
|
||||||
|
return point->hSource == source && point->dwID == id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool should_transform_coordinates() const {
|
||||||
|
return transform_coordinates;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static ContactState contact_state;
|
||||||
|
static SyntheticTouchIdentity synthetic_touch_identity;
|
||||||
|
|
||||||
|
// main game window that receives WM_TOUCH forwarded from the dedicated TDJ subscreen
|
||||||
|
static std::atomic<HWND> touch_delivery_window { nullptr };
|
||||||
|
|
||||||
|
// window whose UI thread owns synthetic contact state and synthetic touch requests;
|
||||||
|
// normally the active touch window, while dedicated TDJ uses the subscreen window
|
||||||
|
static std::atomic<HWND> injection_window { nullptr };
|
||||||
|
|
||||||
|
static std::once_flag initialization_once;
|
||||||
|
static int window_subclass_token;
|
||||||
|
|
||||||
|
// submit one synthetic contact frame to Windows touch injection
|
||||||
|
static bool inject_touch_frame(POINT position, POINTER_FLAGS pointer_flags) {
|
||||||
|
POINTER_TOUCH_INFO contact {};
|
||||||
|
contact.pointerInfo.pointerType = PT_TOUCH;
|
||||||
|
contact.pointerInfo.pointerId = 0;
|
||||||
|
contact.pointerInfo.pointerFlags = pointer_flags;
|
||||||
|
contact.pointerInfo.ptPixelLocation = position;
|
||||||
|
contact.touchFlags = TOUCH_FLAG_NONE;
|
||||||
|
|
||||||
|
BOOL result = InjectTouchInput_ptr(1, &contact);
|
||||||
|
if (result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if UPDATE fails, drop it
|
||||||
|
const auto error = GetLastError();
|
||||||
|
if (error == ERROR_NOT_READY && (pointer_flags & POINTER_FLAG_UPDATE)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if DOWN or UP fails, retry once after a brief delay; if it still fails, log a warning
|
||||||
|
if (error == ERROR_NOT_READY) {
|
||||||
|
Sleep(INJECTION_RETRY_DELAY_MS);
|
||||||
|
result = InjectTouchInput_ptr(1, &contact);
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool error_logged = false;
|
||||||
|
if (!error_logged) {
|
||||||
|
error_logged = true;
|
||||||
|
log_warning("touch::native", "failed to inject synthetic touch input: {}", GetLastError());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite only the contact created by this injector into game touch coordinates
|
||||||
|
bool transform_touch_input(PTOUCHINPUT point) {
|
||||||
|
const auto transform_window = synthetic_touch_identity.get_transform_window();
|
||||||
|
if (transform_window == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!synthetic_touch_identity.matches(point)) {
|
||||||
|
// this contact is not owned by this injector; leave it unchanged
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (synthetic_touch_identity.should_transform_coordinates()) {
|
||||||
|
// Windows receives the physical position; the game receives the mapped position
|
||||||
|
POINT position { point->x / 100, point->y / 100 };
|
||||||
|
if (transform::screen_to_game(transform_window, &position)) {
|
||||||
|
point->x = position.x * 100;
|
||||||
|
point->y = position.y * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point->dwFlags & TOUCHEVENTF_UP) {
|
||||||
|
synthetic_touch_identity.reset(transform_window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contact_is_active() {
|
||||||
|
return contact_state.is_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contact_is_owned_by(ContactOwner owner, HWND window) {
|
||||||
|
return contact_state.is_active() &&
|
||||||
|
contact_state.owner == owner &&
|
||||||
|
contact_state.input_window == window;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool begin_contact(
|
||||||
|
ContactOwner owner,
|
||||||
|
HWND window,
|
||||||
|
POINT position,
|
||||||
|
bool transform_returned_coordinates) {
|
||||||
|
contact_state.owner = owner;
|
||||||
|
contact_state.input_window = window;
|
||||||
|
contact_state.position = position;
|
||||||
|
contact_state.timer_id = 0;
|
||||||
|
synthetic_touch_identity.begin(position, window, transform_returned_coordinates);
|
||||||
|
|
||||||
|
if (inject_touch_frame(position, CONTACT_DOWN_FLAGS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
contact_state = {};
|
||||||
|
synthetic_touch_identity.reset(window);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_contact(ContactOwner owner, HWND window, POINT position) {
|
||||||
|
if (!contact_is_owned_by(owner, window) ||
|
||||||
|
!inject_touch_frame(position, CONTACT_UPDATE_FLAGS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contact_state.position = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_contact_timer(
|
||||||
|
ContactOwner owner, HWND window, UINT_PTR timer_id) {
|
||||||
|
if (contact_is_owned_by(owner, window)) {
|
||||||
|
contact_state.timer_id = timer_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// end whichever producer currently owns the single synthetic contact
|
||||||
|
bool release_active_contact() {
|
||||||
|
if (!contact_state.is_active()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto input_window = contact_state.input_window;
|
||||||
|
if (input_window != nullptr && contact_state.timer_id != 0) {
|
||||||
|
KillTimer(input_window, contact_state.timer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = inject_touch_frame(contact_state.position, POINTER_FLAG_UP);
|
||||||
|
contact_state = {};
|
||||||
|
|
||||||
|
// release mouse capture if this contact owns it
|
||||||
|
if (input_window != nullptr && GetCapture() == input_window) {
|
||||||
|
ReleaseCapture();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// translate primary mouse messages on a touch window into one touch contact
|
||||||
|
static LRESULT CALLBACK touch_window_subclass_proc(
|
||||||
|
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
|
||||||
|
UINT_PTR subclass_id, DWORD_PTR) {
|
||||||
|
|
||||||
|
// IIDX handles touch on its main window even when input belongs to the subscreen
|
||||||
|
// forward the touch message there
|
||||||
|
if (message == WM_TOUCH &&
|
||||||
|
transform::is_tdj_dedicated_subscreen(window)) {
|
||||||
|
const auto delivery_window =
|
||||||
|
touch_delivery_window.load(std::memory_order_acquire);
|
||||||
|
if (delivery_window != nullptr) {
|
||||||
|
return SendMessageW(delivery_window, message, w_param, l_param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle_synthetic_message(window, message, w_param, l_param) ||
|
||||||
|
handle_mouse_message(window, message, w_param)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_NCDESTROY) {
|
||||||
|
if (contact_state.input_window == window) {
|
||||||
|
release_active_contact();
|
||||||
|
}
|
||||||
|
HWND expected_window = window;
|
||||||
|
injection_window.compare_exchange_strong(
|
||||||
|
expected_window, nullptr, std::memory_order_acq_rel);
|
||||||
|
touch_delivery_window.store(nullptr, std::memory_order_release);
|
||||||
|
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefSubclassProc(window, message, w_param, l_param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// attach mouse injection without replacing the window's existing procedure
|
||||||
|
static void attach_window_impl(HWND window, bool register_touch) {
|
||||||
|
initialize_touch_injection();
|
||||||
|
|
||||||
|
if (!touch_injection_available()) {
|
||||||
|
if (register_touch) {
|
||||||
|
log_warning(
|
||||||
|
"touch::native",
|
||||||
|
"touch injection unavailable; touch window was not registered");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if requested, register for touch messages (for cases where the game didn't call it)
|
||||||
|
if (register_touch &&
|
||||||
|
(RegisterTouchWindow_orig == nullptr ||
|
||||||
|
!RegisterTouchWindow_orig(window, TWF_WANTPALM))) {
|
||||||
|
log_warning(
|
||||||
|
"touch::native", "failed to register mouse touch window: {}", GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set window subclass to intercept messages
|
||||||
|
if (!SetWindowSubclass(
|
||||||
|
window,
|
||||||
|
touch_window_subclass_proc,
|
||||||
|
reinterpret_cast<UINT_PTR>(&window_subclass_token),
|
||||||
|
0)) {
|
||||||
|
log_warning("touch::native", "failed to attach mouse touch injection to window: {}", GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish the UI-thread target for synthetic touch requests
|
||||||
|
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
|
||||||
|
injection_window.store(window, std::memory_order_release);
|
||||||
|
}
|
||||||
|
log_misc(
|
||||||
|
"touch::native", "mouse touch injection attached to window {}", fmt::ptr(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND get_injection_window() {
|
||||||
|
return injection_window.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void attach_window(HWND window) {
|
||||||
|
// attach window, but don't register for Windows touch messages
|
||||||
|
attach_window_impl(window, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_and_attach_window(HWND window) {
|
||||||
|
// attach window and register for Windows touch messages
|
||||||
|
attach_window_impl(window, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preserve native registration and attach mouse injection to the touch window
|
||||||
|
static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {
|
||||||
|
|
||||||
|
// for TDJ in windowed subscreen mode, the main game window is
|
||||||
|
// target for delivering touch messages, not the subscreen
|
||||||
|
if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
|
||||||
|
touch_delivery_window.store(window, std::memory_order_release);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// call original
|
||||||
|
const auto result = RegisterTouchWindow_orig(window, flags);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// attach but don't register for touch messages
|
||||||
|
// (we're already in the middle of RegisterTouchWindow as a result
|
||||||
|
// of the game calling it)
|
||||||
|
attach_window(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_touch_injection_functions() {
|
||||||
|
InitializeTouchInjection_ptr = nullptr;
|
||||||
|
InjectTouchInput_ptr = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load and initialize Windows 8 touch injection without a static API dependency
|
||||||
|
void initialize_touch_injection() {
|
||||||
|
std::call_once(initialization_once, [] {
|
||||||
|
initialize_synthetic_touch();
|
||||||
|
|
||||||
|
// load all APIs from user32 without adding static imports
|
||||||
|
const auto user32 = libutils::load_library("user32.dll");
|
||||||
|
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
|
||||||
|
user32, "InitializeTouchInjection");
|
||||||
|
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
|
||||||
|
user32, "InjectTouchInput");
|
||||||
|
|
||||||
|
if (InitializeTouchInjection_ptr == nullptr ||
|
||||||
|
InjectTouchInput_ptr == nullptr) {
|
||||||
|
clear_touch_injection_functions();
|
||||||
|
log_warning(
|
||||||
|
"touch::native", "touch injection API unavailable; mouse touch injection disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserve one synthetic contact and disable Windows' visual touch feedback
|
||||||
|
if (!InitializeTouchInjection_ptr(1, TOUCH_FEEDBACK_NONE)) {
|
||||||
|
log_warning(
|
||||||
|
"touch::native", "failed to initialize mouse touch injection: {}", GetLastError());
|
||||||
|
clear_touch_injection_functions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("touch::native", "mouse touch injection initialized");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch_injection_available() {
|
||||||
|
return InjectTouchInput_ptr != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// install injection support for touch windows registered by the game module
|
||||||
|
void hook(HMODULE module) {
|
||||||
|
initialize_touch_injection();
|
||||||
|
|
||||||
|
RegisterTouchWindow_orig = detour::iat_try(
|
||||||
|
"RegisterTouchWindow", RegisterTouchWindowHook, module);
|
||||||
|
if (RegisterTouchWindow_orig != nullptr) {
|
||||||
|
log_misc("touch::native", "RegisterTouchWindow hooked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
struct tagTOUCHINPUT;
|
||||||
|
|
||||||
|
namespace nativetouch::inject {
|
||||||
|
void attach_window(HWND window);
|
||||||
|
void register_and_attach_window(HWND window);
|
||||||
|
void hook(HMODULE module);
|
||||||
|
bool inject_synthetic_touch(POINT position, bool down);
|
||||||
|
bool transform_touch_input(tagTOUCHINPUT *point);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
|
enum class ContactOwner {
|
||||||
|
None,
|
||||||
|
Mouse,
|
||||||
|
Synthetic,
|
||||||
|
};
|
||||||
|
|
||||||
|
void initialize_touch_injection();
|
||||||
|
void initialize_synthetic_touch();
|
||||||
|
bool touch_injection_available();
|
||||||
|
|
||||||
|
bool contact_is_active();
|
||||||
|
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
||||||
|
bool begin_contact(
|
||||||
|
ContactOwner owner,
|
||||||
|
HWND window,
|
||||||
|
POINT position,
|
||||||
|
bool transform_returned_coordinates);
|
||||||
|
bool update_contact(ContactOwner owner, HWND window, POINT position);
|
||||||
|
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
|
||||||
|
bool release_active_contact();
|
||||||
|
|
||||||
|
HWND get_injection_window();
|
||||||
|
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
|
||||||
|
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||||
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "inject_internal.h"
|
||||||
|
#include "transform.h"
|
||||||
|
|
||||||
|
#include "touch/touch.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
|
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
|
||||||
|
|
||||||
|
static int mouse_contact_timer_token;
|
||||||
|
|
||||||
|
struct PrimaryMouseButton {
|
||||||
|
UINT down_message;
|
||||||
|
UINT double_click_message;
|
||||||
|
UINT up_message;
|
||||||
|
WPARAM state_mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
// honor the user's swapped-button setting when choosing the primary button
|
||||||
|
static PrimaryMouseButton get_primary_mouse_button() {
|
||||||
|
if (GetSystemMetrics(SM_SWAPBUTTON)) {
|
||||||
|
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
|
||||||
|
}
|
||||||
|
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the current physical cursor but reject points outside the subscreen
|
||||||
|
static bool get_mouse_injection_position(HWND window, POINT *position) {
|
||||||
|
|
||||||
|
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
|
||||||
|
// Windows move its primary pointer back to stale positions during a drag.
|
||||||
|
if (!GetCursorPos(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT transformed = *position;
|
||||||
|
return transform::screen_to_game(window, &transformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// release the active injected contact and its window capture
|
||||||
|
static void end_mouse_contact(HWND window) {
|
||||||
|
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
|
release_active_contact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin a contact at the physical cursor position and capture future mouse input
|
||||||
|
static void begin_mouse_contact(HWND window) {
|
||||||
|
if (contact_is_active()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT position;
|
||||||
|
if (!get_mouse_injection_position(window, &position) ||
|
||||||
|
!begin_contact(ContactOwner::Mouse, window, position, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep receiving drag messages after the cursor leaves the client area
|
||||||
|
SetCapture(window);
|
||||||
|
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
||||||
|
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
||||||
|
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
||||||
|
} else {
|
||||||
|
log_warning("touch::native", "failed to start mouse touch injection timer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the contact while the primary button remains held
|
||||||
|
static void move_mouse_contact(
|
||||||
|
HWND window, WPARAM w_param, WPARAM primary_button_state) {
|
||||||
|
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT position;
|
||||||
|
if (!get_mouse_injection_position(window, &position)) {
|
||||||
|
end_mouse_contact(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((w_param & primary_button_state) == 0) {
|
||||||
|
end_mouse_contact(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_contact(ContactOwner::Mouse, window, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit stationary update frames so Windows keeps the contact alive
|
||||||
|
static void refresh_mouse_contact(HWND window) {
|
||||||
|
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT position {};
|
||||||
|
if (!GetCursorPos(&position)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT transformed = position;
|
||||||
|
if (!transform::screen_to_game(window, &transformed)) {
|
||||||
|
end_mouse_contact(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_contact(ContactOwner::Mouse, window, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
|
||||||
|
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
|
||||||
|
is_mouse_message_from_touchscreen()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_TIMER &&
|
||||||
|
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
|
||||||
|
refresh_mouse_contact(window);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto primary_button = get_primary_mouse_button();
|
||||||
|
if (message == primary_button.down_message ||
|
||||||
|
message == primary_button.double_click_message) {
|
||||||
|
begin_mouse_contact(window);
|
||||||
|
} else if (message == WM_MOUSEMOVE) {
|
||||||
|
move_mouse_contact(window, w_param, primary_button.state_mask);
|
||||||
|
} else if (message == primary_button.up_message) {
|
||||||
|
end_mouse_contact(window);
|
||||||
|
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
|
||||||
|
message == WM_CAPTURECHANGED) {
|
||||||
|
end_mouse_contact(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||||
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include "inject.h"
|
||||||
|
#include "inject_internal.h"
|
||||||
|
#include "transform.h"
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
|
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
||||||
|
|
||||||
|
static std::once_flag synthetic_initialization_once;
|
||||||
|
static int synthetic_contact_timer_token;
|
||||||
|
static UINT synthetic_touch_message;
|
||||||
|
|
||||||
|
void initialize_synthetic_touch() {
|
||||||
|
std::call_once(synthetic_initialization_once, [] {
|
||||||
|
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
|
||||||
|
if (synthetic_touch_message == 0) {
|
||||||
|
log_warning(
|
||||||
|
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// synthetic touches preempt the mouse and keep it disabled until release or timeout
|
||||||
|
static void begin_synthetic_contact(HWND window, POINT position) {
|
||||||
|
// dedicated TDJ injects into the physical subscreen, so map Windows'
|
||||||
|
// returned coordinates back into the game's touch coordinate space
|
||||||
|
const auto transform_returned_coordinates =
|
||||||
|
transform::is_tdj_dedicated_subscreen(window);
|
||||||
|
if (!transform::game_to_screen(window, &position)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!release_active_contact()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!begin_contact(
|
||||||
|
ContactOwner::Synthetic,
|
||||||
|
window,
|
||||||
|
position,
|
||||||
|
transform_returned_coordinates)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||||
|
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||||
|
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||||
|
} else {
|
||||||
|
log_warning("touch::native", "failed to start synthetic touch timeout timer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void end_synthetic_contact(HWND window) {
|
||||||
|
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||||
|
release_active_contact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handle_synthetic_message(
|
||||||
|
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||||
|
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
||||||
|
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
||||||
|
if (w_param) {
|
||||||
|
begin_synthetic_contact(window, position);
|
||||||
|
} else {
|
||||||
|
end_synthetic_contact(window);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_TIMER &&
|
||||||
|
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
|
||||||
|
end_synthetic_contact(window);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject synthetic touches; only one contact is supported at a time for simplicity
|
||||||
|
bool inject_synthetic_touch(POINT position, bool down) {
|
||||||
|
initialize_touch_injection();
|
||||||
|
|
||||||
|
const auto window = get_injection_window();
|
||||||
|
if (!touch_injection_available() || window == nullptr ||
|
||||||
|
synthetic_touch_message == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostMessageW(
|
||||||
|
window,
|
||||||
|
synthetic_touch_message,
|
||||||
|
static_cast<WPARAM>(down),
|
||||||
|
MAKELPARAM(position.x, position.y)) != FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
-17
@@ -3,9 +3,9 @@
|
|||||||
#define _WIN32_WINNT 0x0601
|
#define _WIN32_WINNT 0x0601
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "wintouchemu.h"
|
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "inject.h"
|
||||||
|
#include "transform.h"
|
||||||
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -20,9 +20,36 @@
|
|||||||
#define log_debug(module, format_str, ...)
|
#define log_debug(module, format_str, ...)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace nativetouchhook {
|
namespace nativetouch {
|
||||||
|
|
||||||
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||||
|
static bool native_display_initialized = false;
|
||||||
|
static DWORD native_display_orientation = DMDO_DEFAULT;
|
||||||
|
static long native_display_size_x = 1920L;
|
||||||
|
static long native_display_size_y = 1080L;
|
||||||
|
|
||||||
|
static void update_native_display_mode() {
|
||||||
|
RECT display_rect{};
|
||||||
|
if (GetWindowRect(GetDesktopWindow(), &display_rect)) {
|
||||||
|
native_display_size_x = display_rect.right - display_rect.left;
|
||||||
|
native_display_size_y = display_rect.bottom - display_rect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEVMODE display_mode{};
|
||||||
|
display_mode.dmSize = sizeof(display_mode);
|
||||||
|
if (EnumDisplaySettingsEx(nullptr, ENUM_CURRENT_SETTINGS, &display_mode, EDS_RAWMODE) &&
|
||||||
|
(display_mode.dmFields & DM_DISPLAYORIENTATION)) {
|
||||||
|
native_display_orientation = display_mode.dmDisplayOrientation;
|
||||||
|
} else {
|
||||||
|
log_info("touch::native", "failed to determine monitor orientation");
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"touch::native", "primary display mode: {}x{}, orientation {}",
|
||||||
|
native_display_size_x,
|
||||||
|
native_display_size_y,
|
||||||
|
native_display_orientation);
|
||||||
|
}
|
||||||
|
|
||||||
static void strip_contact_size(PTOUCHINPUT point) {
|
static void strip_contact_size(PTOUCHINPUT point) {
|
||||||
|
|
||||||
@@ -67,51 +94,72 @@ namespace nativetouchhook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void flip_touch_points(PTOUCHINPUT point) {
|
static void flip_touch_points(PTOUCHINPUT point) {
|
||||||
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
point->x = native_display_size_x * 100 - point->x;
|
||||||
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
point->y = native_display_size_y * 100 - point->y;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL WINAPI GetTouchInputInfoHook(
|
static BOOL WINAPI GetTouchInputInfoHook(
|
||||||
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||||
|
|
||||||
|
// Refresh after exclusive fullscreen establishes the final display mode.
|
||||||
|
if (!native_display_initialized) {
|
||||||
|
update_native_display_mode();
|
||||||
|
native_display_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
// call the original fist
|
// call the original fist
|
||||||
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool flip_values = false;
|
bool flip_hardware_touch = false;
|
||||||
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
|
if (avs::game::is_model("KFC")) {
|
||||||
log_debug(
|
log_debug(
|
||||||
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
|
"touch::native", "orientation = {}, display size = {}x{}",
|
||||||
rawinput::touch::DISPLAY_ORIENTATION,
|
native_display_orientation,
|
||||||
rawinput::touch::DISPLAY_SIZE_X,
|
native_display_size_x,
|
||||||
rawinput::touch::DISPLAY_SIZE_Y);
|
native_display_size_y);
|
||||||
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
if (native_display_orientation == DMDO_270) {
|
||||||
flip_values = true;
|
flip_hardware_touch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawinput::touch::INVERTED) {
|
|
||||||
flip_values = !flip_values;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = 0; i < cInputs; i++) {
|
for (size_t i = 0; i < cInputs; i++) {
|
||||||
PTOUCHINPUT point = &pInputs[i];
|
PTOUCHINPUT point = &pInputs[i];
|
||||||
|
|
||||||
|
const auto synthetic = inject::transform_touch_input(point);
|
||||||
|
|
||||||
if (avs::game::is_model("LDJ")) {
|
if (avs::game::is_model("LDJ")) {
|
||||||
strip_contact_size(point);
|
strip_contact_size(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto flip_values = !synthetic &&
|
||||||
|
(rawinput::touch::INVERTED ^ flip_hardware_touch);
|
||||||
if (flip_values) {
|
if (flip_values) {
|
||||||
flip_touch_points(point);
|
flip_touch_points(point);
|
||||||
}
|
}
|
||||||
|
if (!synthetic) {
|
||||||
|
POINT position { point->x / 100, point->y / 100 };
|
||||||
|
const auto transform_result =
|
||||||
|
transform::hardware_to_game(&position);
|
||||||
|
if (transform_result == transform::Result::Transformed) {
|
||||||
|
point->x = position.x * 100;
|
||||||
|
point->y = position.y * 100;
|
||||||
|
} else if (transform_result == transform::Result::Rejected &&
|
||||||
|
!(point->dwFlags & TOUCHEVENTF_UP)) {
|
||||||
|
// suppress rejected contacts, but preserve UP to release an active touch ID
|
||||||
|
point->dwFlags = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void hook(HMODULE module) {
|
void hook(HMODULE module) {
|
||||||
|
inject::hook(module);
|
||||||
|
|
||||||
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
||||||
if (GetTouchInputInfo_orig != nullptr) {
|
if (GetTouchInputInfo_orig != nullptr) {
|
||||||
log_misc("touch::native", "GetTouchInputInfo hooked");
|
log_misc("touch::native", "GetTouchInputInfo hooked");
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace nativetouchhook {
|
namespace nativetouch {
|
||||||
void hook(HMODULE module);
|
void hook(HMODULE module);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#include "transform.h"
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
#include "touch/touch.h"
|
||||||
|
|
||||||
|
namespace nativetouch::transform {
|
||||||
|
|
||||||
|
bool is_tdj_dedicated_subscreen(HWND window) {
|
||||||
|
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
||||||
|
window == TDJ_SUBSCREEN_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert game touch coordinates to physical screen coordinates for dedicated subscreen injection
|
||||||
|
bool game_to_screen(HWND window, POINT *position) {
|
||||||
|
if (!is_tdj_dedicated_subscreen(window)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT client_rect {};
|
||||||
|
if (!GetClientRect(window, &client_rect) ||
|
||||||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
|
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
position->x = MulDiv(
|
||||||
|
position->x - SPICETOUCH_TOUCH_X,
|
||||||
|
client_rect.right,
|
||||||
|
SPICETOUCH_TOUCH_WIDTH);
|
||||||
|
position->y = MulDiv(
|
||||||
|
position->y - SPICETOUCH_TOUCH_Y,
|
||||||
|
client_rect.bottom,
|
||||||
|
SPICETOUCH_TOUCH_HEIGHT);
|
||||||
|
return ClientToScreen(window, position) != FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool has_active_overlay_transform() {
|
||||||
|
return overlay::OVERLAY != nullptr &&
|
||||||
|
overlay::OVERLAY->get_active() &&
|
||||||
|
overlay::OVERLAY->can_transform_touch_input();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool transform_overlay_touch_position(POINT *position) {
|
||||||
|
// convert physical screen coordinates to the window-relative coordinates the overlay expects
|
||||||
|
if (GRAPHICS_WINDOWED) {
|
||||||
|
position->x -= SPICETOUCH_TOUCH_X;
|
||||||
|
position->y -= SPICETOUCH_TOUCH_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask the overlay to do the game-specific translation
|
||||||
|
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert physical screen coordinates to game touch coordinates for a known target
|
||||||
|
bool screen_to_game(HWND window, POINT *position) {
|
||||||
|
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
||||||
|
if (is_tdj_dedicated_subscreen(window)) {
|
||||||
|
RECT client_rect {};
|
||||||
|
if (!GetClientRect(window, &client_rect) ||
|
||||||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
|
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ScreenToClient(window, position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!PtInRect(&client_rect, *position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
position->x = SPICETOUCH_TOUCH_X +
|
||||||
|
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
|
||||||
|
position->y = SPICETOUCH_TOUCH_Y +
|
||||||
|
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if subscreen overlay is active and can transform the touch point;
|
||||||
|
// if not, the touch point is valid as-is
|
||||||
|
if (!has_active_overlay_transform()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask the overlay to transform the touch point into game coordinates
|
||||||
|
return transform_overlay_touch_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
||||||
|
Result hardware_to_game(POINT *position) {
|
||||||
|
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
const auto active_overlay = has_active_overlay_transform();
|
||||||
|
|
||||||
|
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
||||||
|
if (!dedicated_subscreen && !active_overlay) {
|
||||||
|
return Result::Unchanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// route through the dedicated subscreen when active, otherwise through the overlay
|
||||||
|
const auto valid = screen_to_game(
|
||||||
|
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
|
||||||
|
position);
|
||||||
|
|
||||||
|
// reject out-of-bounds points and any coordinate conversion failure
|
||||||
|
return valid ? Result::Transformed : Result::Rejected;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace nativetouch::transform {
|
||||||
|
enum class Result {
|
||||||
|
Unchanged,
|
||||||
|
Transformed,
|
||||||
|
Rejected,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_tdj_dedicated_subscreen(HWND window);
|
||||||
|
bool game_to_screen(HWND window, POINT *position);
|
||||||
|
bool screen_to_game(HWND window, POINT *position);
|
||||||
|
Result hardware_to_game(POINT *position);
|
||||||
|
}
|
||||||
@@ -81,8 +81,6 @@ static const char *LOG_MODULE_NAME = "touch";
|
|||||||
|
|
||||||
static TouchHandler *TOUCH_HANDLER = nullptr;
|
static TouchHandler *TOUCH_HANDLER = nullptr;
|
||||||
|
|
||||||
static bool is_mouse_message_from_touchscreen();
|
|
||||||
|
|
||||||
TouchHandler::TouchHandler(std::string name) {
|
TouchHandler::TouchHandler(std::string name) {
|
||||||
log_info("touch", "Using touch handler: {}", name);
|
log_info("touch", "Using touch handler: {}", name);
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1001,7 @@ void update_spicetouch_window_dimensions(HWND hWnd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
||||||
static bool is_mouse_message_from_touchscreen() {
|
bool is_mouse_message_from_touchscreen() {
|
||||||
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
|
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
|
||||||
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
|
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
|
||||||
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
|
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ extern int SPICETOUCH_TOUCH_WIDTH;
|
|||||||
extern int SPICETOUCH_TOUCH_HEIGHT;
|
extern int SPICETOUCH_TOUCH_HEIGHT;
|
||||||
|
|
||||||
bool is_touch_available(LPCSTR caller);
|
bool is_touch_available(LPCSTR caller);
|
||||||
|
bool is_mouse_message_from_touchscreen();
|
||||||
|
|
||||||
void touch_attach_wnd(HWND hWnd);
|
void touch_attach_wnd(HWND hWnd);
|
||||||
void touch_attach_dx_hook();
|
void touch_attach_dx_hook();
|
||||||
|
|||||||
Reference in New Issue
Block a user